From b1c6eaebac30d91022b93cb9144cd093fe2672c6 Mon Sep 17 00:00:00 2001 From: lanrtop Date: Sat, 26 Sep 2026 16:47:24 +0900 Subject: [PATCH] feat(resource/dev-variables): add per-project human filling panel Expose status-only Tauri types and explicit save/revoke actions without caching entered values in React Query. Feature-Confirmed: true Rules-Applied: R01,R12,R13 --- src/components/dashboard/ProjectCard.tsx | 11 ++ .../dashboard/ProjectDevVariablesPanel.tsx | 119 ++++++++++++++++++ src/lib/commands.ts | 19 +++ 3 files changed, 149 insertions(+) create mode 100644 src/components/dashboard/ProjectDevVariablesPanel.tsx diff --git a/src/components/dashboard/ProjectCard.tsx b/src/components/dashboard/ProjectCard.tsx index 9d29d6d..3636503 100644 --- a/src/components/dashboard/ProjectCard.tsx +++ b/src/components/dashboard/ProjectCard.tsx @@ -12,6 +12,7 @@ import { MentorPanel } from "../mentor/MentorPanel"; import { UserTodoPanel } from "./UserTodoPanel"; import { ProjectServerPanel } from "./ProjectServerPanel"; import { ClaudeConfigModal } from "./ClaudeConfigModal"; +import { ProjectDevVariablesPanel } from "./ProjectDevVariablesPanel"; // ── 蓝图状态徽章 ────────────────────────────────────────────────────────────── @@ -66,6 +67,7 @@ export function ProjectCard({ project: p }: Props) { const [showBlueprintPrompt, setShowBlueprintPrompt] = useState(false); const [showMentor, setShowMentor] = useState(false); const [showClaudeConfig, setShowClaudeConfig] = useState(false); + const [showDevVariables, setShowDevVariables] = useState(false); const [showTodos, setShowTodos] = useState(false); const [showServers, setShowServers] = useState(false); const [showBranchMenu, setShowBranchMenu] = useState(false); @@ -613,6 +615,7 @@ export function ProjectCard({ project: p }: Props) {
{([ { icon: "🤖", label: "Claude 自主性", action: () => setShowClaudeConfig(true) }, + { icon: "🔑", label: "开发变量", action: () => setShowDevVariables(true) }, { icon: "📝", label: showTodos ? "收起备忘" : "个人备忘", action: () => setShowTodos((v) => !v) }, { icon: "🖥️", label: showServers ? "收起服务器" : "关联服务器", action: () => setShowServers((v) => !v) }, { icon: "🚀", label: "CI/CD 控制台", action: () => setShowCicd(true) }, @@ -705,6 +708,14 @@ export function ProjectCard({ project: p }: Props) { /> )} + {showDevVariables && ( + setShowDevVariables(false)} + /> + )} + {/* 个人备忘 */} {showTodos && } diff --git a/src/components/dashboard/ProjectDevVariablesPanel.tsx b/src/components/dashboard/ProjectDevVariablesPanel.tsx new file mode 100644 index 0000000..5056397 --- /dev/null +++ b/src/components/dashboard/ProjectDevVariablesPanel.tsx @@ -0,0 +1,119 @@ +import { useState } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { + listProjectDevVariables, + saveProjectDevVariable, + revokeProjectDevVariable, +} from "../../lib/commands"; +import { useUIStore } from "../../store/ui"; +import { Dialog } from "../ui/Dialog"; + +interface Props { + projectId: string; + projectName: string; + onClose: () => void; +} + +export function ProjectDevVariablesPanel({ projectId, projectName, onClose }: Props) { + const queryClient = useQueryClient(); + const showToast = useUIStore((state) => state.showToast); + const [editingName, setEditingName] = useState(null); + const [draftValue, setDraftValue] = useState(""); + const [saving, setSaving] = useState(false); + const { data: variables = [], isPending, error } = useQuery({ + queryKey: ["projectDevVariables", projectId], + queryFn: () => listProjectDevVariables(projectId), + retry: false, + }); + const handleSave = async (name: string) => { + if (!draftValue || saving) return; + setSaving(true); + try { + await saveProjectDevVariable(projectId, name, draftValue); + setDraftValue(""); + setEditingName(null); + queryClient.invalidateQueries({ queryKey: ["projectDevVariables", projectId] }); + showToast("开发变量已保存"); + } catch (cause) { + showToast(`保存失败:${cause}`); + } finally { + setSaving(false); + } + }; + const revokeMutation = useMutation({ + mutationFn: (name: string) => revokeProjectDevVariable(projectId, name), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["projectDevVariables", projectId] }); + showToast("开发变量已撤销"); + }, + onError: (cause) => showToast(`撤销失败:${cause}`), + }); + + return ( + +
+

+ 仅填写非生产、低权限、可重置的开发值。已配置的值可被此项目的已认证 Agent 单项读取, + 不会写入子项目文件;请勿存放生产密码。 +

+ {isPending &&

正在读取需求…

} + {error &&

读取失败:{String(error)}

} + {!isPending && !error && variables.length === 0 && ( +

暂无变量需求。Agent 登记需要的名称和用途后,会显示在这里等待填写。

+ )} + {variables.map((item) => ( +
+
+ {item.name} + + {item.configured ? "已配置" : "待填写"} + +
+

{item.purpose}

+ {editingName === item.name ? ( +
{ + event.preventDefault(); + void handleSave(item.name); + }} + className="flex flex-wrap gap-2" + > + setDraftValue(event.target.value)} + placeholder="填写开发值" + className="flex-1 min-w-44 border border-gray-300 rounded px-2 py-1.5 text-sm" + /> + + +
+ ) : ( +
+ + {item.configured && ( + + )} +
+ )} +
+ ))} +
+
+ ); +} diff --git a/src/lib/commands.ts b/src/lib/commands.ts index 9aeb4ba..f2a1e8e 100644 --- a/src/lib/commands.ts +++ b/src/lib/commands.ts @@ -1405,6 +1405,25 @@ export const deleteApiKey = (id: string) => export const revealApiKey = (id: string) => invoke("reveal_api_key", { id }); +// ── Project development variables ────────────────────────────────────────── + +export interface ProjectDevVariableStatus { + project_id: string; + name: string; + purpose: string; + configured: boolean; + updated_at: string; +} + +export const listProjectDevVariables = (projectId: string) => + invoke("list_project_dev_variables", { projectId }); + +export const saveProjectDevVariable = (projectId: string, name: string, value: string) => + invoke("save_project_dev_variable", { projectId, name, value }); + +export const revokeProjectDevVariable = (projectId: string, name: string) => + invoke("revoke_project_dev_variable", { projectId, name }); + // ── SSH Config & VS Code Remote ────────────────────────────────────────────── export interface SshConfigStatus {