feat: 复盘笔记接入梦核飞轮 + 看板右侧栏 UI 重构

- FlywheelStats 聚合跨项目 project_notes(最近 50 条),梦核 prompt 新增【高频陷阱】分析节
- 新增 flywheel-intelligence 蓝图模块,阶段一标记 done
- ProjectCard 去重:role/recent_focus/部署链接移至 Dashboard 右侧栏统一展示
- 右侧栏改为卡片式布局,部署环境按钮全宽、带箭头图标
- settings.json 权限模式升级为 bypassPermissions;CLAUDE.md 重构蓝图工作流说明
This commit is contained in:
lanrtop 2026-04-11 12:58:47 +09:00
parent b6e4fbe3fa
commit ee4b41f938
9 changed files with 471 additions and 93 deletions

View File

@ -6,7 +6,7 @@ description: >
成为每个项目的贴身导师。炼境本身亦被纳入其中,
形成可自我进化的开发智能闭环。
iteration: 1
updated: 2026-04-10
updated: 2026-04-11
@ -265,6 +265,13 @@ modules:
progress: 100
position: [2500, 0]
- id: flywheel-intelligence
name: 飞轮智能成长
area: backend
status: in_progress
progress: 33
position: [1750, 650]
edges:
# 前端依赖
- from: workspace-mgmt
@ -401,3 +408,11 @@ edges:
- from: mcp-server
to: project-mentor
type: dependency
# 飞轮智能成长
- from: blueprint-feedback
to: flywheel-intelligence
type: dependency
- from: blueprint-flywheel
to: flywheel-intelligence
type: dependency

View File

@ -0,0 +1,77 @@
# 飞轮智能成长
炼境通过管理项目产生的多维数据反哺自身,形成可自我进化的成长闭环。
核心愿景:让炼境像一把可成长神器,执行数据越积越多,自身越来越好用。
## 背景
现有飞轮仅覆盖一个维度usage.json 指标 → 梦核分析 → CONVENTIONS 建议。
复盘笔记、任务时长、返工率等执行质量数据已在采集,但没有进入任何反哺路径——数据在"睡觉"。
本模块分三个阶段逐步打通数据→改善的完整闭环。
## 阶段触发条件
- **阶段二启动条件**:阶段一上线后,梦核输出中稳定出现"高频陷阱摘要"板块,且跨项目复盘笔记数量 ≥ 20 条
- **阶段三启动条件**:阶段二运行 ≥ 3 个月accumulated 数据覆盖 ≥ 15 个项目usage.json 携带 rework_count 和 avg_rounds 字段
---
## 任务卡
### ✅ 阶段一:复盘笔记接入梦核飞轮
- status: done
- complexity: M
- files: src-tauri/src/commands/blueprint_feedback.rs, src/components/blueprint/FlywheelPanel.tsx
- acceptance: 梦核 prompt 构建时聚合所有项目的 project_notesOpus 输出新增"高频陷阱摘要"板块,列出跨项目重复出现的踩坑模式;本地验证:有复盘笔记的项目数据能正确注入 prompt
阶段一详细设计:
- 梦核 prompt 模板增加输入段:通过 MCP 读取各项目 project_notes按"任务类型 × 是否返工"分组
- Opus 输出格式新增板块:
```
### 高频陷阱(跨项目复盘笔记聚合)
- <任务类型><踩坑描述>,出现 N 次项目A、B、C
```
- 修改点:梦核 prompt 构建逻辑Rust 侧)
---
### 阶段二usage.json 扩展采集维度
- status: todo
- complexity: M
- depends: flywheel-intelligence阶段一完成后启动
- files: src-tauri/src/commands/blueprint_flywheel.rs
- acceptance: usage.json 快照新增 complexity_dist、rework_count、avg_rounds_M 字段;字段值从复盘笔记解析填充;梦核分析可输出"M 级任务实际轮数 vs 预估"对比
阶段二详细设计:
```json
"tasks": {
...现有字段...,
"complexity_dist": { "S": 12, "M": 8, "L": 0 },
"rework_count": 3,
"avg_rounds_M": 2.4
}
```
- rework_count 和 avg_rounds 从项目 project_notes 中解析"实际轮数"字段聚合
---
### 阶段三:执行质量预测层
- status: todo
- complexity: L
- depends: flywheel-intelligence阶段二完成且数据积累达标后启动
- files: TBD需 /architect 后确定)
- acceptance: TBD
阶段三方向(待 /architect 设计):
- 任务完成时长分布(为 S/M/L 定义时间基准)
- 高风险任务预警(特定文件组合 → 历史高返工率)
- 跨项目模式库推荐
> ⚠️ 阶段三为 L 级复杂度,到时须先走 /architect → /splitter 流程,禁止直接写代码。
## 决策记录
- 三阶段串行而非并行:数据积累需要时间,阶段二依赖阶段一跑出足够复盘笔记,阶段三依赖阶段二的字段扩展;提前建设无数据支撑的预测层是无效投入
- 蓝图管理而非单独文档三阶段是对炼境自身的迭代开发bluepint 的 depends 字段天然表达阶段间的触发关系,可视化面板也能直接展示当前在哪个阶段
- 不存 CONVENTIONS 全文历史到 DBGit 已有版本记录,效果对比通过 usage.json 的 conventions_version 分组已足够

