mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
feat: 更新分析会话 API,支持分页和并发处理
This commit is contained in:
@@ -43,20 +43,22 @@ Parse `$ARGUMENTS` to extract `--role`. If absent → Orchestration Mode (auto r
|
|||||||
|
|
||||||
### Role Registry
|
### Role Registry
|
||||||
|
|
||||||
| Role | File | Task Prefix | Type |
|
| Role | File | Task Prefix | Type | Compact |
|
||||||
|------|------|-------------|------|
|
|------|------|-------------|------|---------|
|
||||||
| coordinator | roles/coordinator/role.md | (none) | orchestrator |
|
| coordinator | [roles/coordinator/role.md](roles/coordinator/role.md) | (none) | orchestrator | **⚠️ 压缩后必须重读** |
|
||||||
| analyst | roles/analyst/role.md | RESEARCH-* | pipeline |
|
| analyst | [roles/analyst/role.md](roles/analyst/role.md) | RESEARCH-* | pipeline | 压缩后必须重读 |
|
||||||
| writer | roles/writer/role.md | DRAFT-* | pipeline |
|
| writer | [roles/writer/role.md](roles/writer/role.md) | DRAFT-* | pipeline | 压缩后必须重读 |
|
||||||
| discussant | roles/discussant/role.md | DISCUSS-* | pipeline |
|
| discussant | [roles/discussant/role.md](roles/discussant/role.md) | DISCUSS-* | pipeline | 压缩后必须重读 |
|
||||||
| planner | roles/planner/role.md | PLAN-* | pipeline |
|
| planner | [roles/planner/role.md](roles/planner/role.md) | PLAN-* | pipeline | 压缩后必须重读 |
|
||||||
| executor | roles/executor/role.md | IMPL-* | pipeline |
|
| executor | [roles/executor/role.md](roles/executor/role.md) | IMPL-* | pipeline | 压缩后必须重读 |
|
||||||
| tester | roles/tester/role.md | TEST-* | pipeline |
|
| tester | [roles/tester/role.md](roles/tester/role.md) | TEST-* | pipeline | 压缩后必须重读 |
|
||||||
| reviewer | roles/reviewer/role.md | REVIEW-* + QUALITY-* | pipeline |
|
| reviewer | [roles/reviewer/role.md](roles/reviewer/role.md) | REVIEW-* + QUALITY-* | pipeline | 压缩后必须重读 |
|
||||||
| explorer | roles/explorer/role.md | EXPLORE-* | service (on-demand) |
|
| explorer | [roles/explorer/role.md](roles/explorer/role.md) | EXPLORE-* | service (on-demand) | 压缩后必须重读 |
|
||||||
| architect | roles/architect/role.md | ARCH-* | consulting (on-demand) |
|
| architect | [roles/architect/role.md](roles/architect/role.md) | ARCH-* | consulting (on-demand) | 压缩后必须重读 |
|
||||||
| fe-developer | roles/fe-developer/role.md | DEV-FE-* | frontend pipeline |
|
| fe-developer | [roles/fe-developer/role.md](roles/fe-developer/role.md) | DEV-FE-* | frontend pipeline | 压缩后必须重读 |
|
||||||
| fe-qa | roles/fe-qa/role.md | QA-FE-* | frontend pipeline |
|
| fe-qa | [roles/fe-qa/role.md](roles/fe-qa/role.md) | QA-FE-* | frontend pipeline | 压缩后必须重读 |
|
||||||
|
|
||||||
|
> **⚠️ COMPACT PROTECTION**: 角色文件是执行文档,不是参考资料。当 context compression 发生后,角色指令仅剩摘要时,**必须立即 `Read` 对应 role.md 重新加载后再继续执行**。不得基于摘要执行任何 Phase。
|
||||||
|
|
||||||
### Dispatch
|
### Dispatch
|
||||||
|
|
||||||
@@ -179,6 +181,86 @@ Fullstack: PLAN-001 → IMPL-001 ∥ DEV-FE-001 → TEST-001 ∥ QA-FE-001
|
|||||||
Full + FE: [Spec pipeline] → PLAN-001 → IMPL-001 ∥ DEV-FE-001 → TEST-001 ∥ QA-FE-001 → REVIEW-001
|
Full + FE: [Spec pipeline] → PLAN-001 → IMPL-001 ∥ DEV-FE-001 → TEST-001 ∥ QA-FE-001 → REVIEW-001
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Cadence Control
|
||||||
|
|
||||||
|
**节拍模型**: 事件驱动,每个 beat = coordinator 唤醒 → 处理 → spawn → STOP。
|
||||||
|
|
||||||
|
```
|
||||||
|
Beat Cycle (单次节拍)
|
||||||
|
═══════════════════════════════════════════════════════════
|
||||||
|
Event Coordinator Workers
|
||||||
|
───────────────────────────────────────────────────────────
|
||||||
|
callback/resume ──→ ┌─ handleCallback ─┐
|
||||||
|
│ mark completed │
|
||||||
|
│ check pipeline │
|
||||||
|
├─ handleSpawnNext ─┤
|
||||||
|
│ find ready tasks │
|
||||||
|
│ spawn workers ───┼──→ [Worker A] Phase 1-5
|
||||||
|
│ (parallel OK) ──┼──→ [Worker B] Phase 1-5
|
||||||
|
└─ STOP (idle) ─────┘ │
|
||||||
|
│
|
||||||
|
callback ←─────────────────────────────────────────┘
|
||||||
|
(next beat) SendMessage + TaskUpdate(completed)
|
||||||
|
═══════════════════════════════════════════════════════════
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pipeline 节拍视图**:
|
||||||
|
|
||||||
|
```
|
||||||
|
Spec-only (12 beats, 严格串行)
|
||||||
|
──────────────────────────────────────────────────────────
|
||||||
|
Beat 1 2 3 4 5 6 7 8 9 10 11 12
|
||||||
|
│ │ │ │ │ │ │ │ │ │ │ │
|
||||||
|
R1 → D1 → W1 → D2 → W2 → D3 → W3 → D4 → W4 → D5 → Q1 → D6
|
||||||
|
▲ ▲
|
||||||
|
pipeline sign-off
|
||||||
|
start pause
|
||||||
|
|
||||||
|
R=RESEARCH D=DISCUSS W=DRAFT(writer) Q=QUALITY
|
||||||
|
|
||||||
|
Impl-only (3 beats, 含并行窗口)
|
||||||
|
──────────────────────────────────────────────────────────
|
||||||
|
Beat 1 2 3
|
||||||
|
│ │ ┌────┴────┐
|
||||||
|
PLAN → IMPL ──→ TEST ∥ REVIEW ← 并行窗口
|
||||||
|
└────┬────┘
|
||||||
|
pipeline
|
||||||
|
done
|
||||||
|
|
||||||
|
Full-lifecycle (15 beats, spec→impl 过渡含检查点)
|
||||||
|
──────────────────────────────────────────────────────────
|
||||||
|
Beat 1-12: [Spec pipeline 同上]
|
||||||
|
│
|
||||||
|
Beat 12 (D6 完成): ⏸ CHECKPOINT ── 用户确认后 resume
|
||||||
|
│
|
||||||
|
Beat 13 14 15
|
||||||
|
PLAN → IMPL → TEST ∥ REVIEW
|
||||||
|
|
||||||
|
Fullstack (含双并行窗口)
|
||||||
|
──────────────────────────────────────────────────────────
|
||||||
|
Beat 1 2 3 4
|
||||||
|
│ ┌────┴────┐ ┌────┴────┐ │
|
||||||
|
PLAN → IMPL ∥ DEV-FE → TEST ∥ QA-FE → REVIEW
|
||||||
|
▲ ▲ ▲
|
||||||
|
并行窗口 1 并行窗口 2 同步屏障
|
||||||
|
```
|
||||||
|
|
||||||
|
**检查点 (Checkpoint)**:
|
||||||
|
|
||||||
|
| 触发条件 | 位置 | 行为 |
|
||||||
|
|----------|------|------|
|
||||||
|
| Spec→Impl 过渡 | DISCUSS-006 完成后 | ⏸ 暂停,等待用户 `resume` 确认 |
|
||||||
|
| GC 循环上限 | QA-FE max 2 rounds | 超出轮次 → 停止迭代,报告当前状态 |
|
||||||
|
| Pipeline 停滞 | 无 ready + 无 running | 检查缺失任务,报告用户 |
|
||||||
|
|
||||||
|
**Stall 检测** (coordinator `handleCheck` 时执行):
|
||||||
|
|
||||||
|
| 检查项 | 条件 | 处理 |
|
||||||
|
|--------|------|------|
|
||||||
|
| Worker 无响应 | in_progress 任务无回调 | 报告等待中的任务列表,建议用户 `resume` |
|
||||||
|
| Pipeline 死锁 | 无 ready + 无 running + 有 pending | 检查 blockedBy 依赖链,报告卡点 |
|
||||||
|
| GC 循环超限 | DEV-FE / QA-FE 迭代 > max_rounds | 终止循环,输出最新 QA 报告 |
|
||||||
|
|
||||||
### Task Metadata Registry
|
### Task Metadata Registry
|
||||||
|
|
||||||
| Task ID | Role | Phase | Dependencies | Description |
|
| Task ID | Role | Phase | Dependencies | Description |
|
||||||
@@ -282,12 +364,12 @@ Coordinator supports `--resume` / `--continue` for interrupted sessions:
|
|||||||
|
|
||||||
| Resource | Path | Usage |
|
| Resource | Path | Usage |
|
||||||
|----------|------|-------|
|
|----------|------|-------|
|
||||||
| Document Standards | specs/document-standards.md | YAML frontmatter, naming, structure |
|
| Document Standards | [specs/document-standards.md](specs/document-standards.md) | YAML frontmatter, naming, structure |
|
||||||
| Quality Gates | specs/quality-gates.md | Per-phase quality gates |
|
| Quality Gates | [specs/quality-gates.md](specs/quality-gates.md) | Per-phase quality gates |
|
||||||
| Product Brief Template | templates/product-brief.md | DRAFT-001 |
|
| Product Brief Template | [templates/product-brief.md](templates/product-brief.md) | DRAFT-001 |
|
||||||
| Requirements Template | templates/requirements-prd.md | DRAFT-002 |
|
| Requirements Template | [templates/requirements-prd.md](templates/requirements-prd.md) | DRAFT-002 |
|
||||||
| Architecture Template | templates/architecture-doc.md | DRAFT-003 |
|
| Architecture Template | [templates/architecture-doc.md](templates/architecture-doc.md) | DRAFT-003 |
|
||||||
| Epics Template | templates/epics-template.md | DRAFT-004 |
|
| Epics Template | [templates/epics-template.md](templates/epics-template.md) | DRAFT-004 |
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
|
|||||||
@@ -189,6 +189,6 @@ Plan mode and execute mode are triggered by skill name routing (see Mode Detecti
|
|||||||
| Phase | Document | Purpose | Compact |
|
| Phase | Document | Purpose | Compact |
|
||||||
|-------|----------|---------|---------|
|
|-------|----------|---------|---------|
|
||||||
| 1 | [phases/01-lite-plan.md](phases/01-lite-plan.md) | Complete planning pipeline: exploration, clarification, planning, confirmation, handoff | Phase 1 完成后可压缩 |
|
| 1 | [phases/01-lite-plan.md](phases/01-lite-plan.md) | Complete planning pipeline: exploration, clarification, planning, confirmation, handoff | Phase 1 完成后可压缩 |
|
||||||
| 2 | [phases/02-lite-execute.md](phases/02-lite-execute.md) | Complete execution engine: input modes, task grouping, batch execution, code review | **⚠️ 执行期间禁止压缩,压缩后必须重读** |
|
| 2 | [phases/02-lite-execute.md](phases/02-lite-execute.md) | Complete execution engine: input modes, task grouping, batch execution, code review | **⚠️ 压缩时必须完整保留,若仅剩摘要须重读** |
|
||||||
|
|
||||||
**Phase 2 Compact Rule**: Phase 2 是执行引擎,包含 Step 1-6 的完整执行协议。如果 compact 发生且 Phase 2 内容仅剩摘要,**必须立即 `Read("phases/02-lite-execute.md")` 重新加载后再继续执行**。不得基于摘要执行任何 Step。
|
**Phase 2 Compact Rule**: Phase 2 是执行引擎,包含 Step 1-6 的完整执行协议。compact 压缩时必须完整保留 Phase 2 内容。若 compact 后 Phase 2 仅剩摘要,**必须立即 `Read("phases/02-lite-execute.md")` 重新加载后再继续执行**。不得基于摘要执行任何 Step。
|
||||||
|
|||||||
@@ -174,9 +174,9 @@ Multi-phase workflows span long conversations. Context compression (compact) may
|
|||||||
| Phase | Document | Purpose | Compact |
|
| Phase | Document | Purpose | Compact |
|
||||||
|-------|----------|---------|---------|
|
|-------|----------|---------|---------|
|
||||||
| 1 | phases/01-xxx.md | Planning pipeline | Phase 1 完成后可压缩 |
|
| 1 | phases/01-xxx.md | Planning pipeline | Phase 1 完成后可压缩 |
|
||||||
| 2 | phases/02-xxx.md | Execution engine | **⚠️ 执行期间禁止压缩,压缩后必须重读** |
|
| 2 | phases/02-xxx.md | Execution engine | **⚠️ 压缩时必须完整保留,若仅剩摘要须重读** |
|
||||||
|
|
||||||
**Phase N Compact Rule**: Phase N 是执行引擎,包含 Step 1-M 的完整执行协议。如果 compact 发生且 Phase N 内容仅剩摘要,**必须立即 `Read("phases/0N-xxx.md")` 重新加载后再继续执行**。不得基于摘要执行任何 Step。
|
**Phase N Compact Rule**: Phase N 是执行引擎,包含 Step 1-M 的完整执行协议。compact 压缩时必须完整保留 Phase N 内容。若 compact 后仅剩摘要,**必须立即 `Read("phases/0N-xxx.md")` 重新加载后再继续执行**。不得基于摘要执行任何 Step。
|
||||||
```
|
```
|
||||||
|
|
||||||
**Phase 文件顶部** — Compact Protection directive:
|
**Phase 文件顶部** — Compact Protection directive:
|
||||||
|
|||||||
@@ -7141,11 +7141,19 @@ import type { AnalysisSessionSummary, AnalysisSessionDetail } from '../types/ana
|
|||||||
* Fetch list of analysis sessions
|
* Fetch list of analysis sessions
|
||||||
*/
|
*/
|
||||||
export async function fetchAnalysisSessions(
|
export async function fetchAnalysisSessions(
|
||||||
projectPath?: string
|
projectPath?: string,
|
||||||
|
options?: { limit?: number; offset?: number }
|
||||||
): Promise<AnalysisSessionSummary[]> {
|
): Promise<AnalysisSessionSummary[]> {
|
||||||
const data = await fetchApi<{ success: boolean; data: AnalysisSessionSummary[]; error?: string }>(
|
const params = new URLSearchParams();
|
||||||
withPath('/api/analysis', projectPath)
|
if (options?.limit) params.set('limit', String(options.limit));
|
||||||
);
|
if (options?.offset) params.set('offset', String(options.offset));
|
||||||
|
|
||||||
|
const queryString = params.toString();
|
||||||
|
const path = queryString
|
||||||
|
? `${withPath('/api/analysis', projectPath)}&${queryString}`
|
||||||
|
: withPath('/api/analysis', projectPath);
|
||||||
|
|
||||||
|
const data = await fetchApi<{ success: boolean; data: AnalysisSessionSummary[]; error?: string }>(path);
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
throw new Error(data.error || 'Failed to fetch analysis sessions');
|
throw new Error(data.error || 'Failed to fetch analysis sessions');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,10 +24,11 @@ import { Card } from '@/components/ui/Card';
|
|||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { Badge } from '@/components/ui/Badge';
|
import { Badge } from '@/components/ui/Badge';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs';
|
import { TabsNavigation } from '@/components/ui/TabsNavigation';
|
||||||
import { fetchAnalysisSessions, fetchAnalysisDetail } from '@/lib/api';
|
import { fetchAnalysisSessions, fetchAnalysisDetail } from '@/lib/api';
|
||||||
import { MessageRenderer } from '@/components/shared/CliStreamMonitor/MessageRenderer';
|
import { MessageRenderer } from '@/components/shared/CliStreamMonitor/MessageRenderer';
|
||||||
import { JsonCardView } from '@/components/shared/JsonCardView';
|
import { JsonCardView } from '@/components/shared/JsonCardView';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
import type { AnalysisSessionSummary } from '@/types/analysis';
|
import type { AnalysisSessionSummary } from '@/types/analysis';
|
||||||
|
|
||||||
// ========== Session Card Component ==========
|
// ========== Session Card Component ==========
|
||||||
@@ -41,23 +42,22 @@ interface SessionCardProps {
|
|||||||
function SessionCard({ session, onClick, isSelected }: SessionCardProps) {
|
function SessionCard({ session, onClick, isSelected }: SessionCardProps) {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className={`p-4 cursor-pointer transition-colors ${
|
className={cn(
|
||||||
isSelected ? 'ring-2 ring-primary bg-accent/50' : 'hover:bg-accent/50'
|
'p-4 cursor-pointer transition-all hover:shadow-md',
|
||||||
}`}
|
isSelected && 'ring-2 ring-primary'
|
||||||
|
)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between mb-3">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<FileSearch className="w-5 h-5 text-primary flex-shrink-0" />
|
||||||
<FileSearch className="w-4 h-4 text-muted-foreground" />
|
<h3 className="font-medium text-foreground truncate">{session.topic}</h3>
|
||||||
<span className="font-medium truncate">{session.topic}</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted-foreground truncate">{session.id}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<ChevronRight className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
<ChevronRight className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 mt-3">
|
<p className="text-sm text-muted-foreground truncate mb-3">{session.id}</p>
|
||||||
<Badge variant={session.status === 'completed' ? 'default' : 'secondary'}>
|
<div className="flex items-center gap-3">
|
||||||
|
<Badge variant={session.status === 'completed' ? 'success' : 'warning'}>
|
||||||
{session.status === 'completed' ? (
|
{session.status === 'completed' ? (
|
||||||
<><CheckCircle className="w-3 h-3 mr-1" />完成</>
|
<><CheckCircle className="w-3 h-3 mr-1" />完成</>
|
||||||
) : (
|
) : (
|
||||||
@@ -82,6 +82,8 @@ interface DetailPanelProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function DetailPanel({ sessionId, projectPath, onClose }: DetailPanelProps) {
|
function DetailPanel({ sessionId, projectPath, onClose }: DetailPanelProps) {
|
||||||
|
const [activeTab, setActiveTab] = useState('discussion');
|
||||||
|
|
||||||
const { data: detail, isLoading, error } = useQuery({
|
const { data: detail, isLoading, error } = useQuery({
|
||||||
queryKey: ['analysis-detail', sessionId, projectPath],
|
queryKey: ['analysis-detail', sessionId, projectPath],
|
||||||
queryFn: () => fetchAnalysisDetail(sessionId, projectPath),
|
queryFn: () => fetchAnalysisDetail(sessionId, projectPath),
|
||||||
@@ -110,13 +112,20 @@ function DetailPanel({ sessionId, projectPath, onClose }: DetailPanelProps) {
|
|||||||
|
|
||||||
// Build available tabs based on content
|
// Build available tabs based on content
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: 'discussion', label: '讨论记录', icon: MessageSquare, content: detail.discussion },
|
{ value: 'discussion', label: '讨论记录', icon: <MessageSquare className="w-4 h-4" /> },
|
||||||
{ id: 'conclusions', label: '结论', icon: CheckCircle, content: detail.conclusions },
|
{ value: 'conclusions', label: '结论', icon: <CheckCircle className="w-4 h-4" /> },
|
||||||
{ id: 'explorations', label: '代码探索', icon: Code, content: detail.explorations },
|
{ value: 'explorations', label: '代码探索', icon: <Code className="w-4 h-4" /> },
|
||||||
{ id: 'perspectives', label: '视角分析', icon: FileText, content: detail.perspectives },
|
{ value: 'perspectives', label: '视角分析', icon: <FileText className="w-4 h-4" /> },
|
||||||
].filter(tab => tab.content);
|
].filter(tab => {
|
||||||
|
const key = tab.value as keyof typeof detail;
|
||||||
|
return detail[key];
|
||||||
|
});
|
||||||
|
|
||||||
const defaultTab = tabs[0]?.id || 'discussion';
|
// Ensure activeTab is valid
|
||||||
|
const validTab = tabs.find(t => t.value === activeTab);
|
||||||
|
if (!validTab && tabs.length > 0) {
|
||||||
|
setActiveTab(tabs[0].value);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
@@ -138,46 +147,35 @@ function DetailPanel({ sessionId, projectPath, onClose }: DetailPanelProps) {
|
|||||||
|
|
||||||
{/* Tabs Content */}
|
{/* Tabs Content */}
|
||||||
{tabs.length > 0 ? (
|
{tabs.length > 0 ? (
|
||||||
<Tabs defaultValue={defaultTab} className="flex-1 flex flex-col min-h-0">
|
<>
|
||||||
<TabsList className="mx-4 mt-4 shrink-0 w-fit">
|
<TabsNavigation
|
||||||
{tabs.map(tab => (
|
value={activeTab}
|
||||||
<TabsTrigger key={tab.id} value={tab.id} className="gap-1.5">
|
onValueChange={setActiveTab}
|
||||||
<tab.icon className="w-3.5 h-3.5" />
|
tabs={tabs}
|
||||||
{tab.label}
|
className="px-4"
|
||||||
</TabsTrigger>
|
/>
|
||||||
))}
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<div className="flex-1 overflow-auto min-h-0 p-4">
|
<div className="flex-1 overflow-auto min-h-0 p-4">
|
||||||
{/* Discussion Tab */}
|
{/* Discussion Tab */}
|
||||||
<TabsContent value="discussion" className="mt-0 h-full">
|
{activeTab === 'discussion' && detail.discussion && (
|
||||||
{detail.discussion && (
|
|
||||||
<MessageRenderer content={detail.discussion} format="markdown" />
|
<MessageRenderer content={detail.discussion} format="markdown" />
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* Conclusions Tab */}
|
{/* Conclusions Tab */}
|
||||||
<TabsContent value="conclusions" className="mt-0 h-full">
|
{activeTab === 'conclusions' && detail.conclusions && (
|
||||||
{detail.conclusions && (
|
|
||||||
<JsonCardView data={detail.conclusions} />
|
<JsonCardView data={detail.conclusions} />
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* Explorations Tab */}
|
{/* Explorations Tab */}
|
||||||
<TabsContent value="explorations" className="mt-0 h-full">
|
{activeTab === 'explorations' && detail.explorations && (
|
||||||
{detail.explorations && (
|
|
||||||
<JsonCardView data={detail.explorations} />
|
<JsonCardView data={detail.explorations} />
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* Perspectives Tab */}
|
{/* Perspectives Tab */}
|
||||||
<TabsContent value="perspectives" className="mt-0 h-full">
|
{activeTab === 'perspectives' && detail.perspectives && (
|
||||||
{detail.perspectives && (
|
|
||||||
<JsonCardView data={detail.perspectives} />
|
<JsonCardView data={detail.perspectives} />
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
|
||||||
</div>
|
</div>
|
||||||
</Tabs>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-1 flex items-center justify-center text-muted-foreground">
|
<div className="flex-1 flex items-center justify-center text-muted-foreground">
|
||||||
暂无分析内容
|
暂无分析内容
|
||||||
@@ -210,15 +208,17 @@ export function AnalysisPage() {
|
|||||||
{/* Left Panel - List */}
|
{/* Left Panel - List */}
|
||||||
<div className={`p-6 space-y-6 overflow-auto ${selectedSession ? 'w-[400px] shrink-0' : 'flex-1'}`}>
|
<div className={`p-6 space-y-6 overflow-auto ${selectedSession ? 'w-[400px] shrink-0' : 'flex-1'}`}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FileSearch className="w-6 h-6 text-primary" />
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold flex items-center gap-2">
|
<h1 className="text-2xl font-semibold text-foreground">
|
||||||
<FileSearch className="w-6 h-6" />
|
|
||||||
Analysis Viewer
|
Analysis Viewer
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground mt-1">
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
查看 /workflow:analyze-with-file 命令的分析结果
|
查看 /workflow:analyze-with-file 命令的分析结果
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<div className="relative max-w-md">
|
<div className="relative max-w-md">
|
||||||
|
|||||||
@@ -8,11 +8,16 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { readdir, readFile, stat } from 'fs/promises';
|
import { readdir, readFile, stat } from 'fs/promises';
|
||||||
import { existsSync } from 'fs';
|
import { existsSync, statSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import type { RouteContext } from './types.js';
|
import type { RouteContext } from './types.js';
|
||||||
import { resolvePath } from '../../utils/path-resolver.js';
|
import { resolvePath } from '../../utils/path-resolver.js';
|
||||||
|
|
||||||
|
// Concurrency limit for processing folders
|
||||||
|
const MAX_CONCURRENT = 10;
|
||||||
|
// Timeout for individual file operations (ms)
|
||||||
|
const FILE_TIMEOUT = 5000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Analysis session summary for list view
|
* Analysis session summary for list view
|
||||||
*/
|
*/
|
||||||
@@ -153,6 +158,8 @@ export async function handleAnalysisRoutes(ctx: RouteContext): Promise<boolean>
|
|||||||
if (pathname === '/api/analysis' && req.method === 'GET') {
|
if (pathname === '/api/analysis' && req.method === 'GET') {
|
||||||
try {
|
try {
|
||||||
const projectPath = ctx.url.searchParams.get('projectPath') || initialPath;
|
const projectPath = ctx.url.searchParams.get('projectPath') || initialPath;
|
||||||
|
const limit = Math.min(parseInt(ctx.url.searchParams.get('limit') || '50', 10), 100);
|
||||||
|
const offset = parseInt(ctx.url.searchParams.get('offset') || '0', 10);
|
||||||
const resolvedPath = resolvePath(projectPath);
|
const resolvedPath = resolvePath(projectPath);
|
||||||
const analysisDir = join(resolvedPath, '.workflow', '.analysis');
|
const analysisDir = join(resolvedPath, '.workflow', '.analysis');
|
||||||
|
|
||||||
@@ -163,18 +170,22 @@ export async function handleAnalysisRoutes(ctx: RouteContext): Promise<boolean>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const folders = await readdir(analysisDir);
|
const folders = await readdir(analysisDir);
|
||||||
const sessions: AnalysisSessionSummary[] = [];
|
|
||||||
|
|
||||||
for (const folder of folders) {
|
// Parallel processing for better performance
|
||||||
const summary = await getSessionSummary(analysisDir, folder);
|
const summaries = await Promise.all(
|
||||||
if (summary) sessions.push(summary);
|
folders.map(folder => getSessionSummary(analysisDir, folder))
|
||||||
}
|
);
|
||||||
|
|
||||||
// Sort by date descending
|
// Filter valid sessions and sort by date descending
|
||||||
sessions.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
const allSessions = summaries
|
||||||
|
.filter((s): s is AnalysisSessionSummary => s !== null)
|
||||||
|
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||||
|
|
||||||
|
const total = allSessions.length;
|
||||||
|
const paginatedSessions = allSessions.slice(offset, offset + limit);
|
||||||
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify({ success: true, data: sessions, total: sessions.length }));
|
res.end(JSON.stringify({ success: true, data: paginatedSessions, total }));
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
|||||||
Reference in New Issue
Block a user