feat(feature-pruning): 撤销零数据/死代码功能(留撤盘点批次 1) #46
@ -56,7 +56,8 @@ modules:
|
|||||||
- id: project-tools
|
- id: project-tools
|
||||||
name: 项目工具链
|
name: 项目工具链
|
||||||
area: frontend
|
area: frontend
|
||||||
status: done
|
status: abandoned
|
||||||
|
note: "2026-10-04 feature-pruning B1 撤销项目级工具(project_tools 0 行);全局快启保留为 global_tools"
|
||||||
progress: 100
|
progress: 100
|
||||||
position: [500, 150]
|
position: [500, 150]
|
||||||
|
|
||||||
@ -271,7 +272,8 @@ modules:
|
|||||||
- id: user-project-todos
|
- id: user-project-todos
|
||||||
name: 项目个人备忘
|
name: 项目个人备忘
|
||||||
area: frontend
|
area: frontend
|
||||||
status: done
|
status: abandoned
|
||||||
|
note: "2026-10-04 feature-pruning B1 撤销(1 条,表保留不删)"
|
||||||
progress: 100
|
progress: 100
|
||||||
position: [2000, 150]
|
position: [2000, 150]
|
||||||
|
|
||||||
@ -329,14 +331,16 @@ modules:
|
|||||||
- id: deploy-info
|
- id: deploy-info
|
||||||
name: 项目部署信息
|
name: 项目部署信息
|
||||||
area: frontend
|
area: frontend
|
||||||
status: done
|
status: abandoned
|
||||||
|
note: "2026-10-04 feature-pruning B1 撤销部署命令(deploy_commands 0 行)与死文件;生产/测试地址展示保留"
|
||||||
progress: 100
|
progress: 100
|
||||||
position: [3000, 150]
|
position: [3000, 150]
|
||||||
|
|
||||||
- id: host-apps
|
- id: host-apps
|
||||||
name: 宿主机应用管理
|
name: 宿主机应用管理
|
||||||
area: frontend
|
area: frontend
|
||||||
status: done
|
status: abandoned
|
||||||
|
note: "2026-10-04 feature-pruning B1 撤销(server_host_apps 0 行)"
|
||||||
progress: 100
|
progress: 100
|
||||||
position: [2750, 600]
|
position: [2750, 600]
|
||||||
|
|
||||||
@ -534,6 +538,15 @@ modules:
|
|||||||
note: "两层模型(人持有的意图目录 .blueprint/catalog.md + 蓝图正文),书装意图、指向事实;卡侧 intent 钉版,agent 编译可判定 /goal,确认卡兼作最小任务简报,回执写进提交。IC1 设计已确认(最小文件优先方案);IC2–IC4 只新增文档、可与 P8 并行,门控与接线等 P8 收口后做,IC13 按实测数据决定去留。"
|
note: "两层模型(人持有的意图目录 .blueprint/catalog.md + 蓝图正文),书装意图、指向事实;卡侧 intent 钉版,agent 编译可判定 /goal,确认卡兼作最小任务简报,回执写进提交。IC1 设计已确认(最小文件优先方案);IC2–IC4 只新增文档、可与 P8 并行,门控与接线等 P8 收口后做,IC13 按实测数据决定去留。"
|
||||||
position: [4000, 950]
|
position: [4000, 950]
|
||||||
|
|
||||||
|
- id: feature-pruning
|
||||||
|
name: 功能留撤盘点
|
||||||
|
area: infra
|
||||||
|
status: in_progress
|
||||||
|
progress: 40
|
||||||
|
updated: 2026-10-04
|
||||||
|
note: "按管家准入与退出阶段性过滤冗余功能;台账 docs/tasks/2026-10-04-feature-pruning.md。B1 撤零数据/死代码 agent_verify 通过待人终验(PR),B2 产品组退役待设计。"
|
||||||
|
position: [4250, 950]
|
||||||
|
|
||||||
edges:
|
edges:
|
||||||
- from: project-resource-broker
|
- from: project-resource-broker
|
||||||
to: project-dev-variables
|
to: project-dev-variables
|
||||||
|
|||||||
28
.blueprint/modules/feature-pruning.md
Normal file
28
.blueprint/modules/feature-pruning.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
id: feature-pruning
|
||||||
|
name: 功能留撤盘点
|
||||||
|
status: in_progress
|
||||||
|
progress: 40
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
项目管理改用多维表 + 子主题之前,先按「管家准入与退出」阶段性过滤冗余功能。
|
||||||
|
台账(证据、价值评估、批次状态)单一来源:`docs/tasks/2026-10-04-feature-pruning.md`,本文件只登记任务卡。
|
||||||
|
|
||||||
|
## 任务卡
|
||||||
|
|
||||||
|
### 🔵 B1 批次 1:撤销零数据 / 死代码功能
|
||||||
|
- complexity: M
|
||||||
|
- status: in_progress
|
||||||
|
- files: src-tauri/src/commands/{host_apps,activity,user_todos,deploy_info,spaces,servers,projects,templates,mod}.rs, src-tauri/src/commands/project_tools.rs → global_tools.rs, src-tauri/src/commands/publish.rs → local_git.rs, src-tauri/src/models.rs, src-tauri/src/{db,lib}.rs, src/components/dashboard/{ProjectToolsModal → GlobalToolsModal,DeployModal,HostAppsPanel,UserTodoPanel,DeployInfoPanel,Dashboard,ProjectCard,ServerManagerPanel}.tsx, src/components/network/NetworkPage.tsx, src/lib/commands.ts
|
||||||
|
- acceptance:
|
||||||
|
- agent_verify: [cargo test] 无 FAILED;[pnpm typecheck] exit 0;[grep] 全仓无 get_project_tools / get_deploy_commands / get_host_apps / get_user_todos / add_activity 残留(get_server_network_config 核查后撤回:在用,移出本卡);[旧库副本] 迁移启动不报错
|
||||||
|
- human_verify: 项目卡与服务器面板不再出现工具链、部署、宿主机应用、备忘入口,其余功能正常
|
||||||
|
- 数据原则:不 DROP 表、不删已有行,只移除建表与读写代码
|
||||||
|
- 进展(2026-10-04):agent_verify 前三项通过;旧库副本迁移未实测;server_network_config 核查后撤回(在用);待 PR 与人终验
|
||||||
|
|
||||||
|
### 📋 B2 批次 2:产品组退役 + 旧 projects 表(L,先设计)
|
||||||
|
- complexity: L
|
||||||
|
- status: todo
|
||||||
|
- 前置:接入判定改为项目 token;组→标签迁移(仅 ≥2 成员组);`list_group_projects` MCP 契约撤销(T1)
|
||||||
95
docs/tasks/2026-10-04-feature-pruning.md
Normal file
95
docs/tasks/2026-10-04-feature-pruning.md
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
# 炼境功能留撤盘点(持续维护)
|
||||||
|
|
||||||
|
> 起因(2026-10-04 用户):项目管理改用 enterprise-system 风格多维表,务实功能收进项目详情「子主题」,冗余功能去掉。
|
||||||
|
> 本文是**阶段性过滤的单一台账**:每批处理完更新「状态」列与「处理记录」,不另开文档。
|
||||||
|
> 数据证据是 2026-10-04 快照(本机库只读计数),易变——复核时重新计数,不信本表旧数。
|
||||||
|
|
||||||
|
## 0. 评估口径
|
||||||
|
|
||||||
|
- **A|agent 价值**:管家主线程度 / 是否有 MCP 投影。
|
||||||
|
- **H|人用价值**:以本机库真实行数为准(只说明「有无数据」,不说明使用频率;只读查看型功能可能被低估)。
|
||||||
|
- **C|维护成本**:代码体量与耦合面。
|
||||||
|
- 判据对齐 AGENTS.md「管家准入与退出」:实测无收益、被替代、或维护成本超过收益 → 撤。
|
||||||
|
|
||||||
|
状态取值:`待处理` / `进行中` / `已完成` / `待用户判断` / `转后续`。
|
||||||
|
|
||||||
|
## 1. 核心保留(管家主线)
|
||||||
|
|
||||||
|
| 模块 | A | H | C | 证据 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| mcp-server / mcp-single-server | 高 | — | 高 | 所有 agent 能力的入口 |
|
||||||
|
| mcp-config-inject | 高 | — | 高 | 三端注入本体(mcp_inject.rs 约 4300 行) |
|
||||||
|
| onboarding-pack | 高 | 中 | 高 | 一键接入主线 |
|
||||||
|
| agent-client-capability-packs | 高 | — | 中 | 能力包 + 使者回传(envoy_dispositions 63 条) |
|
||||||
|
| toolchain-capability-pack | 高 | — | 中 | get_toolchain_status / prepare_toolchain |
|
||||||
|
| agent-access-reliability | 高 | 中 | 中 | 设置页 readiness |
|
||||||
|
| project-resource-broker | 高 | 中 | 高 | 凭证代办与交付状态 |
|
||||||
|
| gitea-integration | 高 | 中 | 高 | pr_events 362 条 |
|
||||||
|
| cicd-workflow | 高 | 中 | 中 | 4 个 MCP 工具 |
|
||||||
|
| blueprint-reader / view / governance | 高 | 高 | 高 | 任务上下文主载体 |
|
||||||
|
| blueprint-flywheel / flywheel-intelligence | 高 | 中 | 中 | commit_metrics 1840、commit_files 19105 |
|
||||||
|
| blueprint-feedback / project-mentor | 高 | 中 | 中 | project_notes 199、project_snapshots 2036 |
|
||||||
|
| runtime-diagnostics / health-center | 高 | 中 | 中 | runtime_logs 517 |
|
||||||
|
| project-mgmt / workspace-mgmt | 中 | 高 | 高 | 28 个项目、2 个空间;多维表数据源 |
|
||||||
|
| devtools-scan / project-env-scan | 中 | 中 | 中 | 工具链能力包输入(dev_tools 33) |
|
||||||
|
| git-ops / shell-launch | 低 | 高 | 低 | 开编辑器/终端等高频小功能 |
|
||||||
|
| sqlite-db / tray-service / auto-update / settings-page | — | 高 | 中 | 基础设施 |
|
||||||
|
|
||||||
|
## 2. 合并(能力保留,换落点)
|
||||||
|
|
||||||
|
| 模块 | 落点 | 理由 | 状态 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| project-dashboard(看板) | 多维表 | 看板与管理两套视图合一;看板「分组/独立」依赖产品组 | 待处理 |
|
||||||
|
| tags-groups 中的标签 | 多维表可选字段 | 仅 1 个标签 1 条映射,留着成本低 | 待处理 |
|
||||||
|
| repo-registry | 项目档案 repo_url + Gitea | git_repos 仅 2 条,已被 gitea_repos(7)替代 | 待处理 |
|
||||||
|
| server-registry + ssh-config-sync + vscode-remote + server-software + service-config | 资源页「服务器」一页 + 项目详情子主题「部署」 | 5 个模块是同一件事(servers 2 / server_software 9 / service_configs 5) | 待处理 |
|
||||||
|
| cloud-products + cloud-docker-apps + cloud-db-instances | 「云资源」一页 | 同上(7 / 1 / 5) | 待处理 |
|
||||||
|
| agent-infrastructure | 并入接入包 | 与一键接入重叠,入口藏在蓝图弹窗 | 待处理 |
|
||||||
|
| blueprint-onboarding(提示词) | 并入接入面板 | 被一键接入取代,MCP 工具保留 | 待处理 |
|
||||||
|
| api-keys | 并入 credential_store 统一凭证存储 | 11 条真实使用;**需先核实是否明文存储** | 待处理 |
|
||||||
|
|
||||||
|
## 3. 撤销
|
||||||
|
|
||||||
|
### 3a. 零数据 / 死代码(批次 1)
|
||||||
|
|
||||||
|
| 项 | 证据 | 改动范围 | 状态 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| project-tools 项目工具链 | project_tools 0 行 | 项目级 command 删除;文件改名 global_tools.rs / GlobalToolsModal(全局快启 global_tools 有 4 条,保留) | 已完成 |
|
||||||
|
| 部署命令 deploy_commands / deploy-info | deploy_commands 0 行;deploy_info.rs 未编译、DeployInfoPanel 为 `export {}` | spaces.rs 的 4 个部署 command、DeployModal 与「部署配置」按钮、两个死文件(生产/测试地址展示保留) | 已完成 |
|
||||||
|
| host-apps 宿主机应用 | server_host_apps 0 行 | host_apps.rs、HostAppsPanel、ServerManagerPanel 入口、删服务器时的级联清理 | 已完成 |
|
||||||
|
| ~~server_network_config~~ | 0 行,但 set_server_mirror 写、安装命令生成读、ServerSoftwareBlock 在调 | **撤回:属在用的镜像选择功能**,归入「合并 → 服务器」一并考虑 | 不处理 |
|
||||||
|
| activity_log / relations | 均 0 行;add_activity 无调用点,get_activity 恒为空 | activity.rs、ProjectCard「动态」按钮与活动区、get_project 去掉 recent_activities、projects.rs 删除清理 | 已完成 |
|
||||||
|
| user-project-todos 个人备忘 | 1 条(表与该行保留在库中,界面不再可见) | user_todos.rs、UserTodoPanel、ProjectCard 入口 | 已完成 |
|
||||||
|
| github-auth / github-publish 残留 | git_accounts 仅 db.rs 建表、无引用;github_client_* 设置种子 | db.rs 建表、索引与种子删除;publish.rs 改名 local_git.rs(内容是本地 git 与 WSL 路径辅助) | 已完成 |
|
||||||
|
| NetworkPage 孤儿文件 | `export {}` | 删文件 | 已完成 |
|
||||||
|
|
||||||
|
批次 1 数据原则:**不 DROP 表、不删已有行**(可逆);只移除建表语句与读写代码,旧库残留表无害。
|
||||||
|
|
||||||
|
### 3b. 需先设计(批次 2,L 级)
|
||||||
|
|
||||||
|
| 项 | 为什么不能直接删 | 状态 |
|
||||||
|
|---|---|---|
|
||||||
|
| 产品组本体 + product-group-mcp | 约 150 处引用;被当作「已接入」标记(MCP 重注入判断、健康统计、一键接入 product_group_buff 步骤);`list_group_projects` 是 MCP 契约(T1)。须先把接入判定改为项目 token、组→标签迁移(仅 ≥2 成员的组) | 转后续 |
|
||||||
|
| 旧 `projects` 表 | commit_metrics.rs 仍 LEFT JOIN;deploy_commands 外键指向它;credential_store 测试插入它 | 转后续 |
|
||||||
|
|
||||||
|
## 4. 待用户判断
|
||||||
|
|
||||||
|
| 项 | 证据 | C | 倾向 | 状态 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| wsl-proxy + v2rayn-import | 受监控工具 1 个、network_mode_log 0 | 约 1050 行 | 撤或移出炼境(与管家定位无关) | 待用户判断 |
|
||||||
|
| source-library 源码库 | 2 个项目、4 个版本、0 个分类 | 630 行 | 撤 | 待用户判断 |
|
||||||
|
| project-templates 项目模板 | 15 个模板 / 151 个文件(未区分是否为种子) | 约 2460 行 | 视实际使用 | 待用户判断 |
|
||||||
|
| GroupResourceCanvas 资源画布 | canvas_states 6 条 | 约 1370 行 | 随产品组撤 | 待用户判断 |
|
||||||
|
| claude-autonomy-config | 只读展示 beast-mode「狂暴模式」安装状态(初判「绑 Claude 违反跨 agent」不准确,已从撤销移来) | 约 80 行 + 弹窗 | 视是否还看 | 待用户判断 |
|
||||||
|
|
||||||
|
## 5. 对多维表方案的影响
|
||||||
|
|
||||||
|
- 标签几乎未用(1 条),预置视图不宜以标签分组为主;优先用已有数据的字段:空间、状态、类型、技术栈、接入状态。
|
||||||
|
- 项目详情抽屉的子主题 tab 草案:概要 / 蓝图与导师 / 交付(CI、Gitea、PR)/ 环境(工具链、开发变量)/ 部署(服务器)。
|
||||||
|
|
||||||
|
## 6. 处理记录
|
||||||
|
|
||||||
|
| 日期 | 批次 | 内容 | 证据 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 2026-10-04 | — | 建立台账;核查后更正三处初判(claude-autonomy-config 移入待判断;旧 projects 表、产品组移入批次 2) | 本会话盘点 |
|
||||||
|
| 2026-10-04 | B1 | 3a 七项完成、server_network_config 撤回;分支 feat/feature-pruning。db.rs 移除 relations/activity_log/project_tools/git_accounts/deploy_commands/user_project_todos/server_host_apps 建表及两段 FK 迁移,不 DROP 旧表 | cargo test 408 通过 0 失败;pnpm typecheck exit 0;被撤 command 全仓 grep 零残留。**未做**:旧库副本迁移实测(复制真实库的命令被拒),待人终验 |
|
||||||
@ -1,46 +0,0 @@
|
|||||||
use crate::db::pool;
|
|
||||||
use crate::models::ActivityLog;
|
|
||||||
use rusqlite::params;
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn get_activity(project_id: String, limit: Option<i64>) -> Result<Vec<ActivityLog>, String> {
|
|
||||||
let conn = pool().get().map_err(|e| e.to_string())?;
|
|
||||||
let lim = limit.unwrap_or(20);
|
|
||||||
let mut stmt = conn
|
|
||||||
.prepare(
|
|
||||||
"SELECT * FROM activity_log WHERE project_id = ?1 ORDER BY created_at DESC LIMIT ?2",
|
|
||||||
)
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
let result = stmt
|
|
||||||
.query_map(params![project_id, lim], ActivityLog::from_row)
|
|
||||||
.map_err(|e| e.to_string())?
|
|
||||||
.collect::<Result<Vec<_>, _>>()
|
|
||||||
.map_err(|e| e.to_string());
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn add_activity(project_id: String, summary: String) -> Result<(), String> {
|
|
||||||
let conn = pool().get().map_err(|e| e.to_string())?;
|
|
||||||
let exists: bool = conn
|
|
||||||
.query_row(
|
|
||||||
"SELECT 1 FROM project_workspaces WHERE id = ?1",
|
|
||||||
params![project_id],
|
|
||||||
|_| Ok(true),
|
|
||||||
)
|
|
||||||
.unwrap_or(false);
|
|
||||||
if !exists {
|
|
||||||
return Err(format!("Project not found: {project_id}"));
|
|
||||||
}
|
|
||||||
conn.execute(
|
|
||||||
"INSERT INTO activity_log (project_id, summary, created_at) VALUES (?1, ?2, datetime('now'))",
|
|
||||||
params![project_id, summary],
|
|
||||||
)
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
conn.execute(
|
|
||||||
"UPDATE project_workspaces SET updated_at = datetime('now') WHERE id = ?1",
|
|
||||||
params![project_id],
|
|
||||||
)
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@ -1,194 +0,0 @@
|
|||||||
use rusqlite::{params, Connection};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct DeployInfo {
|
|
||||||
pub id: String,
|
|
||||||
pub project_id: String,
|
|
||||||
pub server_host: Option<String>,
|
|
||||||
pub server_port: Option<String>,
|
|
||||||
pub username: Option<String>,
|
|
||||||
pub password: Option<String>,
|
|
||||||
pub ssh_key_path: Option<String>,
|
|
||||||
pub quick_links: Option<String>, // JSON: [{label, url}]
|
|
||||||
pub notes: Option<String>,
|
|
||||||
pub updated_at: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct SaveDeployInfoInput {
|
|
||||||
pub server_host: Option<String>,
|
|
||||||
pub server_port: Option<String>,
|
|
||||||
pub username: Option<String>,
|
|
||||||
pub password: Option<String>,
|
|
||||||
pub ssh_key_path: Option<String>,
|
|
||||||
pub quick_links: Option<String>,
|
|
||||||
pub notes: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 业务函数 ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
pub fn get_info(conn: &Connection, project_id: &str) -> Option<DeployInfo> {
|
|
||||||
conn.query_row(
|
|
||||||
"SELECT id, project_id, server_host, server_port, username, password,
|
|
||||||
ssh_key_path, quick_links, notes, updated_at
|
|
||||||
FROM project_deploy_info WHERE project_id = ?1",
|
|
||||||
params![project_id],
|
|
||||||
|row| {
|
|
||||||
Ok(DeployInfo {
|
|
||||||
id: row.get(0)?,
|
|
||||||
project_id: row.get(1)?,
|
|
||||||
server_host: row.get(2)?,
|
|
||||||
server_port: row.get(3)?,
|
|
||||||
username: row.get(4)?,
|
|
||||||
password: row.get(5)?,
|
|
||||||
ssh_key_path: row.get(6)?,
|
|
||||||
quick_links: row.get(7)?,
|
|
||||||
notes: row.get(8)?,
|
|
||||||
updated_at: row.get(9)?,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn save_info(
|
|
||||||
conn: &Connection,
|
|
||||||
project_id: &str,
|
|
||||||
input: &SaveDeployInfoInput,
|
|
||||||
) -> Result<DeployInfo, String> {
|
|
||||||
let existing = get_info(conn, project_id);
|
|
||||||
let id = existing
|
|
||||||
.as_ref()
|
|
||||||
.map(|e| e.id.clone())
|
|
||||||
.unwrap_or_else(|| format!("deploy-{}", uuid::Uuid::new_v4()));
|
|
||||||
|
|
||||||
conn.execute(
|
|
||||||
"INSERT INTO project_deploy_info
|
|
||||||
(id, project_id, server_host, server_port, username, password,
|
|
||||||
ssh_key_path, quick_links, notes, updated_at)
|
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, datetime('now'))
|
|
||||||
ON CONFLICT(project_id) DO UPDATE SET
|
|
||||||
server_host = ?3,
|
|
||||||
server_port = ?4,
|
|
||||||
username = ?5,
|
|
||||||
password = ?6,
|
|
||||||
ssh_key_path = ?7,
|
|
||||||
quick_links = ?8,
|
|
||||||
notes = ?9,
|
|
||||||
updated_at = datetime('now')",
|
|
||||||
params![
|
|
||||||
id,
|
|
||||||
project_id,
|
|
||||||
input.server_host,
|
|
||||||
input.server_port,
|
|
||||||
input.username,
|
|
||||||
input.password,
|
|
||||||
input.ssh_key_path,
|
|
||||||
input.quick_links,
|
|
||||||
input.notes,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
get_info(conn, project_id).ok_or_else(|| "保存后查询失败".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Tauri command 薄包装层 ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn get_deploy_info(project_id: String) -> Result<Option<DeployInfo>, String> {
|
|
||||||
let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
|
|
||||||
Ok(get_info(&conn, &project_id))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn save_deploy_info(
|
|
||||||
project_id: String,
|
|
||||||
input: SaveDeployInfoInput,
|
|
||||||
) -> Result<DeployInfo, String> {
|
|
||||||
let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
|
|
||||||
save_info(&conn, &project_id, &input)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 单元测试 ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use rusqlite::Connection;
|
|
||||||
|
|
||||||
fn setup() -> Connection {
|
|
||||||
let conn = Connection::open_in_memory().unwrap();
|
|
||||||
conn.execute_batch(
|
|
||||||
"CREATE TABLE project_deploy_info (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
project_id TEXT NOT NULL UNIQUE,
|
|
||||||
server_host TEXT,
|
|
||||||
server_port TEXT DEFAULT '22',
|
|
||||||
username TEXT,
|
|
||||||
password TEXT,
|
|
||||||
ssh_key_path TEXT,
|
|
||||||
quick_links TEXT,
|
|
||||||
notes TEXT,
|
|
||||||
updated_at TEXT DEFAULT (datetime('now'))
|
|
||||||
);",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
conn
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_empty() {
|
|
||||||
let conn = setup();
|
|
||||||
assert!(get_info(&conn, "proj-1").is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_save_and_get() {
|
|
||||||
let conn = setup();
|
|
||||||
let input = SaveDeployInfoInput {
|
|
||||||
server_host: Some("192.168.1.100".to_string()),
|
|
||||||
server_port: Some("22".to_string()),
|
|
||||||
username: Some("root".to_string()),
|
|
||||||
password: Some("secret".to_string()),
|
|
||||||
ssh_key_path: None,
|
|
||||||
quick_links: Some(r#"[{"label":"控制台","url":"https://example.com"}]"#.to_string()),
|
|
||||||
notes: Some("测试服务器".to_string()),
|
|
||||||
};
|
|
||||||
let info = save_info(&conn, "proj-1", &input).unwrap();
|
|
||||||
assert_eq!(info.server_host.as_deref(), Some("192.168.1.100"));
|
|
||||||
assert_eq!(info.username.as_deref(), Some("root"));
|
|
||||||
|
|
||||||
let fetched = get_info(&conn, "proj-1").unwrap();
|
|
||||||
assert_eq!(fetched.password.as_deref(), Some("secret"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_upsert_overwrites() {
|
|
||||||
let conn = setup();
|
|
||||||
let input1 = SaveDeployInfoInput {
|
|
||||||
server_host: Some("1.1.1.1".to_string()),
|
|
||||||
server_port: Some("22".to_string()),
|
|
||||||
username: Some("admin".to_string()),
|
|
||||||
password: None,
|
|
||||||
ssh_key_path: None,
|
|
||||||
quick_links: None,
|
|
||||||
notes: None,
|
|
||||||
};
|
|
||||||
save_info(&conn, "proj-1", &input1).unwrap();
|
|
||||||
|
|
||||||
let input2 = SaveDeployInfoInput {
|
|
||||||
server_host: Some("2.2.2.2".to_string()),
|
|
||||||
server_port: Some("2222".to_string()),
|
|
||||||
username: Some("deploy".to_string()),
|
|
||||||
password: Some("new-pass".to_string()),
|
|
||||||
ssh_key_path: None,
|
|
||||||
quick_links: None,
|
|
||||||
notes: None,
|
|
||||||
};
|
|
||||||
let info = save_info(&conn, "proj-1", &input2).unwrap();
|
|
||||||
assert_eq!(info.server_host.as_deref(), Some("2.2.2.2"));
|
|
||||||
assert_eq!(info.username.as_deref(), Some("deploy"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -6,9 +6,8 @@ use std::process::Command as StdCommand;
|
|||||||
// ── 数据结构 ──────────────────────────────────────────────────────────────────
|
// ── 数据结构 ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct ProjectTool {
|
pub struct GlobalTool {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub project_id: String,
|
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub icon: String,
|
pub icon: String,
|
||||||
pub exe_path: String,
|
pub exe_path: String,
|
||||||
@ -17,7 +16,7 @@ pub struct ProjectTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct ProjectToolInput {
|
pub struct GlobalToolInput {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub icon: String,
|
pub icon: String,
|
||||||
pub exe_path: String,
|
pub exe_path: String,
|
||||||
@ -25,94 +24,10 @@ pub struct ProjectToolInput {
|
|||||||
pub sort_order: Option<i32>,
|
pub sort_order: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Tauri 命令 ────────────────────────────────────────────────────────────────
|
// ── 全局快启命令(写入 global_tools 表)────────────────────────────────────────
|
||||||
|
|
||||||
/// 获取某个项目的所有工具
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn get_project_tools(project_id: String) -> Result<Vec<ProjectTool>, String> {
|
|
||||||
let conn = pool().get().map_err(|e| e.to_string())?;
|
|
||||||
let mut stmt = conn
|
|
||||||
.prepare(
|
|
||||||
"SELECT id, project_id, name, icon, exe_path, args, sort_order
|
|
||||||
FROM project_tools
|
|
||||||
WHERE project_id = ?1
|
|
||||||
ORDER BY sort_order ASC, rowid ASC",
|
|
||||||
)
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
let rows = stmt
|
|
||||||
.query_map(params![project_id], |row| {
|
|
||||||
Ok(ProjectTool {
|
|
||||||
id: row.get(0)?,
|
|
||||||
project_id: row.get(1)?,
|
|
||||||
name: row.get(2)?,
|
|
||||||
icon: row.get(3)?,
|
|
||||||
exe_path: row.get(4)?,
|
|
||||||
args: row.get(5)?,
|
|
||||||
sort_order: row.get(6)?,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.map_err(|e| e.to_string())?
|
|
||||||
.collect::<Result<Vec<_>, _>>()
|
|
||||||
.map_err(|e| e.to_string());
|
|
||||||
rows
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 添加工具到项目
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn add_project_tool(project_id: String, input: ProjectToolInput) -> Result<String, String> {
|
|
||||||
let conn = pool().get().map_err(|e| e.to_string())?;
|
|
||||||
let id = uuid::Uuid::new_v4().to_string();
|
|
||||||
conn.execute(
|
|
||||||
"INSERT INTO project_tools (id, project_id, name, icon, exe_path, args, sort_order)
|
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
|
||||||
params![
|
|
||||||
id,
|
|
||||||
project_id,
|
|
||||||
input.name,
|
|
||||||
input.icon,
|
|
||||||
input.exe_path,
|
|
||||||
input.args,
|
|
||||||
input.sort_order.unwrap_or(0),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
Ok(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 更新工具信息
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn update_project_tool(id: String, input: ProjectToolInput) -> Result<(), String> {
|
|
||||||
let conn = pool().get().map_err(|e| e.to_string())?;
|
|
||||||
conn.execute(
|
|
||||||
"UPDATE project_tools
|
|
||||||
SET name = ?1, icon = ?2, exe_path = ?3, args = ?4, sort_order = ?5
|
|
||||||
WHERE id = ?6",
|
|
||||||
params![
|
|
||||||
input.name,
|
|
||||||
input.icon,
|
|
||||||
input.exe_path,
|
|
||||||
input.args,
|
|
||||||
input.sort_order.unwrap_or(0),
|
|
||||||
id,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 删除项目工具
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn remove_project_tool(id: String) -> Result<(), String> {
|
|
||||||
let conn = pool().get().map_err(|e| e.to_string())?;
|
|
||||||
conn.execute("DELETE FROM project_tools WHERE id = ?1", params![id])
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 全局快启命令(写入 global_tools 表,无项目 FK 约束)────────────────────────
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_global_tools() -> Result<Vec<ProjectTool>, String> {
|
pub fn get_global_tools() -> Result<Vec<GlobalTool>, String> {
|
||||||
let conn = pool().get().map_err(|e| e.to_string())?;
|
let conn = pool().get().map_err(|e| e.to_string())?;
|
||||||
let mut stmt = conn
|
let mut stmt = conn
|
||||||
.prepare(
|
.prepare(
|
||||||
@ -123,9 +38,8 @@ pub fn get_global_tools() -> Result<Vec<ProjectTool>, String> {
|
|||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
let rows = stmt
|
let rows = stmt
|
||||||
.query_map([], |row| {
|
.query_map([], |row| {
|
||||||
Ok(ProjectTool {
|
Ok(GlobalTool {
|
||||||
id: row.get(0)?,
|
id: row.get(0)?,
|
||||||
project_id: "__global__".to_string(),
|
|
||||||
name: row.get(1)?,
|
name: row.get(1)?,
|
||||||
icon: row.get(2)?,
|
icon: row.get(2)?,
|
||||||
exe_path: row.get(3)?,
|
exe_path: row.get(3)?,
|
||||||
@ -140,7 +54,7 @@ pub fn get_global_tools() -> Result<Vec<ProjectTool>, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn add_global_tool(input: ProjectToolInput) -> Result<String, String> {
|
pub fn add_global_tool(input: GlobalToolInput) -> Result<String, String> {
|
||||||
let conn = pool().get().map_err(|e| e.to_string())?;
|
let conn = pool().get().map_err(|e| e.to_string())?;
|
||||||
let id = uuid::Uuid::new_v4().to_string();
|
let id = uuid::Uuid::new_v4().to_string();
|
||||||
conn.execute(
|
conn.execute(
|
||||||
@ -160,7 +74,7 @@ pub fn add_global_tool(input: ProjectToolInput) -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn update_global_tool(id: String, input: ProjectToolInput) -> Result<(), String> {
|
pub fn update_global_tool(id: String, input: GlobalToolInput) -> Result<(), String> {
|
||||||
let conn = pool().get().map_err(|e| e.to_string())?;
|
let conn = pool().get().map_err(|e| e.to_string())?;
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE global_tools
|
"UPDATE global_tools
|
||||||
@ -1,171 +0,0 @@
|
|||||||
use rusqlite::{params, Connection};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct HostApp {
|
|
||||||
pub id: String,
|
|
||||||
pub server_id: String,
|
|
||||||
pub name: String,
|
|
||||||
pub deploy_type: String,
|
|
||||||
pub service_name: Option<String>,
|
|
||||||
pub binary_path: Option<String>,
|
|
||||||
pub config_path: Option<String>,
|
|
||||||
pub log_path: Option<String>,
|
|
||||||
pub port: Option<String>,
|
|
||||||
pub start_cmd: Option<String>,
|
|
||||||
pub stop_cmd: Option<String>,
|
|
||||||
pub restart_cmd: Option<String>,
|
|
||||||
pub status_cmd: Option<String>,
|
|
||||||
pub notes: Option<String>,
|
|
||||||
pub sort_order: Option<i32>,
|
|
||||||
pub created_at: Option<String>,
|
|
||||||
pub updated_at: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct SaveHostAppInput {
|
|
||||||
pub name: String,
|
|
||||||
pub deploy_type: String,
|
|
||||||
pub service_name: Option<String>,
|
|
||||||
pub binary_path: Option<String>,
|
|
||||||
pub config_path: Option<String>,
|
|
||||||
pub log_path: Option<String>,
|
|
||||||
pub port: Option<String>,
|
|
||||||
pub start_cmd: Option<String>,
|
|
||||||
pub stop_cmd: Option<String>,
|
|
||||||
pub restart_cmd: Option<String>,
|
|
||||||
pub status_cmd: Option<String>,
|
|
||||||
pub notes: Option<String>,
|
|
||||||
pub sort_order: Option<i32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
const COLS: &str = "id, server_id, name, deploy_type, service_name, binary_path, config_path, log_path, port, start_cmd, stop_cmd, restart_cmd, status_cmd, notes, sort_order, created_at, updated_at";
|
|
||||||
|
|
||||||
fn row_to_app(row: &rusqlite::Row<'_>) -> rusqlite::Result<HostApp> {
|
|
||||||
Ok(HostApp {
|
|
||||||
id: row.get(0)?,
|
|
||||||
server_id: row.get(1)?,
|
|
||||||
name: row.get(2)?,
|
|
||||||
deploy_type: row.get(3)?,
|
|
||||||
service_name: row.get(4)?,
|
|
||||||
binary_path: row.get(5)?,
|
|
||||||
config_path: row.get(6)?,
|
|
||||||
log_path: row.get(7)?,
|
|
||||||
port: row.get(8)?,
|
|
||||||
start_cmd: row.get(9)?,
|
|
||||||
stop_cmd: row.get(10)?,
|
|
||||||
restart_cmd: row.get(11)?,
|
|
||||||
status_cmd: row.get(12)?,
|
|
||||||
notes: row.get(13)?,
|
|
||||||
sort_order: row.get(14)?,
|
|
||||||
created_at: row.get(15)?,
|
|
||||||
updated_at: row.get(16)?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list_host_apps(conn: &Connection, server_id: &str) -> Vec<HostApp> {
|
|
||||||
let sql = format!("SELECT {COLS} FROM server_host_apps WHERE server_id = ?1 ORDER BY sort_order ASC, created_at ASC");
|
|
||||||
let mut stmt = match conn.prepare(&sql) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(_) => return vec![],
|
|
||||||
};
|
|
||||||
stmt.query_map(params![server_id], row_to_app)
|
|
||||||
.map(|iter| iter.filter_map(|r| r.ok()).collect())
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_host_app(conn: &Connection, id: &str) -> Option<HostApp> {
|
|
||||||
let sql = format!("SELECT {COLS} FROM server_host_apps WHERE id = ?1");
|
|
||||||
conn.query_row(&sql, params![id], row_to_app).ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_host_app(
|
|
||||||
conn: &Connection,
|
|
||||||
server_id: &str,
|
|
||||||
input: &SaveHostAppInput,
|
|
||||||
) -> Result<HostApp, String> {
|
|
||||||
let id = format!("ha-{}", uuid::Uuid::new_v4());
|
|
||||||
conn.execute(
|
|
||||||
"INSERT INTO server_host_apps (id, server_id, name, deploy_type, service_name, binary_path, config_path, log_path, port, start_cmd, stop_cmd, restart_cmd, status_cmd, notes, sort_order)
|
|
||||||
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15)",
|
|
||||||
params![id, server_id, input.name, input.deploy_type, input.service_name, input.binary_path,
|
|
||||||
input.config_path, input.log_path, input.port, input.start_cmd, input.stop_cmd,
|
|
||||||
input.restart_cmd, input.status_cmd, input.notes, input.sort_order.unwrap_or(0)],
|
|
||||||
).map_err(|e| e.to_string())?;
|
|
||||||
get_host_app(conn, &id).ok_or_else(|| "创建后查询失败".into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_host_app(
|
|
||||||
conn: &Connection,
|
|
||||||
id: &str,
|
|
||||||
input: &SaveHostAppInput,
|
|
||||||
) -> Result<HostApp, String> {
|
|
||||||
let rows = conn
|
|
||||||
.execute(
|
|
||||||
"UPDATE server_host_apps SET name=?1, deploy_type=?2, service_name=?3, binary_path=?4,
|
|
||||||
config_path=?5, log_path=?6, port=?7, start_cmd=?8, stop_cmd=?9, restart_cmd=?10,
|
|
||||||
status_cmd=?11, notes=?12, sort_order=?13, updated_at=datetime('now') WHERE id=?14",
|
|
||||||
params![
|
|
||||||
input.name,
|
|
||||||
input.deploy_type,
|
|
||||||
input.service_name,
|
|
||||||
input.binary_path,
|
|
||||||
input.config_path,
|
|
||||||
input.log_path,
|
|
||||||
input.port,
|
|
||||||
input.start_cmd,
|
|
||||||
input.stop_cmd,
|
|
||||||
input.restart_cmd,
|
|
||||||
input.status_cmd,
|
|
||||||
input.notes,
|
|
||||||
input.sort_order.unwrap_or(0),
|
|
||||||
id
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
if rows == 0 {
|
|
||||||
return Err(format!("应用 {} 不存在", id));
|
|
||||||
}
|
|
||||||
get_host_app(conn, id).ok_or_else(|| "更新后查询失败".into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete_host_app_fn(conn: &Connection, id: &str) -> Result<(), String> {
|
|
||||||
conn.execute("DELETE FROM server_host_apps WHERE id=?1", params![id])
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete_apps_by_server(conn: &Connection, server_id: &str) -> Result<(), String> {
|
|
||||||
conn.execute(
|
|
||||||
"DELETE FROM server_host_apps WHERE server_id=?1",
|
|
||||||
params![server_id],
|
|
||||||
)
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Tauri commands ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn get_host_apps(server_id: String) -> Result<Vec<HostApp>, String> {
|
|
||||||
let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
|
|
||||||
Ok(list_host_apps(&conn, &server_id))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn add_host_app(server_id: String, input: SaveHostAppInput) -> Result<HostApp, String> {
|
|
||||||
let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
|
|
||||||
create_host_app(&conn, &server_id, &input)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn edit_host_app(id: String, input: SaveHostAppInput) -> Result<HostApp, String> {
|
|
||||||
let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
|
|
||||||
update_host_app(&conn, &id, &input)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn delete_host_app(id: String) -> Result<(), String> {
|
|
||||||
let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
|
|
||||||
delete_host_app_fn(&conn, &id)
|
|
||||||
}
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
pub mod activity;
|
|
||||||
pub mod agent_infra;
|
pub mod agent_infra;
|
||||||
pub mod api_keys;
|
pub mod api_keys;
|
||||||
pub mod blueprint;
|
pub mod blueprint;
|
||||||
@ -16,16 +15,15 @@ pub mod docker_apps;
|
|||||||
pub mod git_ops;
|
pub mod git_ops;
|
||||||
pub mod git_repos;
|
pub mod git_repos;
|
||||||
pub mod gitea;
|
pub mod gitea;
|
||||||
|
pub mod global_tools;
|
||||||
pub mod groups;
|
pub mod groups;
|
||||||
pub mod host_apps;
|
pub mod local_git;
|
||||||
pub mod mentor;
|
pub mod mentor;
|
||||||
pub mod network;
|
pub mod network;
|
||||||
pub mod onboarding;
|
pub mod onboarding;
|
||||||
pub mod project_scan;
|
pub mod project_scan;
|
||||||
pub mod project_tools;
|
|
||||||
pub mod projects;
|
pub mod projects;
|
||||||
pub mod proxy;
|
pub mod proxy;
|
||||||
pub mod publish;
|
|
||||||
pub mod risk;
|
pub mod risk;
|
||||||
pub mod server_software;
|
pub mod server_software;
|
||||||
pub mod servers;
|
pub mod servers;
|
||||||
@ -38,4 +36,3 @@ pub mod ssh_config;
|
|||||||
pub mod tags;
|
pub mod tags;
|
||||||
pub mod templates;
|
pub mod templates;
|
||||||
pub mod toolchain;
|
pub mod toolchain;
|
||||||
pub mod user_todos;
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
use crate::commands::gitea::{self, RemovedRepoLink};
|
use crate::commands::gitea::{self, RemovedRepoLink};
|
||||||
use crate::db::pool;
|
use crate::db::pool;
|
||||||
use crate::mcp_inject;
|
use crate::mcp_inject;
|
||||||
use crate::models::{ActivityLog, Project};
|
use crate::models::Project;
|
||||||
use rusqlite::{params, OptionalExtension};
|
use rusqlite::{params, OptionalExtension};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@ -93,13 +93,6 @@ pub struct UpdateProjectInput {
|
|||||||
pub startup_commands: Option<String>,
|
pub startup_commands: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
pub struct ProjectDetail {
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub project: Project,
|
|
||||||
pub recent_activities: Vec<ActivityLog>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_projects(
|
pub fn get_projects(
|
||||||
status: Option<String>,
|
status: Option<String>,
|
||||||
@ -156,27 +149,11 @@ pub fn get_projects(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_project(id: String) -> Result<ProjectDetail, String> {
|
pub fn get_project(id: String) -> Result<Project, String> {
|
||||||
let conn = pool().get().map_err(|e| e.to_string())?;
|
let conn = pool().get().map_err(|e| e.to_string())?;
|
||||||
let sql = format!("{PROJECT_VIEW_SQL} WHERE pw.id = ?1");
|
let sql = format!("{PROJECT_VIEW_SQL} WHERE pw.id = ?1");
|
||||||
let project = conn
|
conn.query_row(&sql, params![id], Project::from_row)
|
||||||
.query_row(&sql, params![id], Project::from_row)
|
.map_err(|_| format!("Project not found: {id}"))
|
||||||
.map_err(|_| format!("Project not found: {id}"))?;
|
|
||||||
let mut stmt = conn
|
|
||||||
.prepare(
|
|
||||||
"SELECT * FROM activity_log WHERE project_id = ?1 ORDER BY created_at DESC LIMIT 5",
|
|
||||||
)
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
let activities: Vec<ActivityLog> = stmt
|
|
||||||
.query_map(params![id], ActivityLog::from_row)
|
|
||||||
.map_err(|e| e.to_string())?
|
|
||||||
.collect::<Result<Vec<_>, _>>()
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
drop(stmt);
|
|
||||||
Ok(ProjectDetail {
|
|
||||||
project,
|
|
||||||
recent_activities: activities,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 登记本体(P8f)─────────────────────────────────────────────────────────────
|
// ── 登记本体(P8f)─────────────────────────────────────────────────────────────
|
||||||
@ -681,24 +658,13 @@ pub(crate) fn delete_project_records(
|
|||||||
|
|
||||||
let tx = conn.unchecked_transaction().map_err(|e| e.to_string())?;
|
let tx = conn.unchecked_transaction().map_err(|e| e.to_string())?;
|
||||||
// 清理关联数据(旧 FK 指向 projects 表,需手动清理)
|
// 清理关联数据(旧 FK 指向 projects 表,需手动清理)
|
||||||
for table in [
|
for table in ["project_group_map", "project_env_tools"] {
|
||||||
"activity_log",
|
|
||||||
"project_tools",
|
|
||||||
"project_group_map",
|
|
||||||
"project_env_tools",
|
|
||||||
"deploy_commands",
|
|
||||||
] {
|
|
||||||
tx.execute(
|
tx.execute(
|
||||||
&format!("DELETE FROM {table} WHERE project_id = ?1"),
|
&format!("DELETE FROM {table} WHERE project_id = ?1"),
|
||||||
params![id],
|
params![id],
|
||||||
)
|
)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
}
|
}
|
||||||
tx.execute(
|
|
||||||
"DELETE FROM relations WHERE from_project = ?1 OR to_project = ?1",
|
|
||||||
params![id],
|
|
||||||
)
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
// 不解绑的话,后台 Gitea 轮询会继续把 commit 与 PR 事件写到已删除的项目 ID 下
|
// 不解绑的话,后台 Gitea 轮询会继续把 commit 与 PR 事件写到已删除的项目 ID 下
|
||||||
let removed_link = gitea::remove_repo_link_records(&tx, id)?;
|
let removed_link = gitea::remove_repo_link_records(&tx, id)?;
|
||||||
|
|
||||||
|
|||||||
@ -174,9 +174,8 @@ pub fn delete_server_fn(conn: &Connection, id: &str) -> Result<(), String> {
|
|||||||
params![id],
|
params![id],
|
||||||
)
|
)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
// 级联删除服务连接配置 + 宿主机应用
|
// 级联删除服务连接配置(server_host_apps 已撤销,旧库残留行无害)
|
||||||
let _ = super::service_config::delete_configs_by_server(conn, id);
|
let _ = super::service_config::delete_configs_by_server(conn, id);
|
||||||
let _ = super::host_apps::delete_apps_by_server(conn, id);
|
|
||||||
conn.execute("DELETE FROM servers WHERE id = ?1", params![id])
|
conn.execute("DELETE FROM servers WHERE id = ?1", params![id])
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
use crate::db;
|
use crate::db;
|
||||||
use crate::models::{DeployCommand, Space};
|
use crate::models::Space;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
// ── 空间管理 ────────────────────────────────────────────────
|
// ── 空间管理 ────────────────────────────────────────────────
|
||||||
@ -81,93 +81,3 @@ pub fn set_current_space(id: String) -> Result<(), String> {
|
|||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 部署命令管理 ────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn get_deploy_commands(project_id: String) -> Result<Vec<DeployCommand>, String> {
|
|
||||||
let conn = db::pool().get().map_err(|e| e.to_string())?;
|
|
||||||
let mut stmt = conn
|
|
||||||
.prepare("SELECT id, project_id, name, command, run_in FROM deploy_commands WHERE project_id = ?1")
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
let rows = stmt
|
|
||||||
.query_map(rusqlite::params![project_id], DeployCommand::from_row)
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
rows.map(|r| r.map_err(|e| e.to_string())).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn add_deploy_command(
|
|
||||||
project_id: String,
|
|
||||||
name: String,
|
|
||||||
command: String,
|
|
||||||
run_in: String,
|
|
||||||
) -> Result<DeployCommand, String> {
|
|
||||||
let conn = db::pool().get().map_err(|e| e.to_string())?;
|
|
||||||
let id = Uuid::new_v4().to_string();
|
|
||||||
conn.execute(
|
|
||||||
"INSERT INTO deploy_commands (id, project_id, name, command, run_in) VALUES (?1, ?2, ?3, ?4, ?5)",
|
|
||||||
rusqlite::params![id, project_id, name, command, run_in],
|
|
||||||
)
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
let cmd = conn
|
|
||||||
.query_row(
|
|
||||||
"SELECT id, project_id, name, command, run_in FROM deploy_commands WHERE id = ?1",
|
|
||||||
rusqlite::params![id],
|
|
||||||
DeployCommand::from_row,
|
|
||||||
)
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
Ok(cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn remove_deploy_command(id: String) -> Result<(), String> {
|
|
||||||
let conn = db::pool().get().map_err(|e| e.to_string())?;
|
|
||||||
conn.execute(
|
|
||||||
"DELETE FROM deploy_commands WHERE id = ?1",
|
|
||||||
rusqlite::params![id],
|
|
||||||
)
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 执行部署命令,返回输出内容
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn execute_deploy(
|
|
||||||
command: String,
|
|
||||||
run_in: String,
|
|
||||||
cwd: Option<String>,
|
|
||||||
) -> Result<String, String> {
|
|
||||||
let mut cmd = if run_in == "wsl" {
|
|
||||||
let mut c = crate::silent_cmd("wsl");
|
|
||||||
c.args(["-e", "bash", "-c", &command]);
|
|
||||||
c
|
|
||||||
} else {
|
|
||||||
let mut c = crate::silent_cmd("cmd");
|
|
||||||
c.args(["/c", &command]);
|
|
||||||
c
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(dir) = cwd {
|
|
||||||
cmd.current_dir(&dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
let output = cmd.output();
|
|
||||||
|
|
||||||
match output {
|
|
||||||
Ok(out) => {
|
|
||||||
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
|
|
||||||
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
|
||||||
if out.status.success() {
|
|
||||||
Ok(if stdout.is_empty() {
|
|
||||||
"执行完成".to_string()
|
|
||||||
} else {
|
|
||||||
stdout
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(if !stderr.is_empty() { stderr } else { stdout })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => Err(format!("启动进程失败: {}", e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -220,7 +220,7 @@ pub fn create_project_from_template(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── 1. 计算项目路径 ───────────────────────────────────────────────────────
|
// ── 1. 计算项目路径 ───────────────────────────────────────────────────────
|
||||||
let win_parent = crate::commands::publish::wsl_to_win(&parent_path);
|
let win_parent = crate::commands::local_git::wsl_to_win(&parent_path);
|
||||||
let project_win_path = format!("{}\\{}", win_parent.trim_end_matches('\\'), project_name);
|
let project_win_path = format!("{}\\{}", win_parent.trim_end_matches('\\'), project_name);
|
||||||
let project_root = Path::new(&project_win_path);
|
let project_root = Path::new(&project_win_path);
|
||||||
|
|
||||||
@ -259,7 +259,7 @@ pub fn create_project_from_template(
|
|||||||
|
|
||||||
if let Some(cmds) = &tmpl.init_commands {
|
if let Some(cmds) = &tmpl.init_commands {
|
||||||
let linux_project_path = if platform == "wsl" {
|
let linux_project_path = if platform == "wsl" {
|
||||||
crate::commands::publish::extract_linux_path(&parent_path, &win_parent)
|
crate::commands::local_git::extract_linux_path(&parent_path, &win_parent)
|
||||||
.map(|p| format!("{}/{}", p.trim_end_matches('/'), project_name))
|
.map(|p| format!("{}/{}", p.trim_end_matches('/'), project_name))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|||||||
@ -1,198 +0,0 @@
|
|||||||
use rusqlite::{params, Connection};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct UserTodo {
|
|
||||||
pub id: i64,
|
|
||||||
pub project_id: String,
|
|
||||||
pub content: String,
|
|
||||||
pub done: bool,
|
|
||||||
pub sort_order: i64,
|
|
||||||
pub created_at: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 业务函数(依赖注入,可单元测试)────────────────────────────────────────────
|
|
||||||
|
|
||||||
pub fn get_todos(conn: &Connection, project_id: &str) -> Vec<UserTodo> {
|
|
||||||
let mut stmt = match conn.prepare(
|
|
||||||
"SELECT id, project_id, content, done, sort_order, created_at
|
|
||||||
FROM user_project_todos
|
|
||||||
WHERE project_id = ?1
|
|
||||||
ORDER BY sort_order ASC, id ASC",
|
|
||||||
) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(_) => return vec![],
|
|
||||||
};
|
|
||||||
stmt.query_map(params![project_id], |row| {
|
|
||||||
Ok(UserTodo {
|
|
||||||
id: row.get(0)?,
|
|
||||||
project_id: row.get(1)?,
|
|
||||||
content: row.get(2)?,
|
|
||||||
done: row.get::<_, i64>(3)? != 0,
|
|
||||||
sort_order: row.get(4)?,
|
|
||||||
created_at: row.get(5)?,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.map(|iter| iter.filter_map(|r| r.ok()).collect())
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_todo(conn: &Connection, project_id: &str, content: &str) -> Result<UserTodo, String> {
|
|
||||||
let content = content.trim();
|
|
||||||
if content.is_empty() {
|
|
||||||
return Err("内容不能为空".to_string());
|
|
||||||
}
|
|
||||||
// sort_order = 当前最大值 + 1
|
|
||||||
let max_order: i64 = conn
|
|
||||||
.query_row(
|
|
||||||
"SELECT COALESCE(MAX(sort_order), -1) FROM user_project_todos WHERE project_id = ?1",
|
|
||||||
params![project_id],
|
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.unwrap_or(-1);
|
|
||||||
|
|
||||||
conn.execute(
|
|
||||||
"INSERT INTO user_project_todos (project_id, content, sort_order) VALUES (?1, ?2, ?3)",
|
|
||||||
params![project_id, content, max_order + 1],
|
|
||||||
)
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
let id = conn.last_insert_rowid();
|
|
||||||
conn.query_row(
|
|
||||||
"SELECT id, project_id, content, done, sort_order, created_at
|
|
||||||
FROM user_project_todos WHERE id = ?1",
|
|
||||||
params![id],
|
|
||||||
|row| {
|
|
||||||
Ok(UserTodo {
|
|
||||||
id: row.get(0)?,
|
|
||||||
project_id: row.get(1)?,
|
|
||||||
content: row.get(2)?,
|
|
||||||
done: row.get::<_, i64>(3)? != 0,
|
|
||||||
sort_order: row.get(4)?,
|
|
||||||
created_at: row.get(5)?,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toggle_todo(conn: &Connection, id: i64) -> Result<(), String> {
|
|
||||||
let rows = conn
|
|
||||||
.execute(
|
|
||||||
"UPDATE user_project_todos SET done = CASE WHEN done = 0 THEN 1 ELSE 0 END WHERE id = ?1",
|
|
||||||
params![id],
|
|
||||||
)
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
if rows == 0 {
|
|
||||||
return Err(format!("todo {} 不存在", id));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete_todo(conn: &Connection, id: i64) -> Result<(), String> {
|
|
||||||
conn.execute("DELETE FROM user_project_todos WHERE id = ?1", params![id])
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Tauri command 薄包装层(仅负责获取连接)────────────────────────────────────
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn get_user_todos(project_id: String) -> Result<Vec<UserTodo>, String> {
|
|
||||||
let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
|
|
||||||
Ok(get_todos(&conn, &project_id))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn add_user_todo(project_id: String, content: String) -> Result<UserTodo, String> {
|
|
||||||
let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
|
|
||||||
add_todo(&conn, &project_id, &content)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn toggle_user_todo(id: i64) -> Result<(), String> {
|
|
||||||
let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
|
|
||||||
toggle_todo(&conn, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn delete_user_todo(id: i64) -> Result<(), String> {
|
|
||||||
let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
|
|
||||||
delete_todo(&conn, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 单元测试 ───────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use rusqlite::Connection;
|
|
||||||
|
|
||||||
fn setup() -> Connection {
|
|
||||||
let conn = Connection::open_in_memory().unwrap();
|
|
||||||
conn.execute_batch(
|
|
||||||
"CREATE TABLE user_project_todos (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
project_id TEXT NOT NULL,
|
|
||||||
content TEXT NOT NULL,
|
|
||||||
done INTEGER NOT NULL DEFAULT 0,
|
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
||||||
created_at TEXT DEFAULT (datetime('now', 'localtime'))
|
|
||||||
);",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
conn
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_add_and_get() {
|
|
||||||
let conn = setup();
|
|
||||||
add_todo(&conn, "proj-1", "买牛奶").unwrap();
|
|
||||||
add_todo(&conn, "proj-1", "写测试").unwrap();
|
|
||||||
let todos = get_todos(&conn, "proj-1");
|
|
||||||
assert_eq!(todos.len(), 2);
|
|
||||||
assert_eq!(todos[0].content, "买牛奶");
|
|
||||||
assert!(!todos[0].done);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_empty_content_rejected() {
|
|
||||||
let conn = setup();
|
|
||||||
assert!(add_todo(&conn, "proj-1", " ").is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_toggle() {
|
|
||||||
let conn = setup();
|
|
||||||
let todo = add_todo(&conn, "proj-1", "测试条目").unwrap();
|
|
||||||
toggle_todo(&conn, todo.id).unwrap();
|
|
||||||
let todos = get_todos(&conn, "proj-1");
|
|
||||||
assert!(todos[0].done);
|
|
||||||
toggle_todo(&conn, todo.id).unwrap();
|
|
||||||
let todos = get_todos(&conn, "proj-1");
|
|
||||||
assert!(!todos[0].done);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_delete() {
|
|
||||||
let conn = setup();
|
|
||||||
let todo = add_todo(&conn, "proj-1", "待删除").unwrap();
|
|
||||||
delete_todo(&conn, todo.id).unwrap();
|
|
||||||
assert!(get_todos(&conn, "proj-1").is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_empty_returns_empty() {
|
|
||||||
let conn = setup();
|
|
||||||
assert!(get_todos(&conn, "不存在的项目").is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sort_order_increments() {
|
|
||||||
let conn = setup();
|
|
||||||
add_todo(&conn, "proj-1", "第一").unwrap();
|
|
||||||
add_todo(&conn, "proj-1", "第二").unwrap();
|
|
||||||
let todos = get_todos(&conn, "proj-1");
|
|
||||||
assert!(todos[0].sort_order < todos[1].sort_order);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -63,36 +63,11 @@ pub(crate) fn migrate(conn: &rusqlite::Connection) {
|
|||||||
PRIMARY KEY (project_id, group_id)
|
PRIMARY KEY (project_id, group_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS relations (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
from_project TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
||||||
to_project TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
||||||
relation_type TEXT,
|
|
||||||
note TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS activity_log (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
||||||
summary TEXT NOT NULL,
|
|
||||||
created_at TEXT DEFAULT (datetime('now'))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS settings (
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
key TEXT PRIMARY KEY,
|
key TEXT PRIMARY KEY,
|
||||||
value TEXT NOT NULL
|
value TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS project_tools (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
icon TEXT NOT NULL DEFAULT '🔧',
|
|
||||||
exe_path TEXT NOT NULL,
|
|
||||||
args TEXT,
|
|
||||||
sort_order INTEGER DEFAULT 0
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS dev_tools (
|
CREATE TABLE IF NOT EXISTS dev_tools (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
@ -131,8 +106,6 @@ pub(crate) fn migrate(conn: &rusqlite::Connection) {
|
|||||||
INSERT OR IGNORE INTO settings (key, value) VALUES ('editor', 'cursor');
|
INSERT OR IGNORE INTO settings (key, value) VALUES ('editor', 'cursor');
|
||||||
INSERT OR IGNORE INTO settings (key, value) VALUES ('wsl_distro', 'Ubuntu');
|
INSERT OR IGNORE INTO settings (key, value) VALUES ('wsl_distro', 'Ubuntu');
|
||||||
INSERT OR IGNORE INTO settings (key, value) VALUES ('dev_mode', '0');
|
INSERT OR IGNORE INTO settings (key, value) VALUES ('dev_mode', '0');
|
||||||
INSERT OR IGNORE INTO settings (key, value) VALUES ('github_client_id', '');
|
|
||||||
INSERT OR IGNORE INTO settings (key, value) VALUES ('github_client_secret', '');
|
|
||||||
INSERT OR IGNORE INTO settings (key, value) VALUES ('mcp_port', '27190');
|
INSERT OR IGNORE INTO settings (key, value) VALUES ('mcp_port', '27190');
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
@ -147,14 +120,6 @@ pub(crate) fn migrate(conn: &rusqlite::Connection) {
|
|||||||
// 增量迁移:Git 多空间功能所需新表
|
// 增量迁移:Git 多空间功能所需新表
|
||||||
conn.execute_batch(
|
conn.execute_batch(
|
||||||
"
|
"
|
||||||
CREATE TABLE IF NOT EXISTS git_accounts (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
username TEXT NOT NULL UNIQUE,
|
|
||||||
avatar_url TEXT,
|
|
||||||
provider TEXT NOT NULL DEFAULT 'github',
|
|
||||||
created_at TEXT DEFAULT (datetime('now'))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS spaces (
|
CREATE TABLE IF NOT EXISTS spaces (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
@ -162,24 +127,10 @@ pub(crate) fn migrate(conn: &rusqlite::Connection) {
|
|||||||
is_current INTEGER NOT NULL DEFAULT 0,
|
is_current INTEGER NOT NULL DEFAULT 0,
|
||||||
created_at TEXT DEFAULT (datetime('now'))
|
created_at TEXT DEFAULT (datetime('now'))
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS deploy_commands (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
command TEXT NOT NULL,
|
|
||||||
run_in TEXT NOT NULL DEFAULT 'windows'
|
|
||||||
);
|
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
.expect("migrate git spaces");
|
.expect("migrate git spaces");
|
||||||
|
|
||||||
// 增量迁移:git_accounts 补 UNIQUE 索引(已存在则忽略)
|
|
||||||
let _ = conn.execute(
|
|
||||||
"CREATE UNIQUE INDEX IF NOT EXISTS idx_git_accounts_username ON git_accounts(username)",
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
// 增量迁移:projects 表新增 Git 相关字段(已存在则忽略)
|
// 增量迁移:projects 表新增 Git 相关字段(已存在则忽略)
|
||||||
let _ = conn.execute("ALTER TABLE projects ADD COLUMN space_id TEXT", []);
|
let _ = conn.execute("ALTER TABLE projects ADD COLUMN space_id TEXT", []);
|
||||||
let _ = conn.execute("ALTER TABLE projects ADD COLUMN repo_url TEXT", []);
|
let _ = conn.execute("ALTER TABLE projects ADD COLUMN repo_url TEXT", []);
|
||||||
@ -341,77 +292,7 @@ pub(crate) fn migrate(conn: &rusqlite::Connection) {
|
|||||||
.expect("migrate project_group_map FK to project_workspaces");
|
.expect("migrate project_group_map FK to project_workspaces");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 迁移 project_tools / activity_log / project_env_tools:
|
// 迁移 project_env_tools:将外键从 projects(id) 改为 project_workspaces(id)
|
||||||
// 将外键从 projects(id) 改为 project_workspaces(id)
|
|
||||||
let pt_sql: Option<String> = conn
|
|
||||||
.query_row(
|
|
||||||
"SELECT sql FROM sqlite_master WHERE type='table' AND name='project_tools'",
|
|
||||||
[],
|
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.ok();
|
|
||||||
if pt_sql
|
|
||||||
.as_deref()
|
|
||||||
.map(|s| s.contains("REFERENCES projects"))
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
|
||||||
conn.execute_batch(
|
|
||||||
"
|
|
||||||
PRAGMA foreign_keys=OFF;
|
|
||||||
CREATE TABLE project_tools_new (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
project_id TEXT NOT NULL REFERENCES project_workspaces(id) ON DELETE CASCADE,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
icon TEXT NOT NULL DEFAULT '🔧',
|
|
||||||
exe_path TEXT NOT NULL,
|
|
||||||
args TEXT,
|
|
||||||
sort_order INTEGER DEFAULT 0
|
|
||||||
);
|
|
||||||
INSERT OR IGNORE INTO project_tools_new
|
|
||||||
SELECT t.id, t.project_id, t.name, t.icon, t.exe_path, t.args, t.sort_order
|
|
||||||
FROM project_tools t
|
|
||||||
INNER JOIN project_workspaces pw ON pw.id = t.project_id;
|
|
||||||
DROP TABLE project_tools;
|
|
||||||
ALTER TABLE project_tools_new RENAME TO project_tools;
|
|
||||||
PRAGMA foreign_keys=ON;
|
|
||||||
",
|
|
||||||
)
|
|
||||||
.expect("migrate project_tools FK to project_workspaces");
|
|
||||||
}
|
|
||||||
|
|
||||||
let al_sql: Option<String> = conn
|
|
||||||
.query_row(
|
|
||||||
"SELECT sql FROM sqlite_master WHERE type='table' AND name='activity_log'",
|
|
||||||
[],
|
|
||||||
|row| row.get(0),
|
|
||||||
)
|
|
||||||
.ok();
|
|
||||||
if al_sql
|
|
||||||
.as_deref()
|
|
||||||
.map(|s| s.contains("REFERENCES projects"))
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
|
||||||
conn.execute_batch(
|
|
||||||
"
|
|
||||||
PRAGMA foreign_keys=OFF;
|
|
||||||
CREATE TABLE activity_log_new (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
project_id TEXT NOT NULL REFERENCES project_workspaces(id) ON DELETE CASCADE,
|
|
||||||
summary TEXT NOT NULL,
|
|
||||||
created_at TEXT DEFAULT (datetime('now'))
|
|
||||||
);
|
|
||||||
INSERT OR IGNORE INTO activity_log_new (id, project_id, summary, created_at)
|
|
||||||
SELECT a.id, a.project_id, a.summary, a.created_at
|
|
||||||
FROM activity_log a
|
|
||||||
INNER JOIN project_workspaces pw ON pw.id = a.project_id;
|
|
||||||
DROP TABLE activity_log;
|
|
||||||
ALTER TABLE activity_log_new RENAME TO activity_log;
|
|
||||||
PRAGMA foreign_keys=ON;
|
|
||||||
",
|
|
||||||
)
|
|
||||||
.expect("migrate activity_log FK to project_workspaces");
|
|
||||||
}
|
|
||||||
|
|
||||||
let pet_sql: Option<String> = conn
|
let pet_sql: Option<String> = conn
|
||||||
.query_row(
|
.query_row(
|
||||||
"SELECT sql FROM sqlite_master WHERE type='table' AND name='project_env_tools'",
|
"SELECT sql FROM sqlite_master WHERE type='table' AND name='project_env_tools'",
|
||||||
@ -574,21 +455,6 @@ pub(crate) fn migrate(conn: &rusqlite::Connection) {
|
|||||||
)
|
)
|
||||||
.expect("create envoy_dispositions table");
|
.expect("create envoy_dispositions table");
|
||||||
|
|
||||||
// 用户项目个人备忘(自由文本待办,Claude 不感知)
|
|
||||||
conn.execute_batch(
|
|
||||||
"
|
|
||||||
CREATE TABLE IF NOT EXISTS user_project_todos (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
project_id TEXT NOT NULL,
|
|
||||||
content TEXT NOT NULL,
|
|
||||||
done INTEGER NOT NULL DEFAULT 0,
|
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
||||||
created_at TEXT DEFAULT (datetime('now', 'localtime'))
|
|
||||||
);
|
|
||||||
",
|
|
||||||
)
|
|
||||||
.expect("create user_project_todos table");
|
|
||||||
|
|
||||||
// 蓝图 Buff:记录哪些项目施加了 buff
|
// 蓝图 Buff:记录哪些项目施加了 buff
|
||||||
conn.execute_batch(
|
conn.execute_batch(
|
||||||
"
|
"
|
||||||
@ -876,32 +742,6 @@ pub(crate) fn migrate(conn: &rusqlite::Connection) {
|
|||||||
.expect("create api_keys table");
|
.expect("create api_keys table");
|
||||||
let _ = conn.execute("ALTER TABLE api_keys ADD COLUMN credential_ref TEXT", []);
|
let _ = conn.execute("ALTER TABLE api_keys ADD COLUMN credential_ref TEXT", []);
|
||||||
|
|
||||||
// 增量迁移:宿主机部署应用
|
|
||||||
conn.execute_batch(
|
|
||||||
"
|
|
||||||
CREATE TABLE IF NOT EXISTS server_host_apps (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
server_id TEXT NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
deploy_type TEXT NOT NULL DEFAULT 'systemd',
|
|
||||||
service_name TEXT,
|
|
||||||
binary_path TEXT,
|
|
||||||
config_path TEXT,
|
|
||||||
log_path TEXT,
|
|
||||||
port TEXT,
|
|
||||||
start_cmd TEXT,
|
|
||||||
stop_cmd TEXT,
|
|
||||||
restart_cmd TEXT,
|
|
||||||
status_cmd TEXT,
|
|
||||||
notes TEXT,
|
|
||||||
sort_order INTEGER DEFAULT 0,
|
|
||||||
created_at TEXT DEFAULT (datetime('now')),
|
|
||||||
updated_at TEXT DEFAULT (datetime('now'))
|
|
||||||
);
|
|
||||||
",
|
|
||||||
)
|
|
||||||
.expect("create server_host_apps table");
|
|
||||||
|
|
||||||
// ── 源码库 ──────────────────────────────────────────────────────────
|
// ── 源码库 ──────────────────────────────────────────────────────────
|
||||||
conn.execute_batch(
|
conn.execute_batch(
|
||||||
"
|
"
|
||||||
|
|||||||
@ -120,7 +120,6 @@ pub(crate) fn silent_cmd(program: &str) -> std::process::Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
use commands::{
|
use commands::{
|
||||||
activity::*,
|
|
||||||
blueprint::*,
|
blueprint::*,
|
||||||
buff::{apply_blueprint_buff, get_buff_status, list_buffed_projects, remove_blueprint_buff},
|
buff::{apply_blueprint_buff, get_buff_status, list_buffed_projects, remove_blueprint_buff},
|
||||||
canvas::*,
|
canvas::*,
|
||||||
@ -129,7 +128,9 @@ use commands::{
|
|||||||
devtools::*,
|
devtools::*,
|
||||||
git_ops::*,
|
git_ops::*,
|
||||||
git_repos::*,
|
git_repos::*,
|
||||||
|
global_tools::*,
|
||||||
groups::*,
|
groups::*,
|
||||||
|
local_git::*,
|
||||||
mentor::{
|
mentor::{
|
||||||
add_project_note, get_group_mentor_report, get_mentor_context, get_project_notes,
|
add_project_note, get_group_mentor_report, get_mentor_context, get_project_notes,
|
||||||
get_startup_alerts, run_startup_health_scan,
|
get_startup_alerts, run_startup_health_scan,
|
||||||
@ -139,13 +140,11 @@ use commands::{
|
|||||||
test_wsl_proxy_ip,
|
test_wsl_proxy_ip,
|
||||||
},
|
},
|
||||||
project_scan::*,
|
project_scan::*,
|
||||||
project_tools::*,
|
|
||||||
projects::{
|
projects::{
|
||||||
batch_create_projects, create_project, delete_project, get_project, get_projects,
|
batch_create_projects, create_project, delete_project, get_project, get_projects,
|
||||||
scan_subfolders, update_project, update_project_tags,
|
scan_subfolders, update_project, update_project_tags,
|
||||||
},
|
},
|
||||||
proxy::*,
|
proxy::*,
|
||||||
publish::*,
|
|
||||||
settings::{
|
settings::{
|
||||||
check_mcp_health, detect_editors, get_agent_autostart_status, get_agent_runtime_status,
|
check_mcp_health, detect_editors, get_agent_autostart_status, get_agent_runtime_status,
|
||||||
get_settings, run_health_check, search_editor_path, set_agent_autostart, update_settings,
|
get_settings, run_health_check, search_editor_path, set_agent_autostart, update_settings,
|
||||||
@ -306,9 +305,6 @@ pub fn run() {
|
|||||||
delete_project,
|
delete_project,
|
||||||
scan_subfolders,
|
scan_subfolders,
|
||||||
batch_create_projects,
|
batch_create_projects,
|
||||||
// activity
|
|
||||||
get_activity,
|
|
||||||
add_activity,
|
|
||||||
// groups
|
// groups
|
||||||
get_groups,
|
get_groups,
|
||||||
create_group,
|
create_group,
|
||||||
@ -328,13 +324,8 @@ pub fn run() {
|
|||||||
get_agent_autostart_status,
|
get_agent_autostart_status,
|
||||||
set_agent_autostart,
|
set_agent_autostart,
|
||||||
run_health_check,
|
run_health_check,
|
||||||
// project tools
|
// global quick launch tools
|
||||||
get_project_tools,
|
|
||||||
add_project_tool,
|
|
||||||
update_project_tool,
|
|
||||||
remove_project_tool,
|
|
||||||
launch_tool,
|
launch_tool,
|
||||||
// global quick launch
|
|
||||||
get_global_tools,
|
get_global_tools,
|
||||||
add_global_tool,
|
add_global_tool,
|
||||||
update_global_tool,
|
update_global_tool,
|
||||||
@ -383,10 +374,6 @@ pub fn run() {
|
|||||||
update_space,
|
update_space,
|
||||||
delete_space,
|
delete_space,
|
||||||
set_current_space,
|
set_current_space,
|
||||||
get_deploy_commands,
|
|
||||||
add_deploy_command,
|
|
||||||
remove_deploy_command,
|
|
||||||
execute_deploy,
|
|
||||||
// git repos registry
|
// git repos registry
|
||||||
get_git_repos,
|
get_git_repos,
|
||||||
import_git_repos,
|
import_git_repos,
|
||||||
@ -462,11 +449,6 @@ pub fn run() {
|
|||||||
add_project_note,
|
add_project_note,
|
||||||
get_group_mentor_report,
|
get_group_mentor_report,
|
||||||
get_startup_alerts,
|
get_startup_alerts,
|
||||||
// user todos
|
|
||||||
commands::user_todos::get_user_todos,
|
|
||||||
commands::user_todos::add_user_todo,
|
|
||||||
commands::user_todos::toggle_user_todo,
|
|
||||||
commands::user_todos::delete_user_todo,
|
|
||||||
// servers
|
// servers
|
||||||
commands::servers::get_servers,
|
commands::servers::get_servers,
|
||||||
commands::servers::add_server,
|
commands::servers::add_server,
|
||||||
@ -512,11 +494,6 @@ pub fn run() {
|
|||||||
commands::dev_variables::save_project_test_account,
|
commands::dev_variables::save_project_test_account,
|
||||||
commands::dev_variables::reveal_project_dev_variable,
|
commands::dev_variables::reveal_project_dev_variable,
|
||||||
commands::dev_variables::revoke_project_dev_variable,
|
commands::dev_variables::revoke_project_dev_variable,
|
||||||
// host apps
|
|
||||||
commands::host_apps::get_host_apps,
|
|
||||||
commands::host_apps::add_host_app,
|
|
||||||
commands::host_apps::edit_host_app,
|
|
||||||
commands::host_apps::delete_host_app,
|
|
||||||
// server service configs
|
// server service configs
|
||||||
commands::service_config::get_service_configs,
|
commands::service_config::get_service_configs,
|
||||||
commands::service_config::add_service_config,
|
commands::service_config::add_service_config,
|
||||||
|
|||||||
@ -70,25 +70,6 @@ impl Project {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct ActivityLog {
|
|
||||||
pub id: i64,
|
|
||||||
pub project_id: String,
|
|
||||||
pub summary: String,
|
|
||||||
pub created_at: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActivityLog {
|
|
||||||
pub fn from_row(row: &Row<'_>) -> rusqlite::Result<Self> {
|
|
||||||
Ok(Self {
|
|
||||||
id: row.get("id")?,
|
|
||||||
project_id: row.get("project_id")?,
|
|
||||||
summary: row.get("summary")?,
|
|
||||||
created_at: row.get("created_at")?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct ProductGroup {
|
pub struct ProductGroup {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
@ -146,27 +127,6 @@ impl Space {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct DeployCommand {
|
|
||||||
pub id: String,
|
|
||||||
pub project_id: String,
|
|
||||||
pub name: String,
|
|
||||||
pub command: String,
|
|
||||||
pub run_in: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DeployCommand {
|
|
||||||
pub fn from_row(row: &Row<'_>) -> rusqlite::Result<Self> {
|
|
||||||
Ok(Self {
|
|
||||||
id: row.get("id")?,
|
|
||||||
project_id: row.get("project_id")?,
|
|
||||||
name: row.get("name")?,
|
|
||||||
command: row.get("command")?,
|
|
||||||
run_in: row.get("run_in")?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct CicdConfig {
|
pub struct CicdConfig {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { getGroups, getGroupMaps, getProjects, getSpaces, getTags, getAllProjectTagMaps, getGlobalTools, addGlobalTool, updateGlobalTool, removeGlobalTool, launchTool, type ProductGroup, type Project, type ProjectToolInput } from "../../lib/commands";
|
import { getGroups, getGroupMaps, getProjects, getSpaces, getTags, getAllProjectTagMaps, getGlobalTools, launchTool, type ProductGroup, type Project } from "../../lib/commands";
|
||||||
import { ProjectCard } from "./ProjectCard";
|
import { ProjectCard } from "./ProjectCard";
|
||||||
import { DeployModal } from "./DeployModal";
|
|
||||||
import { GroupResourceCanvas } from "./GroupResourceCanvas";
|
import { GroupResourceCanvas } from "./GroupResourceCanvas";
|
||||||
import { ManagePage } from "../manage/ManagePage";
|
import { ManagePage } from "../manage/ManagePage";
|
||||||
import { BatchAddModal } from "./BatchAddModal";
|
import { BatchAddModal } from "./BatchAddModal";
|
||||||
import { MountRepoModal } from "./MountRepoModal";
|
import { MountRepoModal } from "./MountRepoModal";
|
||||||
import { ProjectToolsModal } from "./ProjectToolsModal";
|
import { GlobalToolsModal, GLOBAL_TOOLS_QUERY_KEY } from "./GlobalToolsModal";
|
||||||
import { StatusBadge } from "../ui/Badge";
|
import { StatusBadge } from "../ui/Badge";
|
||||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||||
import { useUIStore } from "../../store/ui";
|
import { useUIStore } from "../../store/ui";
|
||||||
@ -197,7 +196,6 @@ interface GroupBoardSectionProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function GroupBoardSection({ group: g, members }: GroupBoardSectionProps) {
|
function GroupBoardSection({ group: g, members }: GroupBoardSectionProps) {
|
||||||
const [deployTarget, setDeployTarget] = useState<Project | null>(null);
|
|
||||||
const [groupView, setGroupView] = useState<GroupView>("default");
|
const [groupView, setGroupView] = useState<GroupView>("default");
|
||||||
|
|
||||||
const winMembers = members.filter(({ project: p }) => p.platform !== "wsl");
|
const winMembers = members.filter(({ project: p }) => p.platform !== "wsl");
|
||||||
@ -266,7 +264,6 @@ function GroupBoardSection({ group: g, members }: GroupBoardSectionProps) {
|
|||||||
key={project.id}
|
key={project.id}
|
||||||
project={project}
|
project={project}
|
||||||
role={role ?? undefined}
|
role={role ?? undefined}
|
||||||
onDeploy={setDeployTarget}
|
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
: <p className="text-xs text-gray-300 py-5 text-center border-b border-gray-50">暂无</p>
|
: <p className="text-xs text-gray-300 py-5 text-center border-b border-gray-50">暂无</p>
|
||||||
@ -283,7 +280,6 @@ function GroupBoardSection({ group: g, members }: GroupBoardSectionProps) {
|
|||||||
key={project.id}
|
key={project.id}
|
||||||
project={project}
|
project={project}
|
||||||
role={role ?? undefined}
|
role={role ?? undefined}
|
||||||
onDeploy={setDeployTarget}
|
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
: <p className="text-xs text-gray-300 py-5 text-center">暂无</p>
|
: <p className="text-xs text-gray-300 py-5 text-center">暂无</p>
|
||||||
@ -292,15 +288,6 @@ function GroupBoardSection({ group: g, members }: GroupBoardSectionProps) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{deployTarget && (
|
|
||||||
<DeployModal
|
|
||||||
projectId={deployTarget.id}
|
|
||||||
projectName={deployTarget.name}
|
|
||||||
projectPath={deployTarget.win_path ?? undefined}
|
|
||||||
projectWslPath={deployTarget.wsl_path ?? undefined}
|
|
||||||
onClose={() => setDeployTarget(null)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -308,11 +295,10 @@ function GroupBoardSection({ group: g, members }: GroupBoardSectionProps) {
|
|||||||
/* ── ProjectRow ────────────────────────────────────────────────────────────── */
|
/* ── ProjectRow ────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
function ProjectRow({
|
function ProjectRow({
|
||||||
project: p, role, onDeploy,
|
project: p, role,
|
||||||
}: {
|
}: {
|
||||||
project: Project;
|
project: Project;
|
||||||
role?: string;
|
role?: string;
|
||||||
onDeploy: (p: Project) => void;
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex divide-x divide-gray-100 border-b border-gray-50 last:border-b-0">
|
<div className="flex divide-x divide-gray-100 border-b border-gray-50 last:border-b-0">
|
||||||
@ -388,16 +374,6 @@ function ProjectRow({
|
|||||||
{!p.prod_url && !p.staging_url && !p.server_note && !p.recent_focus && (
|
{!p.prod_url && !p.staging_url && !p.server_note && !p.recent_focus && (
|
||||||
<p className="text-[11px] text-gray-300 italic">未配置部署信息</p>
|
<p className="text-[11px] text-gray-300 italic">未配置部署信息</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 部署配置按钮 */}
|
|
||||||
<div className="mt-auto pt-1">
|
|
||||||
<button
|
|
||||||
onClick={() => onDeploy(p)}
|
|
||||||
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -405,22 +381,13 @@ function ProjectRow({
|
|||||||
|
|
||||||
/* ── GlobalQuickLaunchBar ──────────────────────────────────────────────────── */
|
/* ── GlobalQuickLaunchBar ──────────────────────────────────────────────────── */
|
||||||
|
|
||||||
const GLOBAL_QUERY_KEY = ["globalTools"];
|
|
||||||
|
|
||||||
const globalToolCmds = {
|
|
||||||
getTools: () => getGlobalTools(),
|
|
||||||
addTool: (input: ProjectToolInput) => addGlobalTool(input),
|
|
||||||
updateTool: (id: string, input: ProjectToolInput) => updateGlobalTool(id, input),
|
|
||||||
removeTool: (id: string) => removeGlobalTool(id),
|
|
||||||
};
|
|
||||||
|
|
||||||
function GlobalQuickLaunchBar() {
|
function GlobalQuickLaunchBar() {
|
||||||
const showToast = useUIStore((s) => s.showToast);
|
const showToast = useUIStore((s) => s.showToast);
|
||||||
const [showMgr, setShowMgr] = useState(false);
|
const [showMgr, setShowMgr] = useState(false);
|
||||||
const [mgrDefaultAdd, setMgrDefaultAdd] = useState(false);
|
const [mgrDefaultAdd, setMgrDefaultAdd] = useState(false);
|
||||||
|
|
||||||
const { data: tools = [] } = useQuery({
|
const { data: tools = [] } = useQuery({
|
||||||
queryKey: GLOBAL_QUERY_KEY,
|
queryKey: GLOBAL_TOOLS_QUERY_KEY,
|
||||||
queryFn: () => getGlobalTools(),
|
queryFn: () => getGlobalTools(),
|
||||||
staleTime: 30000,
|
staleTime: 30000,
|
||||||
});
|
});
|
||||||
@ -477,12 +444,8 @@ function GlobalQuickLaunchBar() {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
{showMgr && (
|
{showMgr && (
|
||||||
<ProjectToolsModal
|
<GlobalToolsModal
|
||||||
projectId="__global__"
|
|
||||||
projectName="日常应用"
|
|
||||||
defaultOpenAdd={mgrDefaultAdd}
|
defaultOpenAdd={mgrDefaultAdd}
|
||||||
commands={globalToolCmds}
|
|
||||||
queryKey={GLOBAL_QUERY_KEY}
|
|
||||||
onClose={() => { setShowMgr(false); setMgrDefaultAdd(false); }}
|
onClose={() => { setShowMgr(false); setMgrDefaultAdd(false); }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -497,7 +460,6 @@ function StandaloneBoardSection({
|
|||||||
}: {
|
}: {
|
||||||
standalone: Project[];
|
standalone: Project[];
|
||||||
}) {
|
}) {
|
||||||
const [deployTarget, setDeployTarget] = useState<Project | null>(null);
|
|
||||||
const [selectedTagIds, setSelectedTagIds] = useState<Set<string>>(new Set());
|
const [selectedTagIds, setSelectedTagIds] = useState<Set<string>>(new Set());
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
@ -585,7 +547,7 @@ function StandaloneBoardSection({
|
|||||||
</div>
|
</div>
|
||||||
{winProjects.length > 0
|
{winProjects.length > 0
|
||||||
? winProjects.map((p) => (
|
? winProjects.map((p) => (
|
||||||
<ProjectRow key={p.id} project={p} onDeploy={setDeployTarget} />
|
<ProjectRow key={p.id} project={p} />
|
||||||
))
|
))
|
||||||
: <p className="text-xs text-gray-300 py-5 text-center border-b border-gray-50">暂无</p>
|
: <p className="text-xs text-gray-300 py-5 text-center border-b border-gray-50">暂无</p>
|
||||||
}
|
}
|
||||||
@ -597,21 +559,12 @@ function StandaloneBoardSection({
|
|||||||
</div>
|
</div>
|
||||||
{wslProjects.length > 0
|
{wslProjects.length > 0
|
||||||
? wslProjects.map((p) => (
|
? wslProjects.map((p) => (
|
||||||
<ProjectRow key={p.id} project={p} onDeploy={setDeployTarget} />
|
<ProjectRow key={p.id} project={p} />
|
||||||
))
|
))
|
||||||
: <p className="text-xs text-gray-300 py-5 text-center">暂无</p>
|
: <p className="text-xs text-gray-300 py-5 text-center">暂无</p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{deployTarget && (
|
|
||||||
<DeployModal
|
|
||||||
projectId={deployTarget.id}
|
|
||||||
projectName={deployTarget.name}
|
|
||||||
projectPath={deployTarget.win_path ?? undefined}
|
|
||||||
projectWslPath={deployTarget.wsl_path ?? undefined}
|
|
||||||
onClose={() => setDeployTarget(null)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,2 +0,0 @@
|
|||||||
// 已废弃,部署信息已迁移到 ServerManagerPanel + ProjectServerPanel
|
|
||||||
export {};
|
|
||||||
@ -1,175 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import {
|
|
||||||
getDeployCommands,
|
|
||||||
addDeployCommand,
|
|
||||||
removeDeployCommand,
|
|
||||||
executeDeploy,
|
|
||||||
type DeployCommand,
|
|
||||||
} from "../../lib/commands";
|
|
||||||
import { useUIStore } from "../../store/ui";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
projectId: string;
|
|
||||||
projectName: string;
|
|
||||||
projectPath?: string; // Windows 路径(win_path)
|
|
||||||
projectWslPath?: string; // WSL 路径(wsl_path),作为 WSL 命令的 fallback cwd
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DeployModal({ projectId, projectName, projectPath, projectWslPath, onClose }: Props) {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const showToast = useUIStore((s) => s.showToast);
|
|
||||||
|
|
||||||
const [newName, setNewName] = useState("");
|
|
||||||
const [newCmd, setNewCmd] = useState("");
|
|
||||||
const [newRunIn, setNewRunIn] = useState<"windows" | "wsl">("windows");
|
|
||||||
|
|
||||||
// 正在执行的命令 id
|
|
||||||
const [runningId, setRunningId] = useState<string | null>(null);
|
|
||||||
// 执行输出
|
|
||||||
const [output, setOutput] = useState<{ id: string; text: string; ok: boolean } | null>(null);
|
|
||||||
|
|
||||||
const { data: commands = [] } = useQuery({
|
|
||||||
queryKey: ["deployCommands", projectId],
|
|
||||||
queryFn: () => getDeployCommands(projectId),
|
|
||||||
});
|
|
||||||
|
|
||||||
const addMutation = useMutation({
|
|
||||||
mutationFn: () => addDeployCommand(projectId, newName, newCmd, newRunIn),
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["deployCommands", projectId] });
|
|
||||||
setNewName("");
|
|
||||||
setNewCmd("");
|
|
||||||
},
|
|
||||||
onError: (e) => showToast(`添加失败: ${e}`),
|
|
||||||
});
|
|
||||||
|
|
||||||
const removeMutation = useMutation({
|
|
||||||
mutationFn: (id: string) => removeDeployCommand(id),
|
|
||||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["deployCommands", projectId] }),
|
|
||||||
});
|
|
||||||
|
|
||||||
async function handleRun(cmd: DeployCommand) {
|
|
||||||
setRunningId(cmd.id);
|
|
||||||
setOutput(null);
|
|
||||||
try {
|
|
||||||
// WSL 命令优先用 projectPath(Windows 路径,WSL 自动映射 /mnt/...)
|
|
||||||
// 若无 win_path,则将 wsl_path 通过 `cd && ` 前缀传入命令
|
|
||||||
let effectiveCommand = cmd.command;
|
|
||||||
let cwd: string | undefined = projectPath;
|
|
||||||
if (cmd.run_in === "wsl" && !projectPath && projectWslPath) {
|
|
||||||
effectiveCommand = `cd ${JSON.stringify(projectWslPath)} && ${cmd.command}`;
|
|
||||||
cwd = undefined;
|
|
||||||
}
|
|
||||||
const result = await executeDeploy(effectiveCommand, cmd.run_in, cwd);
|
|
||||||
setOutput({ id: cmd.id, text: result, ok: true });
|
|
||||||
} catch (e) {
|
|
||||||
setOutput({ id: cmd.id, text: String(e), ok: false });
|
|
||||||
} finally {
|
|
||||||
setRunningId(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const canAdd = newName.trim() && newCmd.trim();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
|
|
||||||
<div className="bg-zinc-900 rounded-xl shadow-2xl w-[580px] max-h-[80vh] flex flex-col border border-zinc-700">
|
|
||||||
{/* 标题 */}
|
|
||||||
<div className="px-5 py-4 border-b border-zinc-700 flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-base font-semibold text-white">部署命令</h2>
|
|
||||||
<p className="text-xs text-zinc-400 mt-0.5">{projectName}</p>
|
|
||||||
</div>
|
|
||||||
<button onClick={onClose} className="text-zinc-400 hover:text-white text-xl leading-none">×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto min-h-0 p-4 space-y-3">
|
|
||||||
{/* 已有命令列表 */}
|
|
||||||
{commands.length === 0 && (
|
|
||||||
<p className="text-zinc-500 text-sm text-center py-4">暂无部署命令</p>
|
|
||||||
)}
|
|
||||||
{commands.map((cmd) => (
|
|
||||||
<div key={cmd.id} className="bg-zinc-800 rounded-lg p-3 space-y-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-white text-sm font-medium flex-1">{cmd.name}</span>
|
|
||||||
<span className={`text-xs px-1.5 py-0.5 rounded font-mono ${
|
|
||||||
cmd.run_in === "wsl"
|
|
||||||
? "bg-green-900 text-green-300"
|
|
||||||
: "bg-blue-900 text-blue-300"
|
|
||||||
}`}>
|
|
||||||
{cmd.run_in.toUpperCase()}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
onClick={() => handleRun(cmd)}
|
|
||||||
disabled={runningId === cmd.id}
|
|
||||||
className="px-3 py-1 bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 text-white text-xs rounded transition-colors"
|
|
||||||
>
|
|
||||||
{runningId === cmd.id ? "执行中…" : "▶ 执行"}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => removeMutation.mutate(cmd.id)}
|
|
||||||
className="text-zinc-500 hover:text-red-400 text-sm transition-colors"
|
|
||||||
title="删除"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<code className="block text-xs text-zinc-400 font-mono bg-zinc-900 rounded px-2 py-1.5 break-all">
|
|
||||||
{cmd.command}
|
|
||||||
</code>
|
|
||||||
{/* 执行输出 */}
|
|
||||||
{output?.id === cmd.id && (
|
|
||||||
<div className={`rounded px-2 py-1.5 text-xs font-mono whitespace-pre-wrap break-all max-h-32 overflow-y-auto ${
|
|
||||||
output.ok ? "bg-zinc-950 text-green-400" : "bg-red-950 text-red-300"
|
|
||||||
}`}>
|
|
||||||
{output.text}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 添加新命令 */}
|
|
||||||
<div className="px-4 py-4 border-t border-zinc-700 space-y-2">
|
|
||||||
<p className="text-xs text-zinc-400 font-medium">添加命令</p>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="名称(如:启动服务)"
|
|
||||||
value={newName}
|
|
||||||
onChange={(e) => setNewName(e.target.value)}
|
|
||||||
className="flex-1 bg-zinc-800 text-white text-sm rounded-lg px-3 py-2 outline-none focus:ring-1 focus:ring-blue-500 placeholder:text-zinc-600"
|
|
||||||
/>
|
|
||||||
<select
|
|
||||||
value={newRunIn}
|
|
||||||
onChange={(e) => setNewRunIn(e.target.value as "windows" | "wsl")}
|
|
||||||
className="bg-zinc-800 text-white text-sm rounded-lg px-2 py-2 outline-none focus:ring-1 focus:ring-blue-500"
|
|
||||||
>
|
|
||||||
<option value="windows">Windows</option>
|
|
||||||
<option value="wsl">WSL</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="命令(如:docker compose up -d)"
|
|
||||||
value={newCmd}
|
|
||||||
onChange={(e) => setNewCmd(e.target.value)}
|
|
||||||
onKeyDown={(e) => e.key === "Enter" && canAdd && addMutation.mutate()}
|
|
||||||
className="flex-1 bg-zinc-800 text-white text-sm rounded-lg px-3 py-2 outline-none focus:ring-1 focus:ring-blue-500 placeholder:text-zinc-600 font-mono"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={() => addMutation.mutate()}
|
|
||||||
disabled={!canAdd || addMutation.isPending}
|
|
||||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white text-sm rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
添加
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -2,56 +2,38 @@ import { useState } from "react";
|
|||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { open } from "@tauri-apps/plugin-dialog";
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
import {
|
import {
|
||||||
getProjectTools,
|
getGlobalTools,
|
||||||
addProjectTool,
|
addGlobalTool,
|
||||||
updateProjectTool,
|
updateGlobalTool,
|
||||||
removeProjectTool,
|
removeGlobalTool,
|
||||||
type ProjectTool,
|
type GlobalTool,
|
||||||
type ProjectToolInput,
|
type GlobalToolInput,
|
||||||
} from "../../lib/commands";
|
} from "../../lib/commands";
|
||||||
|
|
||||||
export interface ToolCommands {
|
|
||||||
getTools: () => Promise<ProjectTool[]>;
|
|
||||||
addTool: (input: ProjectToolInput) => Promise<string>;
|
|
||||||
updateTool: (id: string, input: ProjectToolInput) => Promise<void>;
|
|
||||||
removeTool: (id: string) => Promise<void>;
|
|
||||||
}
|
|
||||||
import { useUIStore } from "../../store/ui";
|
import { useUIStore } from "../../store/ui";
|
||||||
import { Dialog } from "../ui/Dialog";
|
import { Dialog } from "../ui/Dialog";
|
||||||
|
|
||||||
|
export const GLOBAL_TOOLS_QUERY_KEY = ["globalTools"];
|
||||||
|
|
||||||
const COMMON_ICONS = ["🔧", "🎬", "🎨", "📷", "🗄️", "📮", "🌐", "📊", "🎵", "📐", "🔬", "⚙️", "📁", "🖨️", "🎮"];
|
const COMMON_ICONS = ["🔧", "🎬", "🎨", "📷", "🗄️", "📮", "🌐", "📊", "🎵", "📐", "🔬", "⚙️", "📁", "🖨️", "🎮"];
|
||||||
|
|
||||||
const EMPTY: ProjectToolInput = { name: "", icon: "🔧", exe_path: "", args: "" };
|
const EMPTY: GlobalToolInput = { name: "", icon: "🔧", exe_path: "", args: "" };
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
projectId: string;
|
|
||||||
projectName: string;
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
defaultOpenAdd?: boolean;
|
defaultOpenAdd?: boolean;
|
||||||
commands?: ToolCommands;
|
|
||||||
queryKey?: unknown[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProjectToolsModal({ projectId, projectName, onClose, defaultOpenAdd, commands, queryKey }: Props) {
|
export function GlobalToolsModal({ onClose, defaultOpenAdd }: Props) {
|
||||||
const showToast = useUIStore((s) => s.showToast);
|
const showToast = useUIStore((s) => s.showToast);
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
|
|
||||||
const cmds: ToolCommands = commands ?? {
|
|
||||||
getTools: () => getProjectTools(projectId),
|
|
||||||
addTool: (input) => addProjectTool(projectId, input),
|
|
||||||
updateTool: (id, input) => updateProjectTool(id, input),
|
|
||||||
removeTool: (id) => removeProjectTool(id),
|
|
||||||
};
|
|
||||||
|
|
||||||
const resolvedKey = queryKey ?? ["projectTools", projectId];
|
|
||||||
|
|
||||||
const { data: tools = [], refetch } = useQuery({
|
const { data: tools = [], refetch } = useQuery({
|
||||||
queryKey: resolvedKey,
|
queryKey: GLOBAL_TOOLS_QUERY_KEY,
|
||||||
queryFn: () => cmds.getTools(),
|
queryFn: () => getGlobalTools(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [editing, setEditing] = useState<ProjectTool | null>(null); // null = 新增模式
|
const [editing, setEditing] = useState<GlobalTool | null>(null); // null = 新增模式
|
||||||
const [form, setForm] = useState<ProjectToolInput>(EMPTY);
|
const [form, setForm] = useState<GlobalToolInput>(EMPTY);
|
||||||
const [showForm, setShowForm] = useState(defaultOpenAdd ?? false);
|
const [showForm, setShowForm] = useState(defaultOpenAdd ?? false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
@ -61,13 +43,13 @@ export function ProjectToolsModal({ projectId, projectName, onClose, defaultOpen
|
|||||||
setShowForm(true);
|
setShowForm(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openEdit = (tool: ProjectTool) => {
|
const openEdit = (tool: GlobalTool) => {
|
||||||
setEditing(tool);
|
setEditing(tool);
|
||||||
setForm({ name: tool.name, icon: tool.icon, exe_path: tool.exe_path, args: tool.args ?? "" });
|
setForm({ name: tool.name, icon: tool.icon, exe_path: tool.exe_path, args: tool.args ?? "" });
|
||||||
setShowForm(true);
|
setShowForm(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const set = (k: keyof ProjectToolInput, v: string) =>
|
const set = (k: keyof GlobalToolInput, v: string) =>
|
||||||
setForm((f) => ({ ...f, [k]: v }));
|
setForm((f) => ({ ...f, [k]: v }));
|
||||||
|
|
||||||
const pickExe = async () => {
|
const pickExe = async () => {
|
||||||
@ -93,18 +75,18 @@ export function ProjectToolsModal({ projectId, projectName, onClose, defaultOpen
|
|||||||
if (!form.exe_path.trim()) { showToast("❌ 程序路径不能为空"); return; }
|
if (!form.exe_path.trim()) { showToast("❌ 程序路径不能为空"); return; }
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const input: ProjectToolInput = {
|
const input: GlobalToolInput = {
|
||||||
...form,
|
...form,
|
||||||
args: form.args?.trim() || undefined,
|
args: form.args?.trim() || undefined,
|
||||||
sort_order: editing?.sort_order ?? tools.length,
|
sort_order: editing?.sort_order ?? tools.length,
|
||||||
};
|
};
|
||||||
if (editing) {
|
if (editing) {
|
||||||
await cmds.updateTool(editing.id, input);
|
await updateGlobalTool(editing.id, input);
|
||||||
} else {
|
} else {
|
||||||
await cmds.addTool(input);
|
await addGlobalTool(input);
|
||||||
}
|
}
|
||||||
await refetch();
|
await refetch();
|
||||||
qc.invalidateQueries({ queryKey: resolvedKey });
|
qc.invalidateQueries({ queryKey: GLOBAL_TOOLS_QUERY_KEY });
|
||||||
setShowForm(false);
|
setShowForm(false);
|
||||||
showToast(editing ? "已更新 ✓" : "已添加 ✓");
|
showToast(editing ? "已更新 ✓" : "已添加 ✓");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -116,9 +98,9 @@ export function ProjectToolsModal({ projectId, projectName, onClose, defaultOpen
|
|||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const handleDelete = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
await cmds.removeTool(id);
|
await removeGlobalTool(id);
|
||||||
await refetch();
|
await refetch();
|
||||||
qc.invalidateQueries({ queryKey: resolvedKey });
|
qc.invalidateQueries({ queryKey: GLOBAL_TOOLS_QUERY_KEY });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast(`❌ ${e}`);
|
showToast(`❌ ${e}`);
|
||||||
}
|
}
|
||||||
@ -127,7 +109,7 @@ export function ProjectToolsModal({ projectId, projectName, onClose, defaultOpen
|
|||||||
const inputCls = "w-full border border-gray-200 rounded-lg px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-200";
|
const inputCls = "w-full border border-gray-200 rounded-lg px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-200";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog title={`工具管理 · ${projectName}`} onClose={onClose} size="lg">
|
<Dialog title="工具管理 · 日常应用" onClose={onClose} size="lg">
|
||||||
<div className="px-6 py-4">
|
<div className="px-6 py-4">
|
||||||
|
|
||||||
{/* 工具列表 */}
|
{/* 工具列表 */}
|
||||||
@ -1,330 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import {
|
|
||||||
getHostApps, addHostApp, editHostApp, deleteHostApp,
|
|
||||||
type HostApp, type SaveHostAppInput,
|
|
||||||
} from "../../lib/commands";
|
|
||||||
|
|
||||||
const DEPLOY_TYPES = [
|
|
||||||
{ value: "systemd", label: "systemd 服务", color: "bg-green-100 text-green-700 border-green-200" },
|
|
||||||
{ value: "pm2", label: "PM2", color: "bg-blue-100 text-blue-700 border-blue-200" },
|
|
||||||
{ value: "supervisor", label: "Supervisor", color: "bg-purple-100 text-purple-700 border-purple-200" },
|
|
||||||
{ value: "screen", label: "Screen/Tmux", color: "bg-cyan-100 text-cyan-700 border-cyan-200" },
|
|
||||||
{ value: "manual", label: "手动启动", color: "bg-gray-100 text-gray-600 border-gray-200" },
|
|
||||||
];
|
|
||||||
|
|
||||||
function getTypeStyle(t: string) {
|
|
||||||
return DEPLOY_TYPES.find((d) => d.value === t)?.color ?? DEPLOY_TYPES[DEPLOY_TYPES.length - 1].color;
|
|
||||||
}
|
|
||||||
function getTypeLabel(t: string) {
|
|
||||||
return DEPLOY_TYPES.find((d) => d.value === t)?.label ?? t;
|
|
||||||
}
|
|
||||||
|
|
||||||
function CopyBtn({ value }: { value: string }) {
|
|
||||||
const [copied, setCopied] = useState(false);
|
|
||||||
const handleCopy = async () => {
|
|
||||||
await navigator.clipboard.writeText(value);
|
|
||||||
setCopied(true);
|
|
||||||
setTimeout(() => setCopied(false), 1500);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<button onClick={handleCopy}
|
|
||||||
className="shrink-0 px-1.5 py-0.5 rounded text-[11px] border border-gray-200 text-gray-400 hover:text-blue-500 hover:border-blue-300 hover:bg-blue-50 transition-colors">
|
|
||||||
{copied ? "已复制" : "复制"}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CmdRow({ label, value }: { label: string; value: string }) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-gray-900 rounded-md group">
|
|
||||||
<span className="text-[10px] text-gray-500 w-10 shrink-0 text-right">{label}</span>
|
|
||||||
<span className="text-[11px] text-green-400 font-mono flex-1 truncate">$ {value}</span>
|
|
||||||
<span className="opacity-0 group-hover:opacity-100 transition-opacity">
|
|
||||||
<CopyBtn value={value} />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 模态框表单 ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function HostAppModal({ initial, onSave, onClose, saving }: {
|
|
||||||
initial?: HostApp;
|
|
||||||
onSave: (input: SaveHostAppInput) => void;
|
|
||||||
onClose: () => void;
|
|
||||||
saving: boolean;
|
|
||||||
}) {
|
|
||||||
const [name, setName] = useState(initial?.name ?? "");
|
|
||||||
const [deployType, setDeployType] = useState(initial?.deploy_type ?? "systemd");
|
|
||||||
const [serviceName, setServiceName] = useState(initial?.service_name ?? "");
|
|
||||||
const [binaryPath, setBinaryPath] = useState(initial?.binary_path ?? "");
|
|
||||||
const [configPath, setConfigPath] = useState(initial?.config_path ?? "");
|
|
||||||
const [logPath, setLogPath] = useState(initial?.log_path ?? "");
|
|
||||||
const [port, setPort] = useState(initial?.port ?? "");
|
|
||||||
const [startCmd, setStartCmd] = useState(initial?.start_cmd ?? "");
|
|
||||||
const [stopCmd, setStopCmd] = useState(initial?.stop_cmd ?? "");
|
|
||||||
const [restartCmd, setRestartCmd] = useState(initial?.restart_cmd ?? "");
|
|
||||||
const [statusCmd, setStatusCmd] = useState(initial?.status_cmd ?? "");
|
|
||||||
const [notes, setNotes] = useState(initial?.notes ?? "");
|
|
||||||
|
|
||||||
// systemd 时自动填充命令
|
|
||||||
const autoFillSystemd = (svc: string) => {
|
|
||||||
setServiceName(svc);
|
|
||||||
if (deployType === "systemd" && svc.trim()) {
|
|
||||||
const s = svc.trim();
|
|
||||||
if (!startCmd) setStartCmd(`systemctl start ${s}`);
|
|
||||||
if (!stopCmd) setStopCmd(`systemctl stop ${s}`);
|
|
||||||
if (!restartCmd) setRestartCmd(`systemctl restart ${s}`);
|
|
||||||
if (!statusCmd) setStatusCmd(`systemctl status ${s}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
if (!name.trim()) return;
|
|
||||||
onSave({
|
|
||||||
name: name.trim(),
|
|
||||||
deploy_type: deployType,
|
|
||||||
service_name: serviceName.trim() || undefined,
|
|
||||||
binary_path: binaryPath.trim() || undefined,
|
|
||||||
config_path: configPath.trim() || undefined,
|
|
||||||
log_path: logPath.trim() || undefined,
|
|
||||||
port: port.trim() || undefined,
|
|
||||||
start_cmd: startCmd.trim() || undefined,
|
|
||||||
stop_cmd: stopCmd.trim() || undefined,
|
|
||||||
restart_cmd: restartCmd.trim() || undefined,
|
|
||||||
status_cmd: statusCmd.trim() || undefined,
|
|
||||||
notes: notes.trim() || undefined,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const isEdit = !!initial;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black/30 z-50 flex items-center justify-center" onClick={onClose}>
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 shadow-xl w-[560px] max-h-[85vh] flex flex-col" onClick={(e) => e.stopPropagation()}>
|
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-100">
|
|
||||||
<h3 className="text-sm font-bold text-gray-800">{isEdit ? "编辑宿主机应用" : "添加宿主机应用"}</h3>
|
|
||||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 text-lg leading-none">x</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto px-6 py-4 space-y-3">
|
|
||||||
<div className="grid grid-cols-[72px_1fr] gap-x-3 gap-y-2.5 items-center">
|
|
||||||
<label className="text-[11px] text-gray-400 text-right">名称 *</label>
|
|
||||||
<input value={name} onChange={(e) => setName(e.target.value)} placeholder="例: Nginx"
|
|
||||||
className="text-xs px-2.5 py-2 rounded border border-gray-200 bg-gray-50 focus:outline-none focus:border-blue-300" />
|
|
||||||
<label className="text-[11px] text-gray-400 text-right">部署方式</label>
|
|
||||||
<select value={deployType} onChange={(e) => setDeployType(e.target.value)}
|
|
||||||
className="text-xs px-2.5 py-2 rounded border border-gray-200 bg-gray-50 focus:outline-none focus:border-blue-300 w-40">
|
|
||||||
{DEPLOY_TYPES.map((d) => <option key={d.value} value={d.value}>{d.label}</option>)}
|
|
||||||
</select>
|
|
||||||
<label className="text-[11px] text-gray-400 text-right">服务名</label>
|
|
||||||
<input value={serviceName} onChange={(e) => autoFillSystemd(e.target.value)}
|
|
||||||
placeholder={deployType === "systemd" ? "例: nginx" : "例: app-name"}
|
|
||||||
className="text-xs px-2.5 py-2 rounded border border-gray-200 bg-gray-50 focus:outline-none focus:border-blue-300 font-mono" />
|
|
||||||
<label className="text-[11px] text-gray-400 text-right">端口</label>
|
|
||||||
<input value={port} onChange={(e) => setPort(e.target.value)} placeholder="例: 8080"
|
|
||||||
className="text-xs px-2.5 py-2 rounded border border-gray-200 bg-gray-50 focus:outline-none focus:border-blue-300 font-mono w-28" />
|
|
||||||
<label className="text-[11px] text-gray-400 text-right">程序路径</label>
|
|
||||||
<input value={binaryPath} onChange={(e) => setBinaryPath(e.target.value)} placeholder="/usr/bin/..."
|
|
||||||
className="text-xs px-2.5 py-2 rounded border border-gray-200 bg-gray-50 focus:outline-none focus:border-blue-300 font-mono" />
|
|
||||||
<label className="text-[11px] text-gray-400 text-right">配置路径</label>
|
|
||||||
<input value={configPath} onChange={(e) => setConfigPath(e.target.value)} placeholder="/etc/..."
|
|
||||||
className="text-xs px-2.5 py-2 rounded border border-gray-200 bg-gray-50 focus:outline-none focus:border-blue-300 font-mono" />
|
|
||||||
<label className="text-[11px] text-gray-400 text-right">日志路径</label>
|
|
||||||
<input value={logPath} onChange={(e) => setLogPath(e.target.value)} placeholder="/var/log/..."
|
|
||||||
className="text-xs px-2.5 py-2 rounded border border-gray-200 bg-gray-50 focus:outline-none focus:border-blue-300 font-mono" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 管理命令 */}
|
|
||||||
<div>
|
|
||||||
<span className="text-[11px] text-gray-500 font-medium">管理命令</span>
|
|
||||||
<div className="grid grid-cols-[72px_1fr] gap-x-3 gap-y-2 items-center mt-1.5">
|
|
||||||
<label className="text-[11px] text-gray-400 text-right">启动</label>
|
|
||||||
<input value={startCmd} onChange={(e) => setStartCmd(e.target.value)} placeholder="systemctl start ..."
|
|
||||||
className="text-xs px-2.5 py-1.5 rounded border border-gray-200 bg-gray-50 focus:outline-none focus:border-blue-300 font-mono" />
|
|
||||||
<label className="text-[11px] text-gray-400 text-right">停止</label>
|
|
||||||
<input value={stopCmd} onChange={(e) => setStopCmd(e.target.value)} placeholder="systemctl stop ..."
|
|
||||||
className="text-xs px-2.5 py-1.5 rounded border border-gray-200 bg-gray-50 focus:outline-none focus:border-blue-300 font-mono" />
|
|
||||||
<label className="text-[11px] text-gray-400 text-right">重启</label>
|
|
||||||
<input value={restartCmd} onChange={(e) => setRestartCmd(e.target.value)} placeholder="systemctl restart ..."
|
|
||||||
className="text-xs px-2.5 py-1.5 rounded border border-gray-200 bg-gray-50 focus:outline-none focus:border-blue-300 font-mono" />
|
|
||||||
<label className="text-[11px] text-gray-400 text-right">状态</label>
|
|
||||||
<input value={statusCmd} onChange={(e) => setStatusCmd(e.target.value)} placeholder="systemctl status ..."
|
|
||||||
className="text-xs px-2.5 py-1.5 rounded border border-gray-200 bg-gray-50 focus:outline-none focus:border-blue-300 font-mono" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-[72px_1fr] gap-x-3 items-center">
|
|
||||||
<label className="text-[11px] text-gray-400 text-right">备注</label>
|
|
||||||
<input value={notes} onChange={(e) => setNotes(e.target.value)} placeholder="用途说明"
|
|
||||||
className="text-xs px-2.5 py-2 rounded border border-gray-200 bg-gray-50 focus:outline-none focus:border-blue-300" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-2 px-6 py-3 border-t border-gray-100 bg-gray-50/50">
|
|
||||||
<button onClick={onClose}
|
|
||||||
className="px-3 py-1.5 rounded-md text-xs border border-gray-200 text-gray-500 hover:bg-gray-50 transition-colors">
|
|
||||||
取消
|
|
||||||
</button>
|
|
||||||
<button onClick={handleSubmit} disabled={saving || !name.trim()}
|
|
||||||
className="px-4 py-1.5 rounded-md text-xs font-medium bg-blue-600 text-white hover:bg-blue-700 transition-colors disabled:opacity-40">
|
|
||||||
{saving ? "保存中…" : "保存"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 应用卡片 ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function HostAppCard({ app, onEdit, onDelete }: {
|
|
||||||
app: HostApp;
|
|
||||||
onEdit: () => void;
|
|
||||||
onDelete: () => void;
|
|
||||||
}) {
|
|
||||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
||||||
|
|
||||||
const infos = [
|
|
||||||
app.service_name && { label: "服务名", value: app.service_name },
|
|
||||||
app.port && { label: "端口", value: app.port },
|
|
||||||
app.binary_path && { label: "程序", value: app.binary_path },
|
|
||||||
app.config_path && { label: "配置", value: app.config_path },
|
|
||||||
app.log_path && { label: "日志", value: app.log_path },
|
|
||||||
].filter(Boolean) as { label: string; value: string }[];
|
|
||||||
|
|
||||||
const cmds = [
|
|
||||||
app.start_cmd && { label: "启动", value: app.start_cmd },
|
|
||||||
app.stop_cmd && { label: "停止", value: app.stop_cmd },
|
|
||||||
app.restart_cmd && { label: "重启", value: app.restart_cmd },
|
|
||||||
app.status_cmd && { label: "状态", value: app.status_cmd },
|
|
||||||
].filter(Boolean) as { label: string; value: string }[];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white border border-gray-200 rounded-lg overflow-hidden">
|
|
||||||
{/* 标题 */}
|
|
||||||
<div className="flex items-center gap-2 px-3 py-2 bg-gray-50/60 border-b border-gray-100">
|
|
||||||
<span className={`text-[10px] px-1.5 py-0.5 rounded border font-medium ${getTypeStyle(app.deploy_type)}`}>
|
|
||||||
{getTypeLabel(app.deploy_type)}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs font-semibold text-gray-800 flex-1 min-w-0 truncate">{app.name}</span>
|
|
||||||
{app.port && <span className="text-[11px] font-mono text-gray-400">:{app.port}</span>}
|
|
||||||
{app.notes && <span className="text-[11px] text-gray-400">{app.notes}</span>}
|
|
||||||
<button onClick={onEdit}
|
|
||||||
className="text-[11px] text-gray-400 hover:text-blue-500 transition-colors">编辑</button>
|
|
||||||
{!confirmDelete ? (
|
|
||||||
<button onClick={() => setConfirmDelete(true)}
|
|
||||||
className="text-[11px] text-gray-400 hover:text-red-500 transition-colors">删除</button>
|
|
||||||
) : (
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<button onClick={onDelete}
|
|
||||||
className="text-[11px] text-red-500 hover:text-red-700 font-medium transition-colors">确认</button>
|
|
||||||
<button onClick={() => setConfirmDelete(false)}
|
|
||||||
className="text-[11px] text-gray-400 transition-colors">取消</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="px-3 py-2 space-y-2">
|
|
||||||
{/* 基础信息 */}
|
|
||||||
{infos.length > 0 && (
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
{infos.map((info, idx) => (
|
|
||||||
<div key={idx} className="flex items-center gap-2 py-0.5 group">
|
|
||||||
<span className="text-[11px] text-gray-400 w-10 shrink-0 text-right">{info.label}</span>
|
|
||||||
<span className="text-xs font-mono text-gray-700 flex-1 min-w-0 truncate">{info.value}</span>
|
|
||||||
<span className="opacity-0 group-hover:opacity-100 transition-opacity">
|
|
||||||
<CopyBtn value={info.value} />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 管理命令 */}
|
|
||||||
{cmds.length > 0 && (
|
|
||||||
<div className="space-y-1">
|
|
||||||
{cmds.map((cmd, idx) => (
|
|
||||||
<CmdRow key={idx} label={cmd.label} value={cmd.value} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 主面板 ────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export function HostAppsPanel({ serverId }: { serverId: string }) {
|
|
||||||
const qc = useQueryClient();
|
|
||||||
const [showModal, setShowModal] = useState(false);
|
|
||||||
const [editingApp, setEditingApp] = useState<HostApp | null>(null);
|
|
||||||
|
|
||||||
const { data: apps = [] } = useQuery({
|
|
||||||
queryKey: ["host-apps", serverId],
|
|
||||||
queryFn: () => getHostApps(serverId),
|
|
||||||
});
|
|
||||||
|
|
||||||
const addMut = useMutation({
|
|
||||||
mutationFn: (input: SaveHostAppInput) => addHostApp(serverId, input),
|
|
||||||
onSuccess: () => { qc.invalidateQueries({ queryKey: ["host-apps", serverId] }); setShowModal(false); },
|
|
||||||
});
|
|
||||||
|
|
||||||
const editMut = useMutation({
|
|
||||||
mutationFn: ({ id, input }: { id: string; input: SaveHostAppInput }) => editHostApp(id, input),
|
|
||||||
onSuccess: () => { qc.invalidateQueries({ queryKey: ["host-apps", serverId] }); setEditingApp(null); },
|
|
||||||
});
|
|
||||||
|
|
||||||
const deleteMut = useMutation({
|
|
||||||
mutationFn: (id: string) => deleteHostApp(id),
|
|
||||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["host-apps", serverId] }),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<h4 className="text-[11px] font-semibold text-gray-400 uppercase tracking-wider">宿主机应用</h4>
|
|
||||||
<button onClick={() => setShowModal(true)}
|
|
||||||
className="text-[11px] px-2 py-0.5 rounded border border-dashed border-gray-300 text-gray-400 hover:border-blue-300 hover:text-blue-500 transition-colors">
|
|
||||||
+ 添加
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
{apps.map((app) => (
|
|
||||||
<HostAppCard
|
|
||||||
key={app.id}
|
|
||||||
app={app}
|
|
||||||
onEdit={() => setEditingApp(app)}
|
|
||||||
onDelete={() => deleteMut.mutate(app.id)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{apps.length === 0 && (
|
|
||||||
<p className="text-[11px] text-gray-300 text-center py-3">
|
|
||||||
暂无宿主机应用,点击「+ 添加」记录 systemd 服务、PM2 进程等
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showModal && (
|
|
||||||
<HostAppModal
|
|
||||||
onSave={(input) => addMut.mutate(input)}
|
|
||||||
onClose={() => setShowModal(false)}
|
|
||||||
saving={addMut.isPending}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{editingApp && (
|
|
||||||
<HostAppModal
|
|
||||||
initial={editingApp}
|
|
||||||
onSave={(input) => editMut.mutate({ id: editingApp.id, input })}
|
|
||||||
onClose={() => setEditingApp(null)}
|
|
||||||
saving={editMut.isPending}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,15 +1,13 @@
|
|||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { getActivity, getSettings, getProjectTools, openEditor, openTerminal, launchTool, gitStatus, gitFetch, gitPull, gitPush, gitUpstreamBehind, gitCreateBranch, gitListBranches, gitCheckoutBranch, getBlueprintStatus, getBuffStatus, getDocFreshnessCache, type Project, type BlueprintStatus, type BuffStatus, type CachedFreshness } from "../../lib/commands";
|
import { getSettings, openEditor, openTerminal, gitStatus, gitFetch, gitPull, gitPush, gitUpstreamBehind, gitCreateBranch, gitListBranches, gitCheckoutBranch, getBlueprintStatus, getBuffStatus, getDocFreshnessCache, type Project, type BlueprintStatus, type BuffStatus, type CachedFreshness } from "../../lib/commands";
|
||||||
import { useUIStore } from "../../store/ui";
|
import { useUIStore } from "../../store/ui";
|
||||||
import { PriorityBadge, StatusBadge } from "../ui/Badge";
|
import { PriorityBadge, StatusBadge } from "../ui/Badge";
|
||||||
import { ProjectToolsModal } from "./ProjectToolsModal";
|
|
||||||
import { ProjectEnvModal } from "./ProjectEnvModal";
|
import { ProjectEnvModal } from "./ProjectEnvModal";
|
||||||
import { CicdModal } from "./CicdModal";
|
import { CicdModal } from "./CicdModal";
|
||||||
import { BlueprintModal } from "./BlueprintModal";
|
import { BlueprintModal } from "./BlueprintModal";
|
||||||
import { BlueprintPromptModal } from "./BlueprintPromptModal";
|
import { BlueprintPromptModal } from "./BlueprintPromptModal";
|
||||||
import { MentorPanel } from "../mentor/MentorPanel";
|
import { MentorPanel } from "../mentor/MentorPanel";
|
||||||
import { UserTodoPanel } from "./UserTodoPanel";
|
|
||||||
import { ProjectServerPanel } from "./ProjectServerPanel";
|
import { ProjectServerPanel } from "./ProjectServerPanel";
|
||||||
import { ClaudeConfigModal } from "./ClaudeConfigModal";
|
import { ClaudeConfigModal } from "./ClaudeConfigModal";
|
||||||
import { ProjectDevVariablesPanel } from "./ProjectDevVariablesPanel";
|
import { ProjectDevVariablesPanel } from "./ProjectDevVariablesPanel";
|
||||||
@ -58,9 +56,6 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ProjectCard({ project: p }: Props) {
|
export function ProjectCard({ project: p }: Props) {
|
||||||
const [expanded, setExpanded] = useState(false);
|
|
||||||
const [showToolsMgr, setShowToolsMgr] = useState(false);
|
|
||||||
const [toolsMgrDefaultAdd, setToolsMgrDefaultAdd] = useState(false);
|
|
||||||
const [showEnvScan, setShowEnvScan] = useState(false);
|
const [showEnvScan, setShowEnvScan] = useState(false);
|
||||||
const [showCicd, setShowCicd] = useState(false);
|
const [showCicd, setShowCicd] = useState(false);
|
||||||
const [showBlueprint, setShowBlueprint] = useState(false);
|
const [showBlueprint, setShowBlueprint] = useState(false);
|
||||||
@ -68,7 +63,6 @@ export function ProjectCard({ project: p }: Props) {
|
|||||||
const [showMentor, setShowMentor] = useState(false);
|
const [showMentor, setShowMentor] = useState(false);
|
||||||
const [showClaudeConfig, setShowClaudeConfig] = useState(false);
|
const [showClaudeConfig, setShowClaudeConfig] = useState(false);
|
||||||
const [showDevVariables, setShowDevVariables] = useState(false);
|
const [showDevVariables, setShowDevVariables] = useState(false);
|
||||||
const [showTodos, setShowTodos] = useState(false);
|
|
||||||
const [showServers, setShowServers] = useState(false);
|
const [showServers, setShowServers] = useState(false);
|
||||||
const [showBranchMenu, setShowBranchMenu] = useState(false);
|
const [showBranchMenu, setShowBranchMenu] = useState(false);
|
||||||
const [newBranchName, setNewBranchName] = useState("");
|
const [newBranchName, setNewBranchName] = useState("");
|
||||||
@ -191,17 +185,6 @@ export function ProjectCard({ project: p }: Props) {
|
|||||||
staleTime: 30000,
|
staleTime: 30000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: projectTools = [] } = useQuery({
|
|
||||||
queryKey: ["projectTools", p.id],
|
|
||||||
queryFn: () => getProjectTools(p.id),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: acts } = useQuery({
|
|
||||||
queryKey: ["activity", p.id],
|
|
||||||
queryFn: () => getActivity(p.id, 5),
|
|
||||||
enabled: expanded,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 当前已启用的编辑器列表
|
// 当前已启用的编辑器列表
|
||||||
const activeEditors: { id: string; name: string; path: string }[] = (() => {
|
const activeEditors: { id: string; name: string; path: string }[] = (() => {
|
||||||
try {
|
try {
|
||||||
@ -238,15 +221,6 @@ export function ProjectCard({ project: p }: Props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLaunchTool = async (exePath: string, args?: string | null, name?: string) => {
|
|
||||||
try {
|
|
||||||
await launchTool(exePath, args ?? undefined);
|
|
||||||
showToast(`已启动 ${name ?? ""} ✓`);
|
|
||||||
} catch (e) {
|
|
||||||
showToast(`❌ ${e}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const copy = (text: string) => {
|
const copy = (text: string) => {
|
||||||
navigator.clipboard.writeText(text).catch(() => {});
|
navigator.clipboard.writeText(text).catch(() => {});
|
||||||
showToast("已复制 ✓");
|
showToast("已复制 ✓");
|
||||||
@ -555,32 +529,6 @@ export function ProjectCard({ project: p }: Props) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-1.5 flex-wrap">
|
|
||||||
<span className="text-[11px] font-semibold text-purple-700 bg-purple-50 border border-purple-200 rounded px-1.5 py-0.5 shrink-0">快启</span>
|
|
||||||
{projectTools.length > 0 ? (
|
|
||||||
<>
|
|
||||||
{projectTools.map((tool) => (
|
|
||||||
<button key={tool.id} onClick={() => handleLaunchTool(tool.exe_path, tool.args, tool.name)}
|
|
||||||
className="px-2.5 py-1 rounded-md text-xs border border-gray-200 text-gray-600 hover:bg-purple-50 hover:border-purple-300 hover:text-purple-800 transition-colors"
|
|
||||||
title={tool.exe_path}>
|
|
||||||
{tool.icon} {tool.name}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
<button
|
|
||||||
onClick={() => { setToolsMgrDefaultAdd(true); setShowToolsMgr(true); }}
|
|
||||||
className="px-2 py-1 rounded-md text-xs border border-dashed border-purple-200 text-purple-400 hover:border-purple-400 hover:text-purple-600 hover:bg-purple-50 transition-colors"
|
|
||||||
title="添加快启应用">
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
onClick={() => { setToolsMgrDefaultAdd(true); setShowToolsMgr(true); }}
|
|
||||||
className="px-2.5 py-1 rounded-md text-xs border border-dashed border-purple-200 text-purple-400 hover:border-purple-400 hover:text-purple-600 hover:bg-purple-50 transition-colors">
|
|
||||||
+ 配置快启应用
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -603,7 +551,7 @@ export function ProjectCard({ project: p }: Props) {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setShowMoreMenu((v) => !v)}
|
onClick={() => setShowMoreMenu((v) => !v)}
|
||||||
className={`px-2.5 py-1.5 rounded-md text-xs border transition-colors ${
|
className={`px-2.5 py-1.5 rounded-md text-xs border transition-colors ${
|
||||||
showMoreMenu || showTodos || showServers
|
showMoreMenu || showServers
|
||||||
? "border-gray-300 bg-gray-100 text-gray-700"
|
? "border-gray-300 bg-gray-100 text-gray-700"
|
||||||
: "border-gray-200 text-gray-500 hover:bg-gray-50"
|
: "border-gray-200 text-gray-500 hover:bg-gray-50"
|
||||||
}`}
|
}`}
|
||||||
@ -616,11 +564,9 @@ export function ProjectCard({ project: p }: Props) {
|
|||||||
{([
|
{([
|
||||||
{ icon: "🤖", label: "Claude 自主性", action: () => setShowClaudeConfig(true) },
|
{ icon: "🤖", label: "Claude 自主性", action: () => setShowClaudeConfig(true) },
|
||||||
{ icon: "🔑", label: "开发变量", action: () => setShowDevVariables(true) },
|
{ icon: "🔑", label: "开发变量", action: () => setShowDevVariables(true) },
|
||||||
{ icon: "📝", label: showTodos ? "收起备忘" : "个人备忘", action: () => setShowTodos((v) => !v) },
|
|
||||||
{ icon: "🖥️", label: showServers ? "收起服务器" : "关联服务器", action: () => setShowServers((v) => !v) },
|
{ icon: "🖥️", label: showServers ? "收起服务器" : "关联服务器", action: () => setShowServers((v) => !v) },
|
||||||
{ icon: "🚀", label: "CI/CD 控制台", action: () => setShowCicd(true) },
|
{ icon: "🚀", label: "CI/CD 控制台", action: () => setShowCicd(true) },
|
||||||
{ icon: "🔬", label: "环境扫描", action: () => setShowEnvScan(true) },
|
{ icon: "🔬", label: "环境扫描", action: () => setShowEnvScan(true) },
|
||||||
{ icon: "🧰", label: "工具管理", action: () => setShowToolsMgr(true) },
|
|
||||||
]).map(({ icon, label, action }) => (
|
]).map(({ icon, label, action }) => (
|
||||||
<button
|
<button
|
||||||
key={label}
|
key={label}
|
||||||
@ -640,22 +586,9 @@ export function ProjectCard({ project: p }: Props) {
|
|||||||
className="px-3 py-1.5 rounded-md text-xs border border-gray-200 text-gray-600 hover:bg-gray-50 transition-colors">
|
className="px-3 py-1.5 rounded-md text-xs border border-gray-200 text-gray-600 hover:bg-gray-50 transition-colors">
|
||||||
编辑
|
编辑
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => setExpanded((v) => !v)}
|
|
||||||
className="px-3 py-1.5 rounded-md text-xs border border-gray-200 text-gray-600 hover:bg-gray-50 transition-colors">
|
|
||||||
{expanded ? "收起 ▲" : "动态 ▼"}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showToolsMgr && (
|
|
||||||
<ProjectToolsModal
|
|
||||||
projectId={p.id}
|
|
||||||
projectName={p.name}
|
|
||||||
defaultOpenAdd={toolsMgrDefaultAdd}
|
|
||||||
onClose={() => { setShowToolsMgr(false); setToolsMgrDefaultAdd(false); }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showEnvScan && (
|
{showEnvScan && (
|
||||||
<ProjectEnvModal
|
<ProjectEnvModal
|
||||||
project={p}
|
project={p}
|
||||||
@ -716,32 +649,8 @@ export function ProjectCard({ project: p }: Props) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 个人备忘 */}
|
|
||||||
{showTodos && <UserTodoPanel projectId={p.id} />}
|
|
||||||
|
|
||||||
{/* 关联服务器 */}
|
{/* 关联服务器 */}
|
||||||
{showServers && <ProjectServerPanel projectId={p.id} />}
|
{showServers && <ProjectServerPanel projectId={p.id} />}
|
||||||
|
|
||||||
{/* Expanded activity */}
|
|
||||||
{expanded && (
|
|
||||||
<div className="border-t border-gray-50 pt-3">
|
|
||||||
<p className="text-xs font-medium text-gray-500 mb-2">最近动态</p>
|
|
||||||
{!acts ? (
|
|
||||||
<p className="text-xs text-gray-400">加载中…</p>
|
|
||||||
) : acts.length === 0 ? (
|
|
||||||
<p className="text-xs text-gray-400">暂无记录</p>
|
|
||||||
) : (
|
|
||||||
<ul className="flex flex-col gap-1">
|
|
||||||
{acts.map((a) => (
|
|
||||||
<li key={a.id} className="flex gap-2 text-xs text-gray-500">
|
|
||||||
<span className="text-gray-300 shrink-0">{(a.created_at ?? "").slice(5, 10)}</span>
|
|
||||||
<span>{a.summary}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import {
|
|||||||
import { useUIStore } from "../../store/ui";
|
import { useUIStore } from "../../store/ui";
|
||||||
|
|
||||||
import { ServiceConfigPanel } from "./ServiceConfigBlock";
|
import { ServiceConfigPanel } from "./ServiceConfigBlock";
|
||||||
import { HostAppsPanel } from "./HostAppsPanel";
|
|
||||||
|
|
||||||
function CopyBtn({ value }: { value: string }) {
|
function CopyBtn({ value }: { value: string }) {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
@ -472,9 +471,6 @@ function ServerCard({ server, onEdit, onDelete, globalKeyPath }: {
|
|||||||
{/* VS Code Remote */}
|
{/* VS Code Remote */}
|
||||||
<VscodeRemoteBlock serverId={server.id} serverName={server.name} />
|
<VscodeRemoteBlock serverId={server.id} serverName={server.name} />
|
||||||
|
|
||||||
{/* 宿主机应用 */}
|
|
||||||
<HostAppsPanel serverId={server.id} />
|
|
||||||
|
|
||||||
{/* 服务连接配置(Linux 客户端层) */}
|
{/* 服务连接配置(Linux 客户端层) */}
|
||||||
<ServiceConfigPanel serverId={server.id} scope="system" />
|
<ServiceConfigPanel serverId={server.id} scope="system" />
|
||||||
|
|
||||||
|
|||||||
@ -1,104 +0,0 @@
|
|||||||
import { useState, useRef, useEffect } from "react";
|
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { getUserTodos, addUserTodo, toggleUserTodo, deleteUserTodo } from "../../lib/commands";
|
|
||||||
|
|
||||||
export function UserTodoPanel({ projectId }: { projectId: string }) {
|
|
||||||
const qc = useQueryClient();
|
|
||||||
const [input, setInput] = useState("");
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const { data: todos = [] } = useQuery({
|
|
||||||
queryKey: ["user-todos", projectId],
|
|
||||||
queryFn: () => getUserTodos(projectId),
|
|
||||||
});
|
|
||||||
|
|
||||||
const addMut = useMutation({
|
|
||||||
mutationFn: (content: string) => addUserTodo(projectId, content),
|
|
||||||
onSuccess: () => {
|
|
||||||
qc.invalidateQueries({ queryKey: ["user-todos", projectId] });
|
|
||||||
setInput("");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const toggleMut = useMutation({
|
|
||||||
mutationFn: (id: number) => toggleUserTodo(id),
|
|
||||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["user-todos", projectId] }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const deleteMut = useMutation({
|
|
||||||
mutationFn: (id: number) => deleteUserTodo(id),
|
|
||||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["user-todos", projectId] }),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
inputRef.current?.focus();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (e.key === "Enter" && input.trim()) {
|
|
||||||
addMut.mutate(input.trim());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const pending = todos.filter((t) => !t.done);
|
|
||||||
const done = todos.filter((t) => t.done);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-2 border-t border-gray-50 pt-3">
|
|
||||||
{/* 输入框 */}
|
|
||||||
<input
|
|
||||||
ref={inputRef}
|
|
||||||
value={input}
|
|
||||||
onChange={(e) => setInput(e.target.value)}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
placeholder="记点什么… 回车确认"
|
|
||||||
className="w-full text-xs px-2 py-1.5 rounded border border-gray-200 bg-gray-50
|
|
||||||
placeholder-gray-300 focus:outline-none focus:border-gray-400 mb-2"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 待办列表 */}
|
|
||||||
{pending.length === 0 && done.length === 0 && (
|
|
||||||
<p className="text-xs text-gray-300 text-center py-1">暂无备忘</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ul className="flex flex-col gap-0.5">
|
|
||||||
{pending.map((t) => (
|
|
||||||
<li key={t.id} className="flex items-center gap-2 group">
|
|
||||||
<button
|
|
||||||
onClick={() => toggleMut.mutate(t.id)}
|
|
||||||
className="w-3.5 h-3.5 rounded border border-gray-300 shrink-0
|
|
||||||
hover:border-blue-400 transition-colors"
|
|
||||||
/>
|
|
||||||
<span className="text-xs text-gray-600 flex-1 leading-relaxed">{t.content}</span>
|
|
||||||
<button
|
|
||||||
onClick={() => deleteMut.mutate(t.id)}
|
|
||||||
className="text-gray-200 hover:text-red-400 transition-colors opacity-0 group-hover:opacity-100 text-xs shrink-0"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* 已完成的条目折叠在末尾 */}
|
|
||||||
{done.map((t) => (
|
|
||||||
<li key={t.id} className="flex items-center gap-2 group">
|
|
||||||
<button
|
|
||||||
onClick={() => toggleMut.mutate(t.id)}
|
|
||||||
className="w-3.5 h-3.5 rounded border border-gray-200 bg-gray-100 shrink-0
|
|
||||||
flex items-center justify-center hover:border-gray-400 transition-colors"
|
|
||||||
>
|
|
||||||
<span className="text-gray-400 text-[9px] leading-none">✓</span>
|
|
||||||
</button>
|
|
||||||
<span className="text-xs text-gray-300 line-through flex-1 leading-relaxed">{t.content}</span>
|
|
||||||
<button
|
|
||||||
onClick={() => deleteMut.mutate(t.id)}
|
|
||||||
className="text-gray-200 hover:text-red-400 transition-colors opacity-0 group-hover:opacity-100 text-xs shrink-0"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export {};
|
|
||||||
@ -34,17 +34,6 @@ export interface Project {
|
|||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProjectDetail extends Project {
|
|
||||||
recent_activities: ActivityLog[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ActivityLog {
|
|
||||||
id: number;
|
|
||||||
project_id: string;
|
|
||||||
summary: string;
|
|
||||||
created_at?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProductGroup {
|
export interface ProductGroup {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -72,7 +61,7 @@ export const getProjects = (status?: string, spaceId?: string) =>
|
|||||||
invoke<Project[]>("get_projects", { status, spaceId });
|
invoke<Project[]>("get_projects", { status, spaceId });
|
||||||
|
|
||||||
export const getProject = (id: string) =>
|
export const getProject = (id: string) =>
|
||||||
invoke<ProjectDetail>("get_project", { id });
|
invoke<Project>("get_project", { id });
|
||||||
|
|
||||||
export const createProject = (input: Omit<Project, "updated_at" | "workspace_id">) =>
|
export const createProject = (input: Omit<Project, "updated_at" | "workspace_id">) =>
|
||||||
invoke<void>("create_project", { input });
|
invoke<void>("create_project", { input });
|
||||||
@ -100,14 +89,6 @@ export interface BatchProjectInput {
|
|||||||
export const batchCreateProjects = (items: BatchProjectInput[]) =>
|
export const batchCreateProjects = (items: BatchProjectInput[]) =>
|
||||||
invoke<string[]>("batch_create_projects", { items });
|
invoke<string[]>("batch_create_projects", { items });
|
||||||
|
|
||||||
// ── Activity ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export const getActivity = (projectId: string, limit?: number) =>
|
|
||||||
invoke<ActivityLog[]>("get_activity", { projectId, limit });
|
|
||||||
|
|
||||||
export const addActivity = (projectId: string, summary: string) =>
|
|
||||||
invoke<void>("add_activity", { projectId, summary });
|
|
||||||
|
|
||||||
// ── Groups ────────────────────────────────────────────────────────────────────
|
// ── Groups ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const getGroups = (spaceId?: string) => invoke<ProductGroup[]>("get_groups", { spaceId });
|
export const getGroups = (spaceId?: string) => invoke<ProductGroup[]>("get_groups", { spaceId });
|
||||||
@ -222,11 +203,10 @@ export interface HealthReport {
|
|||||||
|
|
||||||
export const runHealthCheck = () => invoke<HealthReport>("run_health_check");
|
export const runHealthCheck = () => invoke<HealthReport>("run_health_check");
|
||||||
|
|
||||||
// ── Project Tools ─────────────────────────────────────────────────────────────
|
// ── Global Quick Launch ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface ProjectTool {
|
export interface GlobalTool {
|
||||||
id: string;
|
id: string;
|
||||||
project_id: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
exe_path: string;
|
exe_path: string;
|
||||||
@ -234,7 +214,7 @@ export interface ProjectTool {
|
|||||||
sort_order: number;
|
sort_order: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProjectToolInput {
|
export interface GlobalToolInput {
|
||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
exe_path: string;
|
exe_path: string;
|
||||||
@ -242,30 +222,16 @@ export interface ProjectToolInput {
|
|||||||
sort_order?: number;
|
sort_order?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getProjectTools = (projectId: string) =>
|
|
||||||
invoke<ProjectTool[]>("get_project_tools", { projectId });
|
|
||||||
|
|
||||||
export const addProjectTool = (projectId: string, input: ProjectToolInput) =>
|
|
||||||
invoke<string>("add_project_tool", { projectId, input });
|
|
||||||
|
|
||||||
export const updateProjectTool = (id: string, input: ProjectToolInput) =>
|
|
||||||
invoke<void>("update_project_tool", { id, input });
|
|
||||||
|
|
||||||
export const removeProjectTool = (id: string) =>
|
|
||||||
invoke<void>("remove_project_tool", { id });
|
|
||||||
|
|
||||||
export const launchTool = (exePath: string, args?: string) =>
|
export const launchTool = (exePath: string, args?: string) =>
|
||||||
invoke<void>("launch_tool", { exePath, args });
|
invoke<void>("launch_tool", { exePath, args });
|
||||||
|
|
||||||
// ── Global Quick Launch ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export const getGlobalTools = () =>
|
export const getGlobalTools = () =>
|
||||||
invoke<ProjectTool[]>("get_global_tools");
|
invoke<GlobalTool[]>("get_global_tools");
|
||||||
|
|
||||||
export const addGlobalTool = (input: ProjectToolInput) =>
|
export const addGlobalTool = (input: GlobalToolInput) =>
|
||||||
invoke<string>("add_global_tool", { input });
|
invoke<string>("add_global_tool", { input });
|
||||||
|
|
||||||
export const updateGlobalTool = (id: string, input: ProjectToolInput) =>
|
export const updateGlobalTool = (id: string, input: GlobalToolInput) =>
|
||||||
invoke<void>("update_global_tool", { id, input });
|
invoke<void>("update_global_tool", { id, input });
|
||||||
|
|
||||||
export const removeGlobalTool = (id: string) =>
|
export const removeGlobalTool = (id: string) =>
|
||||||
@ -414,14 +380,6 @@ export interface Space {
|
|||||||
created_at?: string;
|
created_at?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeployCommand {
|
|
||||||
id: string;
|
|
||||||
project_id: string;
|
|
||||||
name: string;
|
|
||||||
command: string;
|
|
||||||
run_in: "windows" | "wsl";
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getSpaces = () =>
|
export const getSpaces = () =>
|
||||||
invoke<Space[]>("get_spaces");
|
invoke<Space[]>("get_spaces");
|
||||||
|
|
||||||
@ -437,22 +395,6 @@ export const deleteSpace = (id: string) =>
|
|||||||
export const setCurrentSpace = (id: string) =>
|
export const setCurrentSpace = (id: string) =>
|
||||||
invoke<void>("set_current_space", { id });
|
invoke<void>("set_current_space", { id });
|
||||||
|
|
||||||
export const getDeployCommands = (projectId: string) =>
|
|
||||||
invoke<DeployCommand[]>("get_deploy_commands", { projectId });
|
|
||||||
|
|
||||||
export const addDeployCommand = (
|
|
||||||
projectId: string,
|
|
||||||
name: string,
|
|
||||||
command: string,
|
|
||||||
runIn: "windows" | "wsl"
|
|
||||||
) => invoke<DeployCommand>("add_deploy_command", { projectId, name, command, runIn });
|
|
||||||
|
|
||||||
export const removeDeployCommand = (id: string) =>
|
|
||||||
invoke<void>("remove_deploy_command", { id });
|
|
||||||
|
|
||||||
export const executeDeploy = (command: string, runIn: "windows" | "wsl", cwd?: string) =>
|
|
||||||
invoke<string>("execute_deploy", { command, runIn, cwd });
|
|
||||||
|
|
||||||
// ── Git 操作 ──────────────────────────────────────────────────────────────────
|
// ── Git 操作 ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface GitStatus {
|
export interface GitStatus {
|
||||||
@ -1171,29 +1113,6 @@ export const getProjectNotes = (projectId: string) =>
|
|||||||
export const addProjectNote = (projectId: string, content: string, source: string = "manual") =>
|
export const addProjectNote = (projectId: string, content: string, source: string = "manual") =>
|
||||||
invoke<void>("add_project_note", { projectId, content, source });
|
invoke<void>("add_project_note", { projectId, content, source });
|
||||||
|
|
||||||
// ── 用户项目个人备忘 ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export interface UserTodo {
|
|
||||||
id: number;
|
|
||||||
project_id: string;
|
|
||||||
content: string;
|
|
||||||
done: boolean;
|
|
||||||
sort_order: number;
|
|
||||||
created_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getUserTodos = (projectId: string) =>
|
|
||||||
invoke<UserTodo[]>("get_user_todos", { projectId });
|
|
||||||
|
|
||||||
export const addUserTodo = (projectId: string, content: string) =>
|
|
||||||
invoke<UserTodo>("add_user_todo", { projectId, content });
|
|
||||||
|
|
||||||
export const toggleUserTodo = (id: number) =>
|
|
||||||
invoke<void>("toggle_user_todo", { id });
|
|
||||||
|
|
||||||
export const deleteUserTodo = (id: number) =>
|
|
||||||
invoke<void>("delete_user_todo", { id });
|
|
||||||
|
|
||||||
// ── Servers ──────────────────────────────────────────────────────────────────
|
// ── Servers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface QuickLink {
|
export interface QuickLink {
|
||||||
@ -1318,56 +1237,6 @@ export const editServiceConfig = (id: string, input: SaveServiceConfigInput) =>
|
|||||||
export const deleteServiceConfig = (id: string) =>
|
export const deleteServiceConfig = (id: string) =>
|
||||||
invoke<void>("delete_service_config", { id });
|
invoke<void>("delete_service_config", { id });
|
||||||
|
|
||||||
// ── Host Apps(宿主机部署应用)────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export interface HostApp {
|
|
||||||
id: string;
|
|
||||||
server_id: string;
|
|
||||||
name: string;
|
|
||||||
deploy_type: string;
|
|
||||||
service_name?: string;
|
|
||||||
binary_path?: string;
|
|
||||||
config_path?: string;
|
|
||||||
log_path?: string;
|
|
||||||
port?: string;
|
|
||||||
start_cmd?: string;
|
|
||||||
stop_cmd?: string;
|
|
||||||
restart_cmd?: string;
|
|
||||||
status_cmd?: string;
|
|
||||||
notes?: string;
|
|
||||||
sort_order?: number;
|
|
||||||
created_at?: string;
|
|
||||||
updated_at?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SaveHostAppInput {
|
|
||||||
name: string;
|
|
||||||
deploy_type: string;
|
|
||||||
service_name?: string;
|
|
||||||
binary_path?: string;
|
|
||||||
config_path?: string;
|
|
||||||
log_path?: string;
|
|
||||||
port?: string;
|
|
||||||
start_cmd?: string;
|
|
||||||
stop_cmd?: string;
|
|
||||||
restart_cmd?: string;
|
|
||||||
status_cmd?: string;
|
|
||||||
notes?: string;
|
|
||||||
sort_order?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getHostApps = (serverId: string) =>
|
|
||||||
invoke<HostApp[]>("get_host_apps", { serverId });
|
|
||||||
|
|
||||||
export const addHostApp = (serverId: string, input: SaveHostAppInput) =>
|
|
||||||
invoke<HostApp>("add_host_app", { serverId, input });
|
|
||||||
|
|
||||||
export const editHostApp = (id: string, input: SaveHostAppInput) =>
|
|
||||||
invoke<HostApp>("edit_host_app", { id, input });
|
|
||||||
|
|
||||||
export const deleteHostApp = (id: string) =>
|
|
||||||
invoke<void>("delete_host_app", { id });
|
|
||||||
|
|
||||||
// ── API Keys ─────────────────────────────────────────────────────────────────
|
// ── API Keys ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface ApiKey {
|
export interface ApiKey {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user