Compare commits
No commits in common. "663a723b3ba97586d234f82ba8557173a736b81e" and "9211883a9b3c601d84ac18efbcea30e73e6f0064" have entirely different histories.
663a723b3b
...
9211883a9b
@ -186,26 +186,3 @@ progress: 91
|
||||
- acceptance: "agent_verify: [真实 scoped MCP/cargo test] 合成项目登记→同一本体填写→再次查询已配置→读取→撤销后读取失败,并确认目录没有新增 .env 或明文配置;cargo test、pnpm typecheck、git diff --check exit 0。human_verify: 真实桌面 UI 与真实 Agent 客户端跨会话便利性另报未验证,不冒充 V7 完成。"
|
||||
- human_verify: pending(真实桌面 UI 和真实 Agent 客户端跨会话复用尚未终验;V7 保持 blocked)
|
||||
- evidence: 真实本机 scoped MCP HTTP 合成链路逐步断言登记、待填写、受保护存储填写、复查已配置、项目 bearer 读取、撤销后拒绝;数据库重开复用测试通过。`cargo test --locked` 227 passed/4 ignored,`mise exec -- pnpm typecheck` 与 `git diff --check` exit 0;未新增 `.env` 或明文配置文件。
|
||||
|
||||
## 任务拆解:UI 终验反馈与 agent 可发现性(2026-09-30)
|
||||
|
||||
> 来源:用户对 V7 的 human_verify 否决理由——「修改时空白,应像修改配置一样呈现旧值」「撤销无确认框」(后者为 bug,已由 `f74fe11` 修复并加回归测试);以及 agent 侧可发现性缺口:接入包 skill 未提示「需要测试账号时先查炼境」,agent 会继续向人索要。
|
||||
|
||||
### ✅ V12 — 修改时显式查看原值(人用投影)
|
||||
- status: done
|
||||
- complexity: M
|
||||
- files: src-tauri/src/commands/dev_variables.rs, src-tauri/src/lib.rs, src/lib/commands.ts, src/components/dashboard/ProjectDevVariablesPanel.tsx, docs/ai-context/project-context.yaml
|
||||
- depends: V10
|
||||
- acceptance: "agent_verify: [cargo test] reveal(已配置测试账户) → 账号与密码原值;reveal(标量) → 原值;未配置 → 错误;MCP tools/list 不含任何 reveal 工具。human_verify: 点「修改」→ 账号预填可见、密码预填但默认遮罩,点「显示」可见;保存后生效。"
|
||||
- verification: "2026-09-30:cargo test reveal_returns_original_value_by_kind_only_for_humans 覆盖测试账户/标量取回、未配置与撤销后报错、MCP tools/list 不含 reveal;全量 cargo test --locked 228 passed / 5 ignored,pnpm typecheck exit 0,vitest 8 passed。"
|
||||
- human_verify: pending(第二次 UI 终验)
|
||||
- decision: 值仅经本机 Tauri command 在人显式点「修改」时取回,沿用 API Key R2 的显式 reveal 模式;不进 MCP,agent 取值仍只走已认证的 read_project_test_account。密码默认遮罩防截图/旁观泄露。
|
||||
|
||||
### ✅ V13 — 接入包 skill 提示 agent 先查炼境测试账户
|
||||
- status: done
|
||||
- complexity: S
|
||||
- files: src-tauri/resources/onboarding/.agents/skills/lian-jing/SKILL.md, src-tauri/src/commands/onboarding.rs
|
||||
- depends: V9
|
||||
- acceptance: "agent_verify: [cargo test onboarding] base-onboarding 版本升级后旧版本项目报告 version_mismatch、新模板含测试账户指引。human_verify: 合并并重建后对 Enterprise apply_onboarding_pack,agent 需要登录时先查炼境而非向人索要。"
|
||||
- verification: "2026-09-30:SKILL.md 新增第 8 步(先 list,已配置单项读取,否则登记并请用户在 UI 填写;禁止对话索要明文、禁止登记生产、禁止写入 Git 跟踪文件、认证被拒如实报告);BASE_ONBOARDING_PACK_VERSION 与模板 capability-packs.json 同步升至 1.5.0(仅升常量时 2 个 onboarding 测试失败,已同步修正);cargo test --locked 全绿。"
|
||||
- human_verify: pending(分发到 Enterprise 后观察 agent 行为)
|
||||
|
||||
@ -31,7 +31,7 @@ contracts:
|
||||
paths:
|
||||
- "src-tauri/src/commands/**/*.rs"
|
||||
- "src/lib/commands.ts"
|
||||
doc: "新增 command 时前后端必须同步。Gitea/API Key 普通 DTO 不含原始凭证;API Key 仅本机 UI 显式 reveal,get_project_resource_status 仅返回非秘密状态;项目开发需求列表 DTO 仅含名称、类型、目标系统、环境、用途、configured 与时间。本机 UI 对 test_account 一次提交账号/密码到同一受保护引用,允许修改或撤销;列表常态不含值,仅人显式点「修改」时经 reveal_project_dev_variable 取回原值(密码默认遮罩),该 command 不进 MCP;旧 scalar 保持兼容。旧 SQLite 明文列按 write→verify→事务切换引用并清除的顺序兼容迁移,失败需可恢复,不能降级明文。Agent readiness 必须保留 configured/reachable/handshake/tools_ready/reload_required 分层;Windows autostart 默认关闭,只允许正式安装版,debug/target 构建必须拒绝启用。"
|
||||
doc: "新增 command 时前后端必须同步。Gitea/API Key 普通 DTO 不含原始凭证;API Key 仅本机 UI 显式 reveal,get_project_resource_status 仅返回非秘密状态;项目开发需求列表 DTO 仅含名称、类型、目标系统、环境、用途、configured 与时间。本机 UI 对 test_account 一次提交账号/密码到同一受保护引用,允许修改或撤销但无常态 reveal;旧 scalar 保持兼容。旧 SQLite 明文列按 write→verify→事务切换引用并清除的顺序兼容迁移,失败需可恢复,不能降级明文。Agent readiness 必须保留 configured/reachable/handshake/tools_ready/reload_required 分层;Windows autostart 默认关闭,只允许正式安装版,debug/target 构建必须拒绝启用。"
|
||||
|
||||
architecture_docs:
|
||||
- "AGENTS.md"
|
||||
|
||||
@ -36,13 +36,13 @@
|
||||
- 返回值、错误文本、日志均不含账号或密码。
|
||||
- MCP 投影走 scoped 路由,属写工具。客户端审批策略(参见 V7 blocker)按正常审批处理,不标注为只读来绕过。人的投影:`ProjectDevVariablesPanel` 可选加「写入工作区」按钮,属于 human_verify 范围,非 v1 必需。
|
||||
|
||||
## 任务卡草案(合入 dev-variables 模块后编号接续 V13;V12/V13 已被 UI 终验反馈占用)
|
||||
## 任务卡草案(合入 dev-variables 模块后编号接续 V11)
|
||||
|
||||
- **V14 落地本体**(M)— files: `src-tauri/src/commands/dev_variables.rs`, `src/lib/commands.ts`;depends: project-dev-variables 合入 master
|
||||
- **V12 落地本体**(M)— files: `src-tauri/src/commands/dev_variables.rs`, `src/lib/commands.ts`;depends: project-dev-variables 合入 master
|
||||
- acceptance: `agent_verify: [cargo test] 临时 git 仓库 + 模拟受保护存储:目标被忽略 → 仅两变量行写入、其他行保留、二次调用字节不变;撤销后调用 → 两行被删;目标未被忽略 / 越出工作区 / worktree 属另一仓库 / 账户未填写 → 拒绝且文件字节不变;返回与错误序列化不含账号密码。`
|
||||
- **V15 MCP 投影**(M)— files: `src-tauri/src/mcp_tools.rs`;depends: V14
|
||||
- **V13 MCP 投影**(M)— files: `src-tauri/src/mcp_tools.rs`;depends: V12
|
||||
- acceptance: `agent_verify: [cargo test] tools/list 含 materialize_project_test_account,schema 要求 project_id;跨项目参数被 scope 拒绝;无认证调用可写入但响应只含变量名;仍无任何批量导出或 get_secret 工具。`
|
||||
- **V16 Enterprise worktree 试点**(M)— files: `.blueprint/modules/project-dev-variables.md`, `docs/ai-context/project-context.yaml`, `docs/ai-context/agent-button-system.md`;depends: V15
|
||||
- **V14 Enterprise worktree 试点**(M)— files: `.blueprint/modules/project-dev-variables.md`, `docs/ai-context/project-context.yaml`, `docs/ai-context/agent-button-system.md`;depends: V13
|
||||
- acceptance: `agent_verify: [CLI] Enterprise 新建隔离 worktree(无 .env.agent)→ [MCP] materialize_project_test_account(enterprise, <账户名>, ".env.agent", "AGENT_USERNAME", "AGENT_PASSWORD", worktree) → [CLI] 起本地 API 后 pnpm --silent --filter @enterprise-system/api agent:token exit 0 且 stdout 非空;git status 显示 .env.agent 未被跟踪;会话记录不含账号值。`
|
||||
- `human_verify: 在炼境登记一次后,agent 在新 worktree 中无需向人索要账号。`
|
||||
- 度量:人工传递账号次数(目标 0)、新 worktree 就绪到首次 agent:token 成功的耗时。未达收益按「管家退出」规则撤销。
|
||||
|
||||
@ -16,7 +16,6 @@ description: 读取炼境项目健康状态,并使用炼境 MCP 的项目管
|
||||
5. 交付、Gitea、CI、蓝图等平台状态优先通过 MCP 查询,不凭静态文档猜测。
|
||||
6. 合并 PR 前调用 `get_delivery_status(project_id)`,向用户展示 PR 编号、完整 head SHA、CI 与 review。只有当前用户明确确认该 PR 与 SHA 后,才调用 `merge_pull_request(project_id, pr_number, expected_head_sha, confirmed=true)`;合并后再次读取交付状态。不得自行推断确认。
|
||||
7. 生成 CI workflow 时先调用 `plan_cicd_workflow(project_id, workflow_type)`,向用户展示目标文件、变更类型、`content_digest`、工具链来源与所需 secret 名称;只有用户明确确认这份摘要后,才以同一 digest 调用 `apply_cicd_workflow(..., confirmed=true)`。该流程只写项目 workflow,不配置 secret、不触发 CI 或部署;不得通过 MCP 传服务器地址、命令、路径或 secret 值,部署类配置先由用户在炼境 UI 保存。
|
||||
8. 需要测试账号或开发变量(登录自验、沙箱 key 等)时,先调用 `list_project_dev_variables` 查找:已配置则用 `read_project_test_account` / `read_project_dev_variable` 单项读取;没有则用 `register_project_test_account` 登记名称、目标系统、非生产环境与用途,并请用户在炼境项目 UI「开发变量」中填写。不得在对话中向用户索要明文密码,不得登记生产账号,不得把读到的值写入受 Git 跟踪的文件。读取因缺少项目认证被拒时(当前仅 Codex 经 helper 验证可读),如实报告并改用项目原有配置方式,不得绕过认证。
|
||||
|
||||
如果 MCP 未出现,按以下顺序降级,不猜测状态:
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"packs": [
|
||||
{
|
||||
"pack_id": "base-onboarding",
|
||||
"version": "1.5.0",
|
||||
"version": "1.4.0",
|
||||
"supported_clients": ["codex", "claude"],
|
||||
"project_artifacts": [
|
||||
"AGENTS.md",
|
||||
|
||||
@ -24,14 +24,6 @@ pub struct TestAccountValue {
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
/// 人在 UI 显式「修改」时取回的原值;仅本机 Tauri 投影使用,MCP 不公开。
|
||||
#[derive(Serialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum DevVariableReveal {
|
||||
Scalar { value: String },
|
||||
TestAccount { username: String, password: String },
|
||||
}
|
||||
|
||||
const SCALAR_KIND: &str = "scalar";
|
||||
const TEST_ACCOUNT_KIND: &str = "test_account";
|
||||
|
||||
@ -246,35 +238,6 @@ pub fn read_test_account(
|
||||
Ok(account)
|
||||
}
|
||||
|
||||
/// 按登记类型取回原值,复用 agent 读取同一本体,只是投影给人。
|
||||
pub fn reveal(
|
||||
conn: &Connection,
|
||||
store: &impl CredentialStore,
|
||||
project_id: &str,
|
||||
name: &str,
|
||||
) -> Result<DevVariableReveal, String> {
|
||||
require_project(conn, project_id)?;
|
||||
let name = normalized_name(name)?;
|
||||
let kind: String = conn
|
||||
.query_row(
|
||||
"SELECT kind FROM project_dev_variables WHERE project_id=?1 AND name=?2",
|
||||
params![project_id, name],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(|_| "开发变量未登记".to_string())?;
|
||||
if kind == TEST_ACCOUNT_KIND {
|
||||
let account = read_test_account(conn, store, project_id, &name)?;
|
||||
Ok(DevVariableReveal::TestAccount {
|
||||
username: account.username,
|
||||
password: account.password,
|
||||
})
|
||||
} else {
|
||||
Ok(DevVariableReveal::Scalar {
|
||||
value: read(conn, store, project_id, &name)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn read_value(
|
||||
conn: &Connection,
|
||||
store: &impl CredentialStore,
|
||||
@ -481,23 +444,6 @@ pub fn save_project_test_account(
|
||||
)
|
||||
}
|
||||
|
||||
/// 仅供本机 UI 在用户显式点「修改」时调用;MCP 不公开此 command。
|
||||
#[tauri::command]
|
||||
pub fn reveal_project_dev_variable(
|
||||
project_id: String,
|
||||
name: String,
|
||||
) -> Result<DevVariableReveal, String> {
|
||||
let conn = crate::db::pool()
|
||||
.get()
|
||||
.map_err(|_| "数据库不可用".to_string())?;
|
||||
reveal(
|
||||
&conn,
|
||||
&crate::credential_store::WindowsCredentialStore,
|
||||
&project_id,
|
||||
&name,
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn revoke_project_dev_variable(project_id: String, name: String) -> Result<(), String> {
|
||||
let conn = crate::db::pool()
|
||||
@ -561,34 +507,6 @@ mod tests {
|
||||
assert!(read(&conn, &store, "p1", "E2E_PASSWORD").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reveal_returns_original_value_by_kind_only_for_humans() {
|
||||
let conn = fixture();
|
||||
let store = MemoryCredentialStore::new();
|
||||
register_test_account(&conn, "p1", "AGENT_ADMIN", "api", "local", "自验").unwrap();
|
||||
register(&conn, "p1", "SANDBOX_KEY", "沙箱").unwrap();
|
||||
assert!(reveal(&conn, &store, "p1", "AGENT_ADMIN").is_err());
|
||||
assert!(reveal(&conn, &store, "p1", "MISSING").is_err());
|
||||
|
||||
save_test_account(&conn, &store, "p1", "AGENT_ADMIN", "synthetic-user", "synthetic-pass")
|
||||
.unwrap();
|
||||
save(&conn, &store, "p1", "SANDBOX_KEY", "synthetic-key").unwrap();
|
||||
assert_eq!(
|
||||
serde_json::to_value(reveal(&conn, &store, "p1", "agent_admin").unwrap()).unwrap(),
|
||||
serde_json::json!({"kind": "test_account", "username": "synthetic-user", "password": "synthetic-pass"})
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(reveal(&conn, &store, "p1", "SANDBOX_KEY").unwrap()).unwrap(),
|
||||
serde_json::json!({"kind": "scalar", "value": "synthetic-key"})
|
||||
);
|
||||
|
||||
revoke(&conn, &store, "p1", "AGENT_ADMIN").unwrap();
|
||||
assert!(reveal(&conn, &store, "p1", "AGENT_ADMIN").is_err());
|
||||
// 人用投影不得进入 agent 通道。
|
||||
let tools = crate::mcp_tools::tools_list_result().to_string();
|
||||
assert!(!tools.contains("reveal"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_account_is_one_idempotent_request_and_one_protected_value() {
|
||||
let conn = fixture();
|
||||
|
||||
@ -26,7 +26,7 @@ const TMPL_CAPABILITY_PACKS: &str =
|
||||
const TMPL_MISE: &str = include_str!("../../resources/onboarding/mise.toml.tmpl");
|
||||
const PROJECT_CONTEXT_SCHEMA_VERSION: u64 = 1;
|
||||
const CAPABILITY_PACK_SCHEMA_VERSION: u64 = 1;
|
||||
const BASE_ONBOARDING_PACK_VERSION: &str = "1.5.0";
|
||||
const BASE_ONBOARDING_PACK_VERSION: &str = "1.4.0";
|
||||
const TOOLCHAIN_PACK_VERSION: &str = "1.0.0";
|
||||
|
||||
/// 单个模板的分发结果。
|
||||
|
||||
@ -406,7 +406,6 @@ pub fn run() {
|
||||
commands::dev_variables::list_project_dev_variables,
|
||||
commands::dev_variables::save_project_dev_variable,
|
||||
commands::dev_variables::save_project_test_account,
|
||||
commands::dev_variables::reveal_project_dev_variable,
|
||||
commands::dev_variables::revoke_project_dev_variable,
|
||||
// host apps
|
||||
commands::host_apps::get_host_apps,
|
||||
|
||||
@ -24,7 +24,6 @@ import {
|
||||
gitPush,
|
||||
type GiteaInstance,
|
||||
} from "../../lib/commands";
|
||||
import { confirm } from "@tauri-apps/plugin-dialog";
|
||||
import { useUIStore } from "../../store/ui";
|
||||
|
||||
type BindMode = "create" | "migrate" | "link";
|
||||
@ -99,7 +98,7 @@ export function GiteaInstancesSection() {
|
||||
};
|
||||
|
||||
const handleDelete = async (inst: GiteaInstance) => {
|
||||
if (!(await confirm(`删除实例「${inst.name}」?其下所有项目绑定会一并删除。`, { kind: "warning" }))) return;
|
||||
if (!confirm(`删除实例「${inst.name}」?其下所有项目绑定会一并删除。`)) return;
|
||||
try {
|
||||
await giteaDeleteInstance(inst.id);
|
||||
qc.invalidateQueries({ queryKey: ["gitea-instances"] });
|
||||
@ -309,7 +308,7 @@ export function GiteaPanel({ projectId, projectName, projectPath }: Props) {
|
||||
};
|
||||
|
||||
const handleUnlink = async () => {
|
||||
if (!(await confirm("解绑当前项目与 Gitea 仓库?(Gitea 侧仓库不会删除)", { kind: "warning" }))) return;
|
||||
if (!confirm("解绑当前项目与 Gitea 仓库?(Gitea 侧仓库不会删除)")) return;
|
||||
try {
|
||||
await giteaUnlinkRepo(projectId);
|
||||
refresh();
|
||||
|
||||
@ -2,13 +2,10 @@ import { useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
listProjectDevVariables,
|
||||
revealProjectDevVariable,
|
||||
saveProjectDevVariable,
|
||||
saveProjectTestAccount,
|
||||
revokeProjectDevVariable,
|
||||
type ProjectDevVariableStatus,
|
||||
} from "../../lib/commands";
|
||||
import { confirm } from "@tauri-apps/plugin-dialog";
|
||||
import { useUIStore } from "../../store/ui";
|
||||
import { Dialog } from "../ui/Dialog";
|
||||
|
||||
@ -26,8 +23,6 @@ export function ProjectDevVariablesPanel({ projectId, projectName, onClose }: Pr
|
||||
const [draftUsername, setDraftUsername] = useState("");
|
||||
const [draftPassword, setDraftPassword] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [showSecret, setShowSecret] = useState(false);
|
||||
const [loadingName, setLoadingName] = useState<string | null>(null);
|
||||
const { data: variables = [], isPending, error } = useQuery({
|
||||
queryKey: ["projectDevVariables", projectId],
|
||||
queryFn: () => listProjectDevVariables(projectId),
|
||||
@ -37,35 +32,7 @@ export function ProjectDevVariablesPanel({ projectId, projectName, onClose }: Pr
|
||||
setDraftValue("");
|
||||
setDraftUsername("");
|
||||
setDraftPassword("");
|
||||
setShowSecret(false);
|
||||
};
|
||||
// 已配置项进入修改时预填原值(密码默认遮罩);取回失败则提示并以空白表单继续。
|
||||
const startEdit = async (item: ProjectDevVariableStatus) => {
|
||||
clearDraft();
|
||||
if (item.configured) {
|
||||
setLoadingName(item.name);
|
||||
try {
|
||||
const original = await revealProjectDevVariable(projectId, item.name);
|
||||
if (original.kind === "test_account") {
|
||||
setDraftUsername(original.username);
|
||||
setDraftPassword(original.password);
|
||||
} else {
|
||||
setDraftValue(original.value);
|
||||
}
|
||||
} catch (cause) {
|
||||
showToast(`读取原值失败:${cause}`);
|
||||
} finally {
|
||||
setLoadingName(null);
|
||||
}
|
||||
}
|
||||
setEditingName(item.name);
|
||||
};
|
||||
const secretToggle = (
|
||||
<button type="button" onClick={() => setShowSecret((shown) => !shown)}
|
||||
className="shrink-0 px-2 text-xs text-gray-500 hover:text-gray-700">
|
||||
{showSecret ? "隐藏" : "显示"}
|
||||
</button>
|
||||
);
|
||||
const handleSave = async (name: string, kind: "scalar" | "test_account") => {
|
||||
if (saving || (kind === "test_account" ? !draftUsername || !draftPassword : !draftValue)) return;
|
||||
setSaving(true);
|
||||
@ -138,32 +105,26 @@ export function ProjectDevVariablesPanel({ projectId, projectName, onClose }: Pr
|
||||
aria-label={`${item.name} 测试账号`}
|
||||
className="w-full border border-gray-300 rounded px-2 py-1.5 text-sm"
|
||||
/>
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
type={showSecret ? "text" : "password"}
|
||||
autoComplete="off"
|
||||
value={draftPassword}
|
||||
onChange={(event) => setDraftPassword(event.target.value)}
|
||||
placeholder="测试密码"
|
||||
aria-label={`${item.name} 测试密码`}
|
||||
className="w-full border border-gray-300 rounded px-2 py-1.5 text-sm"
|
||||
/>
|
||||
{secretToggle}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
type={showSecret ? "text" : "password"}
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
value={draftValue}
|
||||
onChange={(event) => setDraftValue(event.target.value)}
|
||||
placeholder="填写开发值"
|
||||
value={draftPassword}
|
||||
onChange={(event) => setDraftPassword(event.target.value)}
|
||||
placeholder="测试密码"
|
||||
aria-label={`${item.name} 测试密码`}
|
||||
className="w-full border border-gray-300 rounded px-2 py-1.5 text-sm"
|
||||
/>
|
||||
{secretToggle}
|
||||
</div>
|
||||
) : (
|
||||
<input
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
value={draftValue}
|
||||
onChange={(event) => setDraftValue(event.target.value)}
|
||||
placeholder="填写开发值"
|
||||
className="w-full border border-gray-300 rounded px-2 py-1.5 text-sm"
|
||||
/>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<button type="submit" disabled={saving || (item.kind === "test_account" ? !draftUsername || !draftPassword : !draftValue)}
|
||||
@ -176,15 +137,13 @@ export function ProjectDevVariablesPanel({ projectId, projectName, onClose }: Pr
|
||||
</form>
|
||||
) : (
|
||||
<div className="flex gap-3 text-xs">
|
||||
<button onClick={() => void startEdit(item)} disabled={loadingName === item.name}
|
||||
className="text-blue-600 hover:underline disabled:opacity-50">
|
||||
{loadingName === item.name ? "读取中…" : item.configured ? "修改" : "填写"}
|
||||
</button>
|
||||
<button onClick={() => { clearDraft(); setEditingName(item.name); }}
|
||||
className="text-blue-600 hover:underline">{item.configured ? "修改" : "填写"}</button>
|
||||
{item.configured && (
|
||||
<button
|
||||
disabled={revokeMutation.isPending}
|
||||
onClick={async () => {
|
||||
if (await confirm(`撤销 ${item.name}?Agent 将无法再读取该值。`, { kind: "warning" })) {
|
||||
onClick={() => {
|
||||
if (window.confirm(`撤销 ${item.name}?Agent 将无法再读取该值。`)) {
|
||||
revokeMutation.mutate(item.name);
|
||||
}
|
||||
}}
|
||||
|
||||
@ -12,7 +12,6 @@ import {
|
||||
type RuntimeToolStatus,
|
||||
type ToolchainStatus,
|
||||
} from "../../lib/commands";
|
||||
import { confirm } from "@tauri-apps/plugin-dialog";
|
||||
import { useUIStore } from "../../store/ui";
|
||||
|
||||
// ── 层元信息 ───────────────────────────────────────────────────────────────────
|
||||
@ -114,9 +113,8 @@ export function ProjectEnvModal({ project, onClose }: Props) {
|
||||
};
|
||||
|
||||
const handlePrepareToolchain = async () => {
|
||||
const confirmed = await confirm(
|
||||
const confirmed = window.confirm(
|
||||
"炼境将通过 mise install 下载并准备项目已声明的 Node/pnpm 版本。不会修改 package.json 或 lockfile,是否继续?",
|
||||
{ kind: "warning" },
|
||||
);
|
||||
if (!confirmed) return;
|
||||
setPreparing(true);
|
||||
|
||||
@ -7,7 +7,6 @@ import {
|
||||
type GitRepo,
|
||||
type UpdateGitRepoDeployInput,
|
||||
} from "../../lib/commands";
|
||||
import { confirm } from "@tauri-apps/plugin-dialog";
|
||||
import { useUIStore } from "../../store/ui";
|
||||
|
||||
// ── 部署信息编辑表单 ──────────────────────────────────────────────────────────
|
||||
@ -128,7 +127,7 @@ export function RegistryTab() {
|
||||
});
|
||||
|
||||
const handleDelete = async (id: string, name: string) => {
|
||||
if (!(await confirm(`确定删除仓库「${name}」?关联的工作区也会被删除。`, { kind: "warning" }))) return;
|
||||
if (!confirm(`确定删除仓库「${name}」?关联的工作区也会被删除。`)) return;
|
||||
try {
|
||||
await deleteGitRepo(id);
|
||||
qc.invalidateQueries({ queryKey: ["git-repos"] });
|
||||
|
||||
@ -1431,14 +1431,6 @@ export const saveProjectTestAccount = (
|
||||
password: string,
|
||||
) => invoke<void>("save_project_test_account", { projectId, name, username, password });
|
||||
|
||||
/** 仅在人显式点「修改」时取回原值;不经 MCP。 */
|
||||
export type ProjectDevVariableReveal =
|
||||
| { kind: "scalar"; value: string }
|
||||
| { kind: "test_account"; username: string; password: string };
|
||||
|
||||
export const revealProjectDevVariable = (projectId: string, name: string) =>
|
||||
invoke<ProjectDevVariableReveal>("reveal_project_dev_variable", { projectId, name });
|
||||
|
||||
export const revokeProjectDevVariable = (projectId: string, name: string) =>
|
||||
invoke<void>("revoke_project_dev_variable", { projectId, name });
|
||||
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
// tauri-plugin-dialog 把 window.confirm 替换成 async 函数:同步写法拿到的是永远为真的 Promise,
|
||||
// 确认框形同虚设(2026-09-30 撤销/删除未弹框即执行)。破坏性操作必须 await plugin-dialog 的 confirm。
|
||||
const sources = import.meta.glob<string>(["../**/*.{ts,tsx}", "!../**/*.test.ts"], {
|
||||
query: "?raw",
|
||||
import: "default",
|
||||
eager: true,
|
||||
});
|
||||
|
||||
describe("confirm usage", () => {
|
||||
it("scans the frontend sources", () => {
|
||||
expect(Object.keys(sources).length).toBeGreaterThan(10);
|
||||
});
|
||||
|
||||
it("never calls confirm without await", () => {
|
||||
const offenders = Object.entries(sources).flatMap(([file, text]) =>
|
||||
text
|
||||
.split("\n")
|
||||
.map((line, index) => ({ line, at: `${file}:${index + 1}` }))
|
||||
.filter(({ line }) => /(^|[^\w.])(window\.)?confirm\(/.test(line) && !/await\s+(window\.)?confirm\(/.test(line))
|
||||
.map(({ at }) => at),
|
||||
);
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user