View File

@ -734,6 +734,175 @@
"todo": 0,
"total": 140
}
},
{
"at": "2026-04-11T02:33:58Z",
"blocked_reasons": [],
"conventions_version": "1.4.3",
"delta": {
"newly_blocked": [],
"newly_done": [],
"unblocked": []
},
"iteration": 1,
"modules": {
"blocked": 0,
"concept": 0,
"done": 34,
"in_progress": 0,
"planned": 0,
"stalled": [],
"total": 34
},
"task_ids": {
"blocked": [],
"done": [
"auto-update/Capabilities 权限配置",
"auto-update/下载 & 安装",
"auto-update/更新检测",
"auto-update/自动检查更新",
"auto-update/错误提示",
"blueprint-buff/BlueprintView 跨项目切换",
"blueprint-buff/Buff 生命周期 Rust 命令",
"blueprint-buff/blueprint_buffs 数据表",
"blueprint-buff/git watcher + 任务匹配写回 + 飞轮快照",
"blueprint-buff/前端 Buff 管理 UI",
"blueprint-buff/前端实时刷新",
"blueprint-feedback/归档 command",
"blueprint-feedback/归档候选展示",
"blueprint-feedback/梦核分析入口(蓝图弹窗)",
"blueprint-feedback/梦核提示词增加停滞模块数据",
"blueprint-flywheel/get_flywheel_stats command",
"blueprint-flywheel/get_flywheel_stats 支持迭代感知",
"blueprint-flywheel/usage.json 埋点写入",
"blueprint-flywheel/梦核提示词生成 command",
"blueprint-flywheel/飞轮健康面板",
"blueprint-governance/CLAUDE.md 模板定义",
"blueprint-governance/CONVENTIONS.md 版本化",
"blueprint-governance/GovernancePanel 规则编辑器 UI",
"blueprint-governance/get_blueprint_status command",
"blueprint-governance/sync/梦核/summary 切换为 effective 内容",
"blueprint-governance/sync_blueprint_rules command",
"blueprint-governance/批量同步所有项目规则",
"blueprint-governance/用户版 CONVENTIONS 存储层初始化",
"blueprint-governance/用户版 CONVENTIONS 读写 command",
"blueprint-onboarding/generate_blueprint_prompt command",
"blueprint-onboarding/蓝图操作弹窗",
"blueprint-onboarding/项目列表蓝图状态徽章",
"blueprint-reader/合并返回完整蓝图数据",
"blueprint-reader/读取 manifest.yaml",
"blueprint-reader/读取 modules/*.md",
"blueprint-view/.blueprint/ 数据结构设计",
"blueprint-view/React Flow 节点图渲染",
"blueprint-view/blocked 状态支持",
"blueprint-view/下一步汇总面板",
"blueprint-view/任务卡复制派发",
"blueprint-view/批量执行提示词入口",
"blueprint-view/智能连线路由",
"blueprint-view/模块详情面板",
"blueprint-view/统计汇总条",
"blueprint-view/蓝图治理面板",
"blueprint-view/蓝图读取 Command",
"blueprint-view/边高亮与模块聚焦",
"blueprint-view/顶部状态行扩展",
"blueprint-view/项目卡片蓝图入口",
"cicd-workflow/Actions 运行历史",
"cicd-workflow/CI/CD 配置 CRUD",
"cicd-workflow/Tauri OSS 分发模板",
"cicd-workflow/Web SSH 部署模板",
"cicd-workflow/Workflow 文件生成",
"devtools-scan/WSL 工具扫描",
"devtools-scan/Windows 工具扫描",
"devtools-scan/工具信息持久化",
"git-ops/上游状态检测",
"git-ops/分支管理",
"git-ops/基础操作",
"github-auth/OAuth Device Flow",
"github-auth/Token 管理",
"github-publish/.gitignore 生成",
"github-publish/Git 初始化 & 首次推送",
"github-publish/创建 GitHub 仓库",
"github-publish/扫描本地 Git 状态",
"health-center/T1 · Backend 健康检查聚合命令",
"health-center/T2 · 质量指标聚合",
"health-center/T3 · 前端健康中心页面",
"health-center/T4 · 扩展 MCP get_diagnostics",
"mcp-config-inject/加入产品组时注入配置",
"mcp-config-inject/端口变更时重新注入所有配置",
"mcp-config-inject/退出产品组时清除配置",
"mcp-server/MCP 端口可配置Rust 侧)",
"mcp-server/启动时绑定 HTTP 端口",
"mcp-server/实现 MCP JSON-RPC 基础协议",
"mcp-server/实现产品组上下文工具",
"product-group-mcp/产品组详情显示 MCP 注入状态",
"product-group-mcp/设置页添加 MCP 端口配置",
"project-dashboard/分组画布",
"project-dashboard/项目卡片列表",
"project-dashboard/项目发现面板",
"project-dashboard/项目快捷操作",
"project-env-scan/环境信息持久化",
"project-env-scan/项目环境检测",
"project-mentor/T1 · 后端聚合 commandMentorContext",
"project-mentor/T10 · 启动健康扫描 + 前端 Alert Banner",
"project-mentor/T11 · 项目历史快照",
"project-mentor/T2 · MCP 项目导师查询接口",
"project-mentor/T3 · 前端导师面板",
"project-mentor/T4 · 炼境自指接入",
"project-mentor/T5 · project_notes 数据层 + 后端命令",
"project-mentor/T6 · MCP append_project_note 工具",
"project-mentor/T7 · MentorPanel 展示 AI 笔记",
"project-mentor/T8 · 后端全组巡检命令",
"project-mentor/T9 · 前端全组巡检按钮",
"project-mgmt/子文件夹扫描",
"project-mgmt/批量导入项目",
"project-mgmt/项目 CRUD",
"project-mgmt/项目档案Profile",
"project-templates/从模板创建项目",
"project-templates/内置模板种植",
"project-templates/模板 CRUD",
"project-tools/工具 CRUD",
"project-tools/工具启动",
"repo-registry/仓库导入",
"repo-registry/仓库挂载",
"repo-registry/仓库注册表管理",
"runtime-diagnostics/T1 · SQLite runtime_logs 表",
"runtime-diagnostics/T2 · 替换 eprintln! 为结构化日志",
"runtime-diagnostics/T3 · MCP Server 新增 get_diagnostics 工具",
"runtime-diagnostics/T4 · 注入失败结果返回前端",
"runtime-diagnostics/T5 · 设置页 MCP 真实健康检查",
"settings-page/WSL 发行版选择",
"settings-page/关于 & 更新",
"settings-page/终端配置",
"settings-page/编辑器检测",
"shell-launch/打开终端",
"shell-launch/打开编辑器",
"sqlite-db/数据库迁移",
"sqlite-db/连接池初始化",
"tags-groups/分组管理",
"tags-groups/标签管理",
"tray-service/关闭窗口最小化到托盘",
"tray-service/系统托盘图标与菜单",
"tray-service/集成 tauri-plugin-single-instance",
"v2rayn-import/当前节点读取",
"v2rayn-import/数据库路径检测",
"v2rayn-import/节点列表扫描",
"workspace-mgmt/空间 CRUD",
"workspace-mgmt/空间级设置",
"workspace-mgmt/部署命令管理",
"wsl-proxy/代理一致性测试",
"wsl-proxy/代理同步",
"wsl-proxy/代理清除",
"wsl-proxy/代理状态检测"
]
},
"tasks": {
"blocked": 0,
"dispatched": 0,
"done": 136,
"in_progress": 0,
"todo": 0,
"total": 140
}
}
]
}

View File

@ -13,6 +13,6 @@
]
},
"permissions": {
"defaultMode": "acceptEdits"
"defaultMode": "bypassPermissions"
}
}

View File

@ -22,6 +22,12 @@ src-tauri/src/commands/ Tauri command 实现
docs/ 部署和更新指南
```
## 命令执行规则
- 所有命令直接用 Bash tool 执行,禁止输出命令让用户手动运行
- `pnpm tauri build` / `cargo build --release` 等 Rust 构建用 `run_in_background: true` + `timeout: 600000`(首次编译可能需要 10 分钟)
- 超过 10 分钟的任务(如完整 release build改为输出单行完整命令让用户 `! <命令>` 一键执行
## 技能使用规则
根据任务卡 complexity 决定执行后的检查:
@ -36,27 +42,39 @@ docs/ 部署和更新指南
- 代码重构/简化:/simplify → /code-review
- 发现深层问题:/Dev-deep-think
<!-- BLUEPRINT_MANAGED_START version="1.4.2" -->
<!-- BLUEPRINT_MANAGED_START version="1.4.3" -->
## 项目蓝图
本项目使用 `.blueprint/` 目录维护项目全景架构图,驱动需求讨论和任务管理。
**每次对话都应遵循以下流程:**
### 对话开始时必须执行
### 1. 了解当前状态
对话开始时,先读取 `.blueprint/manifest.yaml` 了解项目模块和状态。
1. 读取 `.blueprint/manifest.yaml` 了解项目全景和当前状态
2. 读取 `.blueprint/CONVENTIONS.md` 了解完整工作流规则
### 2. 讨论需求时更新蓝图
- 确认了新需求 → 在 manifest.yaml 新增模块,创建 `.blueprint/modules/<id>.md`
- 拆分了大需求 → 在模块文件中添加任务卡
- 调整了方向 → 更新模块关系edges
### 需求讨论 → 蓝图同步规则
### 3. 完成实现后标记蓝图
- 完成了功能 → 更新对应任务卡状态为 `done`(前缀改为 ✅)
- 更新 manifest.yaml 顶部的 `updated` 日期
| 用户说的 | Claude 应做的 |
|----------|--------------|
| "我想加个 XX 功能" | 确认需求后manifest 新增模块status: concept创建 `.blueprint/modules/<id>.md` |
| "XX 功能需要 A、B、C" | 在模块文件中拆分为任务卡 |
| "这个方案确定了" | 模块 status → planned补充任务卡 files/acceptance |
| "开始做 XX" | 模块 status → in_progress |
| 没提蓝图但在讨论功能 | 主动提议更新蓝图 |
### 4. 任务卡规范
详见 `.blueprint/CONVENTIONS.md`,核心规则:
- 任务卡同时具备 `files` + `acceptance` + complexity `S/M` → 标记为 📋 可派发
- 可派发的任务卡可以直接交给 Claude Agent 独立完成
### 复杂度决策(讨论新功能 / 拆解需求时)
| complexity | 执行流程 |
|-----------|---------|
| S / M | 直接拆任务卡,确保每张卡有 `files` + `acceptance` + `complexity` → 标记 📋 |
| L复杂系统设计 | 先 `/architect` 输出设计文档,用户确认后 `/splitter` 拆成 S/M 任务卡 |
> L 级任务禁止直接写代码,必须先经过 `/architect``/splitter` 流程。
### 任务执行两阶段提交
- **开始写代码前**:任务卡前缀改为 🔵status → in_progress
- **完成验收后**:任务卡前缀改为 ✅status → done更新 manifest `updated` 日期
完整规则(迭代管理、阻塞处理、复盘笔记等)详见 `.blueprint/CONVENTIONS.md`
<!-- BLUEPRINT_MANAGED_END -->

View File

@ -1364,6 +1364,14 @@ pub struct IterationJump {
pub rate_after: f64,
}
/// 单条跨项目复盘笔记Claude 通过 MCP 写入)
#[derive(Debug, Serialize, Deserialize)]
pub struct ProjectNoteEntry {
pub project_name: String,
pub content: String,
pub created_at: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FlywheelStats {
pub cohorts: Vec<FlywheelCohort>,
@ -1378,6 +1386,8 @@ pub struct FlywheelStats {
pub stalled_modules: Vec<String>,
pub total_projects_scanned: u32,
pub projects_with_data: u32,
/// 跨项目复盘笔记(最近 50 条,按时间倒序)
pub project_notes: Vec<ProjectNoteEntry>,
}
/// 内部:单项目最新快照的指标摘要
@ -1577,6 +1587,8 @@ pub fn get_flywheel_stats(project_paths: Vec<String>) -> Result<FlywheelStats, S
.map(|(reason, count)| BlockedReasonCount { reason, count })
.collect();
let project_notes = collect_flywheel_notes(&project_paths);
Ok(FlywheelStats {
cohorts,
top_blocked_reasons,
@ -1586,9 +1598,62 @@ pub fn get_flywheel_stats(project_paths: Vec<String>) -> Result<FlywheelStats, S
stalled_modules,
total_projects_scanned: total_scanned,
projects_with_data,
project_notes,
})
}
/// 从 SQLite 收集各项目的 Claude 复盘笔记(按 win_path 匹配项目,取最近 50 条)。
/// DB 访问失败时静默返回空列表,不影响主流程。
fn collect_flywheel_notes(project_paths: &[String]) -> Vec<ProjectNoteEntry> {
let conn = match crate::db::pool().get() {
Ok(c) => c,
Err(_) => return vec![],
};
let mut notes: Vec<ProjectNoteEntry> = Vec::new();
for path_str in project_paths {
// 通过 win_path 或 wsl_path 查找 project_id 和 name
let proj: Result<(String, String), _> = conn.query_row(
"SELECT id, name FROM projects WHERE win_path = ?1 OR wsl_path = ?1 LIMIT 1",
rusqlite::params![path_str],
|row| Ok((row.get(0)?, row.get(1)?)),
);
let (project_id, project_name) = match proj {
Ok(v) => v,
Err(_) => continue,
};
let mut stmt = match conn.prepare(
"SELECT content, created_at FROM project_notes
WHERE project_id = ?1
ORDER BY id DESC LIMIT 20",
) {
Ok(s) => s,
Err(_) => continue,
};
let rows = stmt.query_map(
rusqlite::params![project_id],
|row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)),
);
if let Ok(iter) = rows {
for row in iter.filter_map(|r| r.ok()) {
notes.push(ProjectNoteEntry {
project_name: project_name.clone(),
content: row.0,
created_at: row.1,
});
}
}
}
// 按时间倒序,限 50 条(避免 prompt 过长)
notes.sort_by(|a, b| b.created_at.cmp(&a.created_at));
notes.truncate(50);
notes
}
// ── 梦核提示词生成 ────────────────────────────────────────────────────────────
/// 从完整 CONVENTIONS 中抽取对梦核有判断价值的规则段落。
@ -1711,6 +1776,31 @@ pub fn generate_muhe_prompt(stats: FlywheelStats) -> String {
.join("\n")
};
// ── 复盘笔记 ──────────────────────────────────────────────────────────────
let notes_summary = if stats.project_notes.is_empty() {
"暂无复盘笔记——Claude 尚未在执行 M/L 任务后写入笔记)".to_string()
} else {
// 按项目分组展示
let mut by_project: std::collections::BTreeMap<String, Vec<&ProjectNoteEntry>> = Default::default();
for note in &stats.project_notes {
by_project.entry(note.project_name.clone()).or_default().push(note);
}
let mut sections: Vec<String> = Vec::new();
for (proj, entries) in &by_project {
sections.push(format!("#### {}", proj));
for e in entries {
// 多行内容缩进,保持 prompt 可读性
let indented = e.content.lines()
.map(|l| format!(" {}", l))
.collect::<Vec<_>>()
.join("\n");
sections.push(format!("- `{}`\n{}", e.created_at, indented));
}
}
sections.join("\n")
};
let notes_total = stats.project_notes.len();
format!(r#"你是炼境Dev Manager的蓝图架构师负责对飞轮运转数据进行记忆巩固分析。
##
@ -1737,6 +1827,13 @@ pub fn generate_muhe_prompt(stats: FlywheelStats) -> String {
### in_progress /id
{stalled_modules_summary}
### Claude {notes_total}
> Claude M/L MCP
>
{notes_summary}
---
## CONVENTIONS.mdv{conventions_version}
@ -1749,12 +1846,30 @@ pub fn generate_muhe_prompt(stats: FlywheelStats) -> String {
##
###
> 0
1. ×
2.
3.
4.
```
###
- **[]** <> N AB
<>
```
< 5 "笔记积累不足,暂无高频模式"
### CONVENTIONS
1. CONVENTIONS
1. 2 CONVENTIONS
2. /
3. CONVENTIONS
@ -1785,6 +1900,8 @@ pub fn generate_muhe_prompt(stats: FlywheelStats) -> String {
ai_stale_summary = ai_stale_summary,
iteration_summary = iteration_summary,
stalled_modules_summary = stalled_modules_summary,
notes_total = notes_total,
notes_summary = notes_summary,
conventions_version = effective_conventions_version(),
conventions = conventions_rules_summary(),
)

View File

@ -348,13 +348,13 @@ function ProjectRow({
<ProjectCard project={p} role={role} spacesJson={spacesJson} />
</div>
{/* 右:部署信息(与卡片同行对齐) */}
<div className="w-72 shrink-0 p-4 flex flex-col gap-2">
{/* 状态 + 角色 */}
<div className="flex items-center gap-2 flex-wrap">
{/* 右:部署信息 */}
<div className="w-72 shrink-0 p-4 flex flex-col gap-2.5">
{/* 状态 + 角色 + 时间 */}
<div className="flex items-center gap-1.5 flex-wrap">
{p.status && <StatusBadge status={p.status} />}
{role && (
<span className="text-[10px] px-1.5 py-0.5 rounded bg-purple-50 text-purple-600 border border-purple-100">
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-purple-50 text-purple-600 border border-purple-100 font-medium">
{role}
</span>
)}
@ -363,38 +363,52 @@ function ProjectRow({
)}
</div>
{/* recent_focus */}
{/* 当前专注 */}
{p.recent_focus && (
<p className="text-[11px] text-indigo-500 leading-relaxed line-clamp-2">
<div className="bg-indigo-50 border border-indigo-100 rounded-md px-2.5 py-2">
<p className="text-[11px] text-indigo-600 leading-relaxed line-clamp-3">
📌 {p.recent_focus}
</p>
</div>
)}
{/* URL */}
<div className="flex flex-wrap gap-1.5">
{/* 部署环境入口 */}
{(p.prod_url || p.staging_url) && (
<div className="flex flex-col gap-1.5">
{p.prod_url && (
<button
onClick={() => openUrl(p.prod_url!).catch(() => {})}
className="flex items-center gap-1 text-[11px] px-2 py-0.5 rounded bg-emerald-50 text-emerald-700 border border-emerald-200 hover:bg-emerald-100 transition-colors"
className="flex items-center gap-2 text-[11px] px-2.5 py-2 rounded-md bg-emerald-50 text-emerald-700 border border-emerald-200 hover:bg-emerald-100 transition-colors w-full text-left"
title={p.prod_url}
>
🌐
<span>🌐</span>
<span className="font-medium"></span>
<svg className="w-3 h-3 ml-auto text-emerald-400 shrink-0" viewBox="0 0 16 16" fill="currentColor">
<path d="M6.22 8.72a.75.75 0 001.06 1.06l5.22-5.22v1.69a.75.75 0 001.5 0V3a.75.75 0 00-.75-.75H9.5a.75.75 0 000 1.5h1.69L6.22 8.72z"/><path d="M3.5 6.75a.75.75 0 00-.75.75v5.75c0 .414.336.75.75.75h5.75a.75.75 0 000-1.5H4.25V7.5a.75.75 0 00-.75-.75z"/>
</svg>
</button>
)}
{p.staging_url && (
<button
onClick={() => openUrl(p.staging_url!).catch(() => {})}
className="flex items-center gap-1 text-[11px] px-2 py-0.5 rounded bg-amber-50 text-amber-700 border border-amber-200 hover:bg-amber-100 transition-colors"
className="flex items-center gap-2 text-[11px] px-2.5 py-2 rounded-md bg-amber-50 text-amber-700 border border-amber-200 hover:bg-amber-100 transition-colors w-full text-left"
title={p.staging_url}
>
🧪
<span>🧪</span>
<span className="font-medium"></span>
<svg className="w-3 h-3 ml-auto text-amber-400 shrink-0" viewBox="0 0 16 16" fill="currentColor">
<path d="M6.22 8.72a.75.75 0 001.06 1.06l5.22-5.22v1.69a.75.75 0 001.5 0V3a.75.75 0 00-.75-.75H9.5a.75.75 0 000 1.5h1.69L6.22 8.72z"/><path d="M3.5 6.75a.75.75 0 00-.75.75v5.75c0 .414.336.75.75.75h5.75a.75.75 0 000-1.5H4.25V7.5a.75.75 0 00-.75-.75z"/>
</svg>
</button>
)}
</div>
)}
{/* 服务器备注 */}
{p.server_note && (
<p className="text-[11px] text-gray-400" title={p.server_note}>🖥 {p.server_note}</p>
<div className="bg-gray-50 border border-gray-100 rounded-md px-2.5 py-2">
<p className="text-[11px] text-gray-500 leading-relaxed">🖥 {p.server_note}</p>
</div>
)}
{/* 无信息提示 */}
@ -402,13 +416,13 @@ function ProjectRow({
<p className="text-[11px] text-gray-300 italic"></p>
)}
{/* 部署按钮 */}
{/* 部署配置按钮 */}
<div className="mt-auto pt-1">
<button
onClick={() => onDeploy(p)}
className="px-2.5 py-1 rounded text-[11px] border border-gray-200 text-gray-500 hover:bg-gray-50 transition-colors"
className="w-full px-2.5 py-1.5 rounded-md text-[11px] border border-gray-200 text-gray-500 hover:bg-gray-50 transition-colors"
>
🚀
🚀
</button>
</div>
</div>

View File

@ -354,11 +354,6 @@ export function ProjectCard({ project: p, role, spacesJson }: Props) {
{p.type}
</span>
)}
{role && (
<span className="px-1.5 py-0.5 rounded text-xs bg-purple-50 text-purple-600 border border-purple-100">
{role}
</span>
)}
</div>
{p.description && (
<p className="text-xs text-gray-400 mt-1 leading-relaxed line-clamp-2">{p.description}</p>
@ -407,13 +402,6 @@ export function ProjectCard({ project: p, role, spacesJson }: Props) {
{/* ── 元信息区 ── */}
<div className="px-5 flex flex-col gap-2.5 pb-3">
{/* Recent focus */}
{p.recent_focus && (
<p className="text-xs text-indigo-500 bg-indigo-50 rounded-md px-2.5 py-1.5 leading-relaxed">
📌 {p.recent_focus}
</p>
)}
{/* Tech stack tags */}
{tags.length > 0 && (
<div className="flex flex-wrap gap-1">
@ -454,34 +442,6 @@ export function ProjectCard({ project: p, role, spacesJson }: Props) {
</div>
)}
{/* 部署链接 */}
{(p.prod_url || p.staging_url || p.server_note) && (
<div className="flex items-center gap-2 flex-wrap">
{p.prod_url && (
<button
onClick={() => openUrl(p.prod_url!).catch(() => {})}
className="flex items-center gap-1 text-xs px-2.5 py-1 rounded-md bg-emerald-50 text-emerald-700 border border-emerald-200 hover:bg-emerald-100 transition-colors"
title={p.prod_url}
>
🌐
</button>
)}
{p.staging_url && (
<button
onClick={() => openUrl(p.staging_url!).catch(() => {})}
className="flex items-center gap-1 text-xs px-2.5 py-1 rounded-md bg-amber-50 text-amber-700 border border-amber-200 hover:bg-amber-100 transition-colors"
title={p.staging_url}
>
🧪
</button>
)}
{p.server_note && (
<span className="text-xs text-gray-400 truncate" title={p.server_note}>
🖥 {p.server_note}
</span>
)}
</div>
)}
</div>
{/* ── Git 状态栏 ── */}

View File

@ -1057,6 +1057,12 @@ export interface IterationJump {
rate_after: number;
}
export interface ProjectNoteEntry {
project_name: string;
content: string;
created_at: string;
}
export interface FlywheelStats {
cohorts: FlywheelCohort[];
top_blocked_reasons: BlockedReasonCount[];
@ -1070,6 +1076,8 @@ export interface FlywheelStats {
stalled_modules: string[];
total_projects_scanned: number;
projects_with_data: number;
/** 跨项目复盘笔记(最近 50 条) */
project_notes: ProjectNoteEntry[];
}
export const getFlywheelStats = (projectPaths: string[]) =>