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 76 additions and 24 deletions
Showing only changes of commit d926591ace - Show all commits

View File

@ -405,6 +405,7 @@ pub fn run() {
// project development variables (human filling; no reveal command)
commands::dev_variables::list_project_dev_variables,
commands::dev_variables::save_project_dev_variable,
commands::dev_variables::save_project_test_account,
commands::dev_variables::revoke_project_dev_variable,
// host apps
commands::host_apps::get_host_apps,

View File

@ -3,6 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
listProjectDevVariables,
saveProjectDevVariable,
saveProjectTestAccount,
revokeProjectDevVariable,
} from "../../lib/commands";
import { useUIStore } from "../../store/ui";
@ -19,21 +20,32 @@ export function ProjectDevVariablesPanel({ projectId, projectName, onClose }: Pr
const showToast = useUIStore((state) => state.showToast);
const [editingName, setEditingName] = useState<string | null>(null);
const [draftValue, setDraftValue] = useState("");
const [draftUsername, setDraftUsername] = useState("");
const [draftPassword, setDraftPassword] = 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;
const clearDraft = () => {
setDraftValue("");
setDraftUsername("");
setDraftPassword("");
};
const handleSave = async (name: string, kind: "scalar" | "test_account") => {
if (saving || (kind === "test_account" ? !draftUsername || !draftPassword : !draftValue)) return;
setSaving(true);
try {
await saveProjectDevVariable(projectId, name, draftValue);
setDraftValue("");
if (kind === "test_account") {
await saveProjectTestAccount(projectId, name, draftUsername, draftPassword);
} else {
await saveProjectDevVariable(projectId, name, draftValue);
}
clearDraft();
setEditingName(null);
queryClient.invalidateQueries({ queryKey: ["projectDevVariables", projectId] });
showToast("开发变量已保存");
showToast(kind === "test_account" ? "测试账户已保存" : "开发变量已保存");
} catch (cause) {
showToast(`保存失败:${cause}`);
} finally {
@ -53,7 +65,7 @@ export function ProjectDevVariablesPanel({ projectId, projectName, onClose }: Pr
<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 单项读取,
仅填写非生产、低权限、可重置的开发值或测试账户。已配置的值可被此项目的已认证 Agent 单项读取,
不会写入子项目文件;请勿存放生产密码。
</p>
{isPending && <p className="text-gray-500">正在读取需求…</p>}
@ -69,34 +81,63 @@ export function ProjectDevVariablesPanel({ projectId, projectName, onClose }: Pr
{item.configured ? "已配置" : "待填写"}
</span>
</div>
{item.kind === "test_account" && (
<p className="text-xs text-gray-600">测试账户 · {item.service} · {item.environment}</p>
)}
<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);
void handleSave(item.name, item.kind);
}}
className="flex flex-wrap gap-2"
className="space-y-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>
{item.kind === "test_account" ? (
<div className="grid gap-2">
<input
type="text"
autoComplete="off"
autoFocus
value={draftUsername}
onChange={(event) => setDraftUsername(event.target.value)}
placeholder="测试账号"
aria-label={`${item.name} 测试账号`}
className="w-full border border-gray-300 rounded px-2 py-1.5 text-sm"
/>
<input
type="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"
/>
</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)}
className="px-3 py-1.5 bg-blue-600 text-white rounded disabled:opacity-50">
{saving ? "保存中…" : "保存"}
</button>
<button type="button" onClick={() => { clearDraft(); setEditingName(null); }}
className="px-2 py-1.5 text-gray-500">取消</button>
</div>
</form>
) : (
<div className="flex gap-3 text-xs">
<button onClick={() => { setDraftValue(""); setEditingName(item.name); }}
<button onClick={() => { clearDraft(); setEditingName(item.name); }}
className="text-blue-600 hover:underline">{item.configured ? "修改" : "填写"}</button>
{item.configured && (
<button

View File

@ -1411,6 +1411,9 @@ export interface ProjectDevVariableStatus {
project_id: string;
name: string;
purpose: string;
kind: "scalar" | "test_account";
service: string | null;
environment: string | null;
configured: boolean;
updated_at: string;
}
@ -1421,6 +1424,13 @@ export const listProjectDevVariables = (projectId: string) =>
export const saveProjectDevVariable = (projectId: string, name: string, value: string) =>
invoke<void>("save_project_dev_variable", { projectId, name, value });
export const saveProjectTestAccount = (
projectId: string,
name: string,
username: string,
password: string,
) => invoke<void>("save_project_test_account", { projectId, name, username, password });
export const revokeProjectDevVariable = (projectId: string, name: string) =>
invoke<void>("revoke_project_dev_variable", { projectId, name });