feat(submitter,update-center): add multiple features and optimize submission flow

1. add shell caller ssupdate command support
2. add pre-update system refresh for update center
3. add get-git-email IPC handler and auto-fill email
4. optimize deb submission UI and category handling
5. disable and auto-fill package name/version fields
6. add loading and error states for category list
7. fix category value matching for history data
This commit is contained in:
2026-07-13 22:07:01 +08:00
parent 0aca5744b0
commit 7c6bdf0e55
5 changed files with 256 additions and 152 deletions
+18
View File
@@ -911,6 +911,24 @@ export function registerSubmitterHandlers(
} }
}); });
ipcMain.handle("get-git-email", async () => {
try {
const { exec } = await import("node:child_process");
const util = await import("util");
const execAsync = util.promisify(exec);
const { stdout } = await execAsync("git config user.email");
const email = stdout.trim();
logger.info({ email }, "[Submitter] Git email retrieved");
return { success: true, data: email || "" };
} catch (err) {
logger.warn(
{ err },
"[Submitter] Failed to get git email, not a git repo or git not installed",
);
return { success: false, data: "" };
}
});
ipcMain.handle("get-tags-list", async () => { ipcMain.handle("get-tags-list", async () => {
try { try {
const apiUrl = "https://upload.deepinos.org.cn/api/index/get_tags_list"; const apiUrl = "https://upload.deepinos.org.cn/api/index/get_tags_list";
@@ -2,6 +2,8 @@ import { spawn } from "node:child_process";
import { BrowserWindow, ipcMain } from "electron"; import { BrowserWindow, ipcMain } from "electron";
import { SHELL_CALLER_PATH } from "../shared-installer";
import { findExecutable, SUPER_USER_COMMAND_CANDIDATES } from "../superuser";
import { import {
buildInstalledSourceMap, buildInstalledSourceMap,
mergeUpdateSources, mergeUpdateSources,
@@ -524,6 +526,75 @@ export const registerUpdateCenterIpc = (
| "subscribe" | "subscribe"
>, >,
): void => { ): void => {
ipc.handle(
"update-center-run-system-update",
async (_event, storeFilter: StoreFilter = "both") => {
console.log(
`[UpdateCenter] update-center-run-system-update called with storeFilter=${storeFilter}`,
);
const results: { aptss?: string; apm?: string } = {};
const runCommand = (command: string, args: string[]): Promise<{ code: number; stdout: string; stderr: string }> =>
new Promise((resolve) => {
const child = spawn(command, args, { shell: false, env: process.env });
let stdout = "";
let stderr = "";
child.stdout?.on("data", (data) => { stdout += data.toString(); });
child.stderr?.on("data", (data) => { stderr += data.toString(); });
child.on("error", (err) => resolve({ code: -1, stdout, stderr: err.message }));
child.on("close", (code) => resolve({ code: code ?? -1, stdout, stderr }));
});
const isSourceEnabled = (
filter: StoreFilter,
source: "spark" | "apm",
): boolean => filter === "both" || filter === source;
// aptss update — 需要提权
if (isSourceEnabled(storeFilter, "spark")) {
const whichResult = await runCommand("which", ["aptss"]);
const aptssAvailable = whichResult.code === 0 && whichResult.stdout.trim().length > 0;
if (aptssAvailable) {
console.log("[UpdateCenter] Running: pkexec shell-caller aptss ssupdate");
const superUserCmd = await findExecutable(SUPER_USER_COMMAND_CANDIDATES[0]);
if (superUserCmd) {
const result = await runCommand(superUserCmd, [SHELL_CALLER_PATH, "aptss", "ssupdate"]);
results.aptss = result.code === 0 ? "ok" : `failed: ${result.stderr.substring(0, 200)}`;
console.log("[UpdateCenter] aptss ssupdate result:", results.aptss);
} else {
results.aptss = "failed: pkexec not found";
console.warn("[UpdateCenter] pkexec not found, skipping aptss update");
}
} else {
results.aptss = "skipped: aptss not installed";
}
}
// apm update — 也需要提权
if (isSourceEnabled(storeFilter, "apm")) {
const whichResult = await runCommand("which", ["apm"]);
const apmAvailable = whichResult.code === 0 && whichResult.stdout.trim().length > 0;
if (apmAvailable) {
console.log("[UpdateCenter] Running: pkexec shell-caller apm update");
const superUserCmd = await findExecutable(SUPER_USER_COMMAND_CANDIDATES[0]);
if (superUserCmd) {
const result = await runCommand(superUserCmd, [SHELL_CALLER_PATH, "apm", "update"]);
results.apm = result.code === 0 ? "ok" : `failed: ${result.stderr.substring(0, 200)}`;
console.log("[UpdateCenter] apm update result:", results.apm);
} else {
results.apm = "failed: pkexec not found";
console.warn("[UpdateCenter] pkexec not found, skipping apm update");
}
} else {
results.apm = "skipped: apm not installed";
}
}
return results;
},
);
ipc.handle( ipc.handle(
"update-center-open", "update-center-open",
(_event, storeFilter: StoreFilter = "both") => service.open(storeFilter), (_event, storeFilter: StoreFilter = "both") => service.open(storeFilter),
+4 -1
View File
@@ -155,7 +155,10 @@ case "$command_type" in
echo "操作已取消" echo "操作已取消"
exit 0 exit 0
fi fi
elif [[ "$2" == "ssupdate" ]]; then
/usr/bin/aptss "${@:2}" -y 2>&1
exit_code=$?
exit $?
else else
# 非 remove/install 命令,拒绝执行 # 非 remove/install 命令,拒绝执行
echo "拒绝执行 aptss 白名单外的指令" echo "拒绝执行 aptss 白名单外的指令"
+138 -128
View File
@@ -46,12 +46,12 @@
class="hidden" class="hidden"
@change="handleDebFileSelect" @change="handleDebFileSelect"
/> />
<div v-if="isParsingDeb" class="flex flex-col items-center"> <div v-if="isParsingDeb || isSearchingHistory" class="flex flex-col items-center">
<div <div
class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mb-4" class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mb-4"
></div> ></div>
<p class="text-slate-600 dark:text-slate-400"> <p class="text-slate-600 dark:text-slate-400">
正在解析 deb 文件... {{ isParsingDeb ? '正在解析 deb 文件...' : '正在从服务器查询已上架信息...' }}
</p> </p>
</div> </div>
<div v-else> <div v-else>
@@ -93,8 +93,9 @@
<input <input
v-model="formData.pkgname" v-model="formData.pkgname"
type="text" type="text"
placeholder="唯一标识符" disabled
class="w-full px-4 py-2.5 rounded-lg border border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" title="包名由 deb 文件自动解析,不可手动修改"
class="w-full px-4 py-2.5 rounded-lg border border-slate-200 bg-slate-100 dark:bg-slate-700 dark:border-slate-600 text-slate-500 dark:text-slate-400 cursor-not-allowed"
/> />
</div> </div>
</div> </div>
@@ -108,8 +109,9 @@
<input <input
v-model="formData.version" v-model="formData.version"
type="text" type="text"
placeholder="如 1.0.0" disabled
class="w-full px-4 py-2.5 rounded-lg border border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" title="版本号由 deb 文件自动解析,不可手动修改"
class="w-full px-4 py-2.5 rounded-lg border border-slate-200 bg-slate-100 dark:bg-slate-700 dark:border-slate-600 text-slate-500 dark:text-slate-400 cursor-not-allowed"
/> />
</div> </div>
<div> <div>
@@ -302,14 +304,27 @@
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
>分类</label >分类</label
> >
<div v-if="isLoadingCategories" class="text-sm text-slate-400 py-2">
正在加载分类列表...
</div>
<div
v-else-if="categoriesLoadError"
class="p-3 bg-yellow-50 border border-yellow-200 rounded-lg dark:bg-yellow-900/20 dark:border-yellow-800"
>
<p class="text-yellow-700 dark:text-yellow-400 text-sm">
分类加载失败: {{ categoriesLoadError }}
</p>
</div>
<select <select
v-else
v-model="formData.category" v-model="formData.category"
class="w-full px-4 py-2.5 rounded-lg border border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" class="w-full px-4 py-2.5 rounded-lg border border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
> >
<option value="" disabled>请选择分类</option>
<option <option
v-for="category in categoriesList" v-for="category in categoriesList"
:key="category.id" :key="category.id"
:value="category.name" :value="category.value"
> >
{{ category.name }} {{ category.name }}
</option> </option>
@@ -581,11 +596,15 @@ const submitSuccess = ref(false);
const submitError = ref(""); const submitError = ref("");
const isParsingDeb = ref(false); const isParsingDeb = ref(false);
const debParseError = ref(""); const debParseError = ref("");
const isSearchingHistory = ref(false);
const showArchDialog = ref(false); const showArchDialog = ref(false);
const availableArchs = ref<HistoryArchInfo[]>([]); const availableArchs = ref<HistoryArchInfo[]>([]);
const currentDebArch = ref(""); const currentDebArch = ref("");
const iconPreview = ref(""); const iconPreview = ref("");
const isLoadingCategories = ref(false);
const categoriesLoadError = ref("");
const isPackaging = ref(false); const isPackaging = ref(false);
const packageSuccess = ref(false); const packageSuccess = ref(false);
const packageError = ref(""); const packageError = ref("");
@@ -605,8 +624,26 @@ const packArchOptions = [
interface Category { interface Category {
id: number; id: number;
name: string; name: string;
value: string;
} }
// 服务器分类 id → 英文标识符映射(与后端 submitter.ts 的 categoryNameToIdMap 反向)
// 历史记录中 category 字段存储的就是这些英文标识符
const categoryIdToValueMap: Record<number, string> = {
1: "games",
2: "music",
3: "network",
4: "office",
5: "others",
6: "image_graphics",
7: "development",
8: "reading",
9: "chat",
10: "themes",
11: "tools",
12: "video",
};
interface Tag { interface Tag {
name: string; name: string;
value: string; value: string;
@@ -622,6 +659,7 @@ const isFormValid = computed(() => {
formData.name.trim() && formData.name.trim() &&
formData.pkgname.trim() && formData.pkgname.trim() &&
formData.version.trim() && formData.version.trim() &&
formData.category.trim() &&
formData.debFilePath formData.debFilePath
); );
}); });
@@ -635,147 +673,75 @@ const getArchDisplayName = (store: string): string => {
return archMap[store] || store; return archMap[store] || store;
}; };
const loadCategoriesList = async () => { const loadCategoriesPromise = ref<Promise<void> | null>(null);
const loadCategoriesList = async (): Promise<void> => {
// 防止并发调用
if (isLoadingCategories.value) {
if (loadCategoriesPromise.value) {
return loadCategoriesPromise.value;
}
return;
}
isLoadingCategories.value = true;
categoriesLoadError.value = "";
const promise = (async () => {
console.log( console.log(
"[Submitter] ============== LOAD CATEGORIES START ==============", "[Submitter] ============== LOAD CATEGORIES START ==============",
); );
console.log("[Submitter] Calling IPC: get-category-list");
try { try {
const startTime = Date.now();
const result = await window.ipcRenderer.invoke("get-category-list"); const result = await window.ipcRenderer.invoke("get-category-list");
const endTime = Date.now();
console.log(
"[Submitter] ============== IPC RESPONSE RECEIVED ==============",
);
console.log("[Submitter] Request duration:", endTime - startTime, "ms");
console.log("[Submitter] Result success:", result?.success);
console.log("[Submitter] Result message:", result?.message);
console.log("[Submitter] Full result:", JSON.stringify(result, null, 2));
if (result?.success && result.data) { if (result?.success && result.data) {
const data = result.data; const data = result.data;
console.log(
"[Submitter] ============== PROCESSING RESPONSE ==============",
);
console.log("[Submitter] Response code:", data.code);
console.log("[Submitter] Response message:", data.msg);
console.log("[Submitter] Data type:", typeof data.data);
console.log("[Submitter] Data length:", data.data?.length);
console.log("[Submitter] Raw data:", JSON.stringify(data.data, null, 2));
if (data.code === 0 && data.data) { if (data.code === 0 && Array.isArray(data.data) && data.data.length > 0) {
categoriesList.value = data.data.map( categoriesList.value = data.data.map(
( (item: { id: number; name: string }, index: number) => ({
item: { id: number; name: string; value: string },
index: number,
) => ({
id: typeof item.id === "number" ? item.id : index + 1, id: typeof item.id === "number" ? item.id : index + 1,
name: item.name || item.value || "", // API 只返回中文 namevalue 通过 id 反向映射为英文标识符
// 英文标识符与历史记录中的 category 字段一致,用于匹配
name: item.name || "",
value: categoryIdToValueMap[item.id] || item.name || "",
}), }),
); );
console.log( categoriesLoadError.value = "";
"[Submitter] ============== CATEGORIES LOADED ==============", console.log("[Submitter] Categories loaded from API:", categoriesList.value);
);
console.log("[Submitter] Categories list:", categoriesList.value);
console.log(
"[Submitter] Categories count:",
categoriesList.value.length,
);
} else if (data.code === 0 && Array.isArray(data)) { } else if (data.code === 0 && Array.isArray(data)) {
categoriesList.value = data.map( categoriesList.value = data.map(
( (item: { id: number; name: string }, index: number) => ({
item: { id: number; name: string; value: string },
index: number,
) => ({
id: typeof item.id === "number" ? item.id : index + 1, id: typeof item.id === "number" ? item.id : index + 1,
name: item.name || item.value || "", name: item.name || "",
value: categoryIdToValueMap[item.id] || item.name || "",
}), }),
); );
console.log( categoriesLoadError.value = "";
"[Submitter] ============== CATEGORIES LOADED (direct array) ==============", console.log("[Submitter] Categories loaded from API (direct array):", categoriesList.value);
);
console.log("[Submitter] Categories list:", categoriesList.value);
console.log(
"[Submitter] Categories count:",
categoriesList.value.length,
);
} else { } else {
console.error( const errMsg = `服务器返回异常: code=${data.code}, msg=${data.msg || "未知"}`;
"[Submitter] ============== INVALID RESPONSE CODE ==============", categoriesLoadError.value = errMsg;
); console.error("[Submitter]", errMsg);
console.error("[Submitter] Expected code 200, got:", data.code);
console.error("[Submitter] Response message:", data.msg);
categoriesList.value = [
{ id: 1, name: "chat" },
{ id: 2, name: "development" },
{ id: 3, name: "games" },
{ id: 4, name: "image_graphics" },
{ id: 5, name: "music" },
{ id: 6, name: "network" },
{ id: 7, name: "office" },
{ id: 8, name: "others" },
{ id: 9, name: "reading" },
{ id: 10, name: "themes" },
{ id: 11, name: "tools" },
{ id: 12, name: "video" },
];
console.log(
"[Submitter] ============== USING FALLBACK CATEGORIES ==============",
);
console.log("[Submitter] Categories list:", categoriesList.value);
} }
} else { } else {
console.error( const errMsg = result?.message || "获取分类列表失败";
"[Submitter] ============== IPC CALL FAILED ==============", categoriesLoadError.value = errMsg;
); console.error("[Submitter] IPC failed:", errMsg);
console.error("[Submitter] Success:", result?.success);
console.error("[Submitter] Message:", result?.message);
console.error("[Submitter] Data:", result?.data);
categoriesList.value = [
{ id: 1, name: "chat" },
{ id: 2, name: "development" },
{ id: 3, name: "games" },
{ id: 4, name: "image_graphics" },
{ id: 5, name: "music" },
{ id: 6, name: "network" },
{ id: 7, name: "office" },
{ id: 8, name: "others" },
{ id: 9, name: "reading" },
{ id: 10, name: "themes" },
{ id: 11, name: "tools" },
{ id: 12, name: "video" },
];
console.log(
"[Submitter] ============== USING FALLBACK CATEGORIES ==============",
);
console.log("[Submitter] Categories list:", categoriesList.value);
} }
} catch (error) { } catch (error) {
console.error("[Submitter] ============== EXCEPTION CAUGHT =============="); const errMsg = (error as Error)?.message || "获取分类列表异常";
console.error("[Submitter] Error type:", (error as Error)?.name); categoriesLoadError.value = errMsg;
console.error("[Submitter] Error message:", (error as Error)?.message); console.error("[Submitter] Exception:", errMsg);
console.error("[Submitter] Error stack:", (error as Error)?.stack); } finally {
categoriesList.value = [ isLoadingCategories.value = false;
{ id: 1, name: "chat" }, loadCategoriesPromise.value = null;
{ id: 2, name: "development" },
{ id: 3, name: "games" },
{ id: 4, name: "image_graphics" },
{ id: 5, name: "music" },
{ id: 6, name: "network" },
{ id: 7, name: "office" },
{ id: 8, name: "others" },
{ id: 9, name: "reading" },
{ id: 10, name: "themes" },
{ id: 11, name: "tools" },
{ id: 12, name: "video" },
];
console.log(
"[Submitter] ============== USING FALLBACK CATEGORIES ==============",
);
console.log("[Submitter] Categories list:", categoriesList.value);
} }
})();
loadCategoriesPromise.value = promise;
return promise;
}; };
const loadTagsList = async () => { const loadTagsList = async () => {
@@ -925,7 +891,15 @@ const searchHistoryApp = async () => {
showArchDialog.value, showArchDialog.value,
); );
availableArchs.value = historyResult.data; // 按 amd64 → arm64 → loong64 顺序排序
const archOrder: Record<string, number> = {
store: 0,
"aarch64-store": 1,
"loong64-store": 2,
};
availableArchs.value = [...historyResult.data].sort(
(a, b) => (archOrder[a.store] ?? 99) - (archOrder[b.store] ?? 99),
);
console.log( console.log(
"[Submitter] availableArchs after:", "[Submitter] availableArchs after:",
availableArchs.value, availableArchs.value,
@@ -935,6 +909,18 @@ const searchHistoryApp = async () => {
availableArchs.value.length, availableArchs.value.length,
); );
// 确保分类列表已加载,避免 select 无法回显
if (categoriesList.value.length === 0) {
await loadCategoriesList();
}
// 从第一条历史记录预填名称和分类
const firstArch = historyResult.data[0];
if (firstArch) {
formData.name = firstArch.name || formData.name;
formData.category = firstArch.category || formData.category;
}
showArchDialog.value = true; showArchDialog.value = true;
console.log( console.log(
"[Submitter] showArchDialog after:", "[Submitter] showArchDialog after:",
@@ -1054,7 +1040,12 @@ const parseDebFileAndSearchHistory = async (debPath: string) => {
); );
if (formData.pkgname) { if (formData.pkgname) {
isSearchingHistory.value = true;
try {
await searchHistoryApp(); await searchHistoryApp();
} finally {
isSearchingHistory.value = false;
}
} }
} else { } else {
console.error("[Submitter] Failed to parse deb file"); console.error("[Submitter] Failed to parse deb file");
@@ -1145,10 +1136,15 @@ const handleDrop = async (event: DragEvent) => {
} }
}; };
const selectArch = (arch: HistoryArchInfo) => { const selectArch = async (arch: HistoryArchInfo) => {
showArchDialog.value = false; showArchDialog.value = false;
console.log("[Submitter] selectArch called with:", arch); console.log("[Submitter] selectArch called with:", arch);
// 确保分类列表已加载,避免 select 无法回显
if (categoriesList.value.length === 0) {
await loadCategoriesList();
}
formData.name = arch.name || formData.name; formData.name = arch.name || formData.name;
formData.author = arch.author || formData.author; formData.author = arch.author || formData.author;
formData.contributor = arch.contributor || formData.contributor; formData.contributor = arch.contributor || formData.contributor;
@@ -1401,9 +1397,23 @@ const closeWindow = () => {
import { onMounted, nextTick } from "vue"; import { onMounted, nextTick } from "vue";
onMounted(() => { const getGitEmail = async () => {
try {
const result = await window.ipcRenderer.invoke("get-git-email");
if (result?.success && result.data) {
formData.mail = result.data;
console.log("[Submitter] Git email auto-filled:", result.data);
}
} catch (err) {
console.warn("[Submitter] Failed to get git email:", err);
}
};
onMounted(async () => {
console.log("[Submitter] Component mounted, loading categories and tags"); console.log("[Submitter] Component mounted, loading categories and tags");
loadCategoriesList(); // 先等待分类列表加载完成,避免后续竞态
loadTagsList(); await Promise.all([loadCategoriesList(), loadTagsList()]);
// 尝试从 git 配置读取邮箱
await getGitEmail();
}); });
</script> </script>
+2
View File
@@ -152,6 +152,8 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
lastStoreFilter = storeFilter; lastStoreFilter = storeFilter;
loading.value = true; loading.value = true;
try { try {
// 先运行系统更新(aptss update / apm update),确保本地包信息最新
await window.ipcRenderer.invoke("update-center-run-system-update", storeFilter);
const nextSnapshot = await window.updateCenter.refresh(storeFilter); const nextSnapshot = await window.updateCenter.refresh(storeFilter);
applySnapshot(nextSnapshot); applySnapshot(nextSnapshot);
} finally { } finally {