feat(dev-variables): 项目开发变量与测试账户补给(V1–V11) #23

Merged
lanrtop merged 15 commits from feat/project-dev-variables into master 2026-09-30 13:35:23 +08:00
3 changed files with 149 additions and 0 deletions
Showing only changes of commit b1c6eaebac - Show all commits

View File

@ -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) {
<div className="absolute right-0 bottom-full mb-1 w-40 bg-white border border-gray-200 rounded-lg shadow-lg z-50 py-1 text-xs">
{([
{ 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 && (
<ProjectDevVariablesPanel
projectId={p.id}
projectName={p.name}
onClose={() => setShowDevVariables(false)}
/>
)}
{/* 个人备忘 */}
{showTodos && <UserTodoPanel projectId={p.id} />}

View File

@ -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<string | null>(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 (
<Dialog title={`开发变量 · ${projectName}`} onClose={onClose} size="md">
<div className="overflow-y-auto p-5 space-y-4 text-sm">
<p className="text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded-lg p-3">
仅填写非生产、低权限、可重置的开发值。已配置的值可被此项目的已认证 Agent 单项读取,
不会写入子项目文件;请勿存放生产密码。
</p>
{isPending && <p className="text-gray-500">正在读取需求…</p>}
{error && <p className="text-red-600">读取失败:{String(error)}</p>}
{!isPending && !error && variables.length === 0 && (
<p className="text-gray-500">暂无变量需求。Agent 登记需要的名称和用途后,会显示在这里等待填写。</p>
)}
{variables.map((item) => (
<div key={item.name} className="border border-gray-200 rounded-lg p-3 space-y-2">
<div className="flex items-center gap-2">
<code className="font-semibold text-gray-800 break-all flex-1">{item.name}</code>
<span className={`text-xs px-2 py-0.5 rounded-full ${item.configured ? "bg-green-50 text-green-700" : "bg-amber-50 text-amber-700"}`}>
{item.configured ? "已配置" : "待填写"}
</span>
</div>
<p className="text-xs text-gray-500 whitespace-pre-wrap">{item.purpose}</p>
{editingName === item.name ? (
<form
onSubmit={(event) => {
event.preventDefault();
void handleSave(item.name);
}}
className="flex flex-wrap gap-2"
>
<input
type="password"
autoComplete="off"
autoFocus
value={draftValue}
onChange={(event) => setDraftValue(event.target.value)}
placeholder="填写开发值"
className="flex-1 min-w-44 border border-gray-300 rounded px-2 py-1.5 text-sm"
/>
<button type="submit" disabled={!draftValue || saving}
className="px-3 py-1.5 bg-blue-600 text-white rounded disabled:opacity-50">
{saving ? "保存中…" : "保存"}
</button>
<button type="button" onClick={() => { setDraftValue(""); setEditingName(null); }}
className="px-2 py-1.5 text-gray-500">取消</button>
</form>
) : (
<div className="flex gap-3 text-xs">
<button onClick={() => { setDraftValue(""); setEditingName(item.name); }}
className="text-blue-600 hover:underline">{item.configured ? "修改" : "填写"}</button>
{item.configured && (
<button
disabled={revokeMutation.isPending}
onClick={() => {
if (window.confirm(`撤销 ${item.name}?Agent 将无法再读取该值。`)) {
revokeMutation.mutate(item.name);
}
}}
className="text-red-600 hover:underline disabled:opacity-50"
>撤销</button>
)}
</div>
)}
</div>
))}
</div>
</Dialog>
);
}

View File

@ -1405,6 +1405,25 @@ export const deleteApiKey = (id: string) =>
export const revealApiKey = (id: string) =>
invoke<string>("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<ProjectDevVariableStatus[]>("list_project_dev_variables", { projectId });
export const saveProjectDevVariable = (projectId: string, name: string, value: string) =>
invoke<void>("save_project_dev_variable", { projectId, name, value });
export const revokeProjectDevVariable = (projectId: string, name: string) =>
invoke<void>("revoke_project_dev_variable", { projectId, name });
// ── SSH Config & VS Code Remote ──────────────────────────────────────────────
export interface SshConfigStatus {