Compare commits

..

11 Commits

9 changed files with 370 additions and 300 deletions
-1
View File
@@ -35,7 +35,6 @@ yarn-error.log*
coverage coverage
# Docker # Docker
docker-compose*.yml
Dockerfile* Dockerfile*
.dockerignore .dockerignore
+3
View File
@@ -13,3 +13,6 @@ AI_MODEL=GLM-4.7-Flash
# 版本更新配置 # 版本更新配置
# 在 OneDev 中创建 Access Token (Settings → Access Tokens → New Token, scope: Read code) # 在 OneDev 中创建 Access Token (Settings → Access Tokens → New Token, scope: Read code)
ONEDEV_ACCESS_TOKEN=Gtn1krR7tmv4x2c7xLupvW6OqgwWbmanpKonknpQ ONEDEV_ACCESS_TOKEN=Gtn1krR7tmv4x2c7xLupvW6OqgwWbmanpKonknpQ
REGISTRY_IMAGE=recipe_tool:latest
REPOSITORY_URL=https://git.hanhan.ltd/recipe_tool
+2 -2
View File
@@ -1,5 +1,5 @@
{ {
"version": "1.0.4", "version": "1.0.8",
"release_date": "2025-01-15", "release_date": "2025-01-15",
"changelog": "将更新内容移入版本信息卡片内,移除独立 changelog 区域" "changelog": "修复构建bug,排除_docker-compose__yml_导致构建失败.patch"
} }
+1
View File
@@ -47,6 +47,7 @@ COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/config.json ./ COPY --from=builder /app/config.json ./
COPY --from=builder /app/.version ./ COPY --from=builder /app/.version ./
COPY --from=builder /app/.env ./ COPY --from=builder /app/.env ./
COPY --from=builder /app/docker-compose.yml ./
# 给 nextjs 用户授权整个目录 # 给 nextjs 用户授权整个目录
RUN chown -R nextjs:nodejs /app RUN chown -R nextjs:nodejs /app
+31 -28
View File
@@ -42,19 +42,19 @@ interface WeeklyMenu {
// 餐食类型配置 // 餐食类型配置
const MEAL_TYPES = [ const MEAL_TYPES = [
{ code: MealTypeCode.BREAKFAST, label: "早餐" }, {code: MealTypeCode.BREAKFAST, label: "早餐"},
{ code: MealTypeCode.FRUIT, label: "早点/水果餐" }, {code: MealTypeCode.FRUIT, label: "早点/水果餐"},
{ code: MealTypeCode.LUNCH, label: "中餐" }, {code: MealTypeCode.LUNCH, label: "中餐"},
{ code: MealTypeCode.SPECIAL, label: "体弱儿餐" }, {code: MealTypeCode.SPECIAL, label: "体弱儿餐"},
{ code: MealTypeCode.SNACK, label: "午点" }, {code: MealTypeCode.SNACK, label: "午点"},
]; ];
const WEEK_DAYS = [ const WEEK_DAYS = [
{ code: 1, label: "周一" }, {code: 1, label: "周一"},
{ code: 2, label: "周二" }, {code: 2, label: "周二"},
{ code: 3, label: "周三" }, {code: 3, label: "周三"},
{ code: 4, label: "周四" }, {code: 4, label: "周四"},
{ code: 5, label: "周五" }, {code: 5, label: "周五"},
]; ];
/** /**
@@ -128,11 +128,14 @@ function splitCellContent(content: string): string[] {
* 确定餐食类型 * 确定餐食类型
*/ */
function getMealType(mealName: string): MealTypeCode | null { function getMealType(mealName: string): MealTypeCode | null {
if (mealName.includes("早") && !mealName.includes("早点")) return MealTypeCode.BREAKFAST; const header = mealName.trim();
if (mealName.includes("水果") || mealName.includes("早点")) return MealTypeCode.FRUIT;
if (mealName.includes("") || mealName.includes("餐")) return MealTypeCode.LUNCH; if (header.includes("早餐") || header.includes("餐")) return MealTypeCode.BREAKFAST;
if (mealName.includes("点") || mealName.includes("点心")) return MealTypeCode.SNACK; if (header.includes("点") || header.includes("水果餐") || header.includes("早 点") || header.includes("早点/水果餐")) return MealTypeCode.FRUIT;
if (mealName.includes("体弱") || mealName.includes("")) return MealTypeCode.SPECIAL; if (header.includes("中餐") || header.includes("中 餐")) return MealTypeCode.LUNCH;
if (header.includes("午点") || header.includes("午 点")) return MealTypeCode.SNACK;
if (header.includes("体弱儿餐") || header.includes("体弱儿")) return MealTypeCode.SPECIAL;
return null; return null;
} }
@@ -178,7 +181,7 @@ function transformTableToWeeklyMenu(tableData: string[][][]): WeeklyMenu {
mealCode: mealType, mealCode: mealType,
dishList: mealItems.map((item, idx) => ({ dishList: mealItems.map((item, idx) => ({
id: `${dayIndex + 1}-${mealType}-${idx + 1}`, id: `${dayIndex + 1}-${mealType}-${idx + 1}`,
name: { zh: item }, name: {zh: item},
})), })),
}); });
} }
@@ -191,7 +194,7 @@ function transformTableToWeeklyMenu(tableData: string[][][]): WeeklyMenu {
(m) => m.dayCode === day.code && m.mealCode === meal.code (m) => m.dayCode === day.code && m.mealCode === meal.code
); );
if (!exists) { if (!exists) {
meals.push({ dayCode: day.code, mealCode: meal.code, dishList: [] }); meals.push({dayCode: day.code, mealCode: meal.code, dishList: []});
} }
} }
} }
@@ -199,16 +202,16 @@ function transformTableToWeeklyMenu(tableData: string[][][]): WeeklyMenu {
// 构建 days 数组,使用实际解析出的星期 // 构建 days 数组,使用实际解析出的星期
const days = weekdays const days = weekdays
.filter(Boolean) .filter(Boolean)
.map((name, index) => ({ code: index + 1, name: { zh: name } })); .map((name, index) => ({code: index + 1, name: {zh: name}}));
return { return {
menuId: `menu-${Date.now()}`, menuId: `menu-${Date.now()}`,
weekNumber: "", weekNumber: "",
title: { zh: "食谱", en: "Recipe" }, title: {zh: "食谱", en: "Recipe"},
days: days, days: days,
mealCategories: MEAL_TYPES.map((m, i) => ({ mealCategories: MEAL_TYPES.map((m, i) => ({
code: m.code, code: m.code,
name: { zh: m.label }, name: {zh: m.label},
sort: i + 1, sort: i + 1,
})), })),
meals: meals.sort((a, b) => { meals: meals.sort((a, b) => {
@@ -229,11 +232,11 @@ function createEmptyMenu(): WeeklyMenu {
return { return {
menuId: `menu-${Date.now()}`, menuId: `menu-${Date.now()}`,
weekNumber: "", weekNumber: "",
title: { zh: "食谱", en: "Recipe" }, title: {zh: "食谱", en: "Recipe"},
days: WEEK_DAYS.map((d) => ({ code: d.code, name: { zh: d.label } })), days: WEEK_DAYS.map((d) => ({code: d.code, name: {zh: d.label}})),
mealCategories: MEAL_TYPES.map((m, i) => ({ mealCategories: MEAL_TYPES.map((m, i) => ({
code: m.code, code: m.code,
name: { zh: m.label }, name: {zh: m.label},
sort: i + 1, sort: i + 1,
})), })),
meals: [], meals: [],
@@ -246,11 +249,11 @@ export async function POST(request: NextRequest) {
const file = formData.get("file") as File | null; const file = formData.get("file") as File | null;
if (!file) { if (!file) {
return NextResponse.json({ error: "未上传文件" }, { status: 400 }); return NextResponse.json({error: "未上传文件"}, {status: 400});
} }
if (!file.name.endsWith(".docx")) { if (!file.name.endsWith(".docx")) {
return NextResponse.json({ error: "仅支持 .docx 格式" }, { status: 400 }); return NextResponse.json({error: "仅支持 .docx 格式"}, {status: 400});
} }
// 读取文件为 ArrayBuffer // 读取文件为 ArrayBuffer
@@ -258,7 +261,7 @@ export async function POST(request: NextRequest) {
const buffer = Buffer.from(arrayBuffer); const buffer = Buffer.from(arrayBuffer);
// 使用 mammoth 转换为 HTML(保留表格结构) // 使用 mammoth 转换为 HTML(保留表格结构)
const { value: htmlContent } = await mammoth.convertToHtml({ buffer }); const {value: htmlContent} = await mammoth.convertToHtml({buffer});
// 使用 cheerio 提取表格数据 // 使用 cheerio 提取表格数据
const tableData = extractRawDataFromHtml(htmlContent); const tableData = extractRawDataFromHtml(htmlContent);
@@ -273,8 +276,8 @@ export async function POST(request: NextRequest) {
} catch (error) { } catch (error) {
console.error("Parse error:", error); console.error("Parse error:", error);
return NextResponse.json( return NextResponse.json(
{ error: "文件解析失败", details: String(error) }, {error: "文件解析失败", details: String(error)},
{ status: 500 } {status: 500}
); );
} }
} }
+6
View File
@@ -42,6 +42,12 @@ export default function UpdatePage() {
logEndRef.current?.scrollIntoView({behavior: "smooth"}); logEndRef.current?.scrollIntoView({behavior: "smooth"});
}, [logs]); }, [logs]);
// 页面加载时自动检查更新
useEffect(() => {
handleCheckUpdate();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// 检查更新 // 检查更新
const handleCheckUpdate = useCallback(async () => { const handleCheckUpdate = useCallback(async () => {
setCheckState("checking"); setCheckState("checking");
+3 -1
View File
@@ -6,7 +6,7 @@ services:
container_name: recipe_tool_app container_name: recipe_tool_app
restart: unless-stopped restart: unless-stopped
ports: ports:
- "3000:3000" - "8011:3000"
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
environment: environment:
@@ -20,4 +20,6 @@ services:
- AI_BASE_URL=${AI_BASE_URL:-https://api.openai.com/v1} - AI_BASE_URL=${AI_BASE_URL:-https://api.openai.com/v1}
- AI_MODEL=${AI_MODEL:-gpt-3.5-turbo} - AI_MODEL=${AI_MODEL:-gpt-3.5-turbo}
- ONEDEV_ACCESS_TOKEN=${ONEDEV_ACCESS_TOKEN} - ONEDEV_ACCESS_TOKEN=${ONEDEV_ACCESS_TOKEN}
- REGISTRY_IMAGE=${REGISTRY_IMAGE:-recipe_tool:latest}
- REPOSITORY_URL=${REPOSITORY_URL:-https://git.hanhan.ltd/recipe_tool}
- GIT_REMOTE_URL=https://git.hanhan.ltd/recipe_tool.git - GIT_REMOTE_URL=https://git.hanhan.ltd/recipe_tool.git
+148 -96
View File
@@ -1,6 +1,6 @@
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import { execSync } from "child_process"; import { spawn } from "child_process";
// ============================================================ // ============================================================
// 版本更新模块 // 版本更新模块
@@ -57,38 +57,78 @@ function consoleErr(tag: string, msg: string): void {
} }
/** /**
* 执行命令并记录到控制台 * 异步执行命令,实时流式输出(不阻塞事件循环)
* @param command 可执行文件
* @param args 参数列表
* @param tag 日志标签
* @param options.onLine 每行输出回调(用于 SSE 推送到前端)
*/ */
function execSyncAndLog( function execAsync(
command: string, command: string,
args: string[],
tag: string, tag: string,
options: Parameters<typeof execSync>[1] & { encoding?: "utf-8" } = {} options?: {
): string { timeout?: number;
consoleLog(tag, `执行: ${command}`); cwd?: string;
try { onLine?: (line: string) => void;
const stdout = execSync(command, { }
encoding: "utf-8", ): Promise<void> {
return new Promise((resolve, reject) => {
const desc = `${command} ${args.join(" ")}`;
consoleLog(tag, `执行: ${desc}`);
const child = spawn(command, args, {
cwd: options?.cwd,
timeout: options?.timeout,
stdio: ["ignore", "pipe", "pipe"],
windowsHide: true, windowsHide: true,
...options,
}); });
const out = stdout?.trim();
if (out) { let timedOut = false;
const lines = out.split("\n").filter(Boolean); if (options?.timeout) {
// 最多打印 5 行,避免刷屏 const timer = setTimeout(() => {
lines.slice(0, 5).forEach((l) => consoleLog(tag, `${l}`)); timedOut = true;
if (lines.length > 5) consoleLog(tag, ` │ ... 共 ${lines.length}`); child.kill();
reject(new Error(`命令超时 (${options.timeout}ms): ${desc}`));
}, options.timeout);
child.on("close", () => clearTimeout(timer));
} }
consoleLog(tag, "✓ 完成");
return stdout || ""; const handleData = (data: Buffer, isStderr: boolean) => {
} catch (err) { const str = data.toString();
const e = err as Error & { stderr?: Buffer }; const lines = str.split("\n").filter(Boolean);
const stderr = e.stderr?.toString().trim(); for (const raw of lines) {
if (stderr) { const line = raw.trimEnd();
stderr.split("\n").filter(Boolean).forEach((l) => consoleErr(tag, `${l}`)); if (!line) continue;
if (isStderr) {
consoleErr(tag, `${line.slice(0, 2000)}`);
} else {
consoleLog(tag, `${line.slice(0, 2000)}`);
} }
consoleErr(tag, `失败: ${e.message}`); options?.onLine?.(line);
throw err;
} }
};
child.stdout?.on("data", (data: Buffer) => handleData(data, false));
child.stderr?.on("data", (data: Buffer) => handleData(data, true));
child.on("close", (code) => {
if (timedOut) return;
if (code === 0) {
consoleLog(tag, `✓ 完成`);
resolve();
} else {
consoleErr(tag, `失败 (exit=${code})`);
reject(new Error(`Command failed with exit code ${code}: ${desc}`));
}
});
child.on("error", (err) => {
if (timedOut) return;
consoleErr(tag, `异常: ${err.message}`);
reject(err);
});
});
} }
/** /**
@@ -130,19 +170,15 @@ function getAuthenticatedRemoteUrl(): string {
/** /**
* 通过 git ls-remote 获取远程最新 commit hash * 通过 git ls-remote 获取远程最新 commit hash
*/ */
export function getRemoteCommitHash(): string | null { export async function getRemoteCommitHash(): Promise<string | null> {
try { try {
const url = getAuthenticatedRemoteUrl(); const url = getAuthenticatedRemoteUrl();
const output = execSync(`git ls-remote "${url}" HEAD`, { let stdout = "";
encoding: "utf-8", await execAsync("git", ["ls-remote", url, "HEAD"], "git-ls-remote", {
timeout: 15000, timeout: 15000,
env: { onLine: (line) => { stdout += line + "\n"; },
...process.env,
GIT_TERMINAL_PROMPT: "0",
},
windowsHide: true,
}); });
const match = output.match(/^([a-f0-9]+)\s+HEAD/m); const match = stdout.match(/^([a-f0-9]+)\s+HEAD/m);
return match?.[1] || null; return match?.[1] || null;
} catch { } catch {
return null; return null;
@@ -152,19 +188,16 @@ export function getRemoteCommitHash(): string | null {
/** /**
* 获取远程 .version 文件内容并解析为 VersionFile * 获取远程 .version 文件内容并解析为 VersionFile
*/ */
export function getRemoteVersionFile(): VersionFile | null { export async function getRemoteVersionFile(): Promise<VersionFile | null> {
// 尝试方式1: git archive // 尝试方式1: git archive
try { try {
const url = getAuthenticatedRemoteUrl(); const url = getAuthenticatedRemoteUrl();
const output = execSync( let stdout = "";
`git archive --remote="${url}" HEAD:.version 2>nul`, await execAsync("git", ["archive", `--remote=${url}`, "HEAD:.version"], "git-archive", {
{
encoding: "utf-8",
timeout: 15000, timeout: 15000,
windowsHide: true, onLine: (line) => { stdout += line; },
} });
); const parsed = JSON.parse(stdout.trim()) as VersionFile;
const parsed = JSON.parse(output.trim()) as VersionFile;
if (parsed.version) return parsed; if (parsed.version) return parsed;
} catch { } catch {
// fall through // fall through
@@ -178,18 +211,15 @@ export function getRemoteVersionFile(): VersionFile | null {
* 通过浅克隆获取远程 .version 文件(备选方案) * 通过浅克隆获取远程 .version 文件(备选方案)
* OneDev 不支持 git archive --remote,改用完整浅克隆 * OneDev 不支持 git archive --remote,改用完整浅克隆
*/ */
function getRemoteVersionByClone(): VersionFile | null { async function getRemoteVersionByClone(): Promise<VersionFile | null> {
const tmpDir = path.join( const tmpDir = path.join(
process.env.TEMP || "/tmp", process.env.TEMP || "/tmp",
`recipe_tool_version_${Date.now()}` `recipe_tool_version_${Date.now()}`
); );
try { try {
const url = getAuthenticatedRemoteUrl(); const url = getAuthenticatedRemoteUrl();
execSync(`git clone --depth 1 "${url}" "${tmpDir}"`, { await execAsync("git", ["clone", "--depth", "1", url, tmpDir], "git-clone", {
encoding: "utf-8",
timeout: 60000, timeout: 60000,
windowsHide: true,
stdio: ["ignore", "pipe", "pipe"],
}); });
const versionPath = path.join(tmpDir, ".version"); const versionPath = path.join(tmpDir, ".version");
if (fs.existsSync(versionPath)) { if (fs.existsSync(versionPath)) {
@@ -234,7 +264,7 @@ export async function checkForUpdate(): Promise<VersionInfo> {
const current = readLocalVersionFile(); const current = readLocalVersionFile();
consoleLog("检查", `当前版本: v${current.version} (${current.release_date || "无日期"})`); consoleLog("检查", `当前版本: v${current.version} (${current.release_date || "无日期"})`);
const remoteCommit = getRemoteCommitHash(); const remoteCommit = await getRemoteCommitHash();
if (!remoteCommit) { if (!remoteCommit) {
consoleErr("检查", "无法连接到远程仓库"); consoleErr("检查", "无法连接到远程仓库");
@@ -247,7 +277,7 @@ export async function checkForUpdate(): Promise<VersionInfo> {
} }
consoleLog("检查", `远程 HEAD: ${remoteCommit}`); consoleLog("检查", `远程 HEAD: ${remoteCommit}`);
const remoteVersionFile = getRemoteVersionFile(); const remoteVersionFile = await getRemoteVersionFile();
if (remoteVersionFile) { if (remoteVersionFile) {
const cmp = compareVersions(remoteVersionFile.version, current.version); const cmp = compareVersions(remoteVersionFile.version, current.version);
@@ -279,10 +309,10 @@ export async function checkForUpdate(): Promise<VersionInfo> {
/** /**
* 检查 Docker 是否可用 * 检查 Docker 是否可用
*/ */
function checkDockerAvailable(): { ok: boolean; message: string } { async function checkDockerAvailable(): Promise<{ ok: boolean; message: string }> {
consoleLog("Docker", "检查 Docker 守护进程..."); consoleLog("Docker", "检查 Docker 守护进程...");
try { try {
execSyncAndLog("docker info", "Docker"); await execAsync("docker", ["info"], "Docker");
consoleLog("Docker", "✓ Docker 正常"); consoleLog("Docker", "✓ Docker 正常");
return { ok: true, message: "" }; return { ok: true, message: "" };
} catch { } catch {
@@ -294,7 +324,7 @@ function checkDockerAvailable(): { ok: boolean; message: string } {
"更新功能需要在 Docker 容器内运行,当前环境不支持。\n" + "更新功能需要在 Docker 容器内运行,当前环境不支持。\n" +
"如需测试,请使用 Docker 部署后访问容器内的页面:\n" + "如需测试,请使用 Docker 部署后访问容器内的页面:\n" +
" docker compose -p recipe_tool up -d\n" + " docker compose -p recipe_tool up -d\n" +
"然后访问 http://localhost:3000/settings/update", "然后访问 http://localhost:8011/settings/update",
}; };
} }
} }
@@ -306,13 +336,23 @@ function checkDockerAvailable(): { ok: boolean; message: string } {
export async function executeUpdate( export async function executeUpdate(
onLog: LogCallback = noopLog onLog: LogCallback = noopLog
): Promise<{ success: boolean; message: string }> { ): Promise<{ success: boolean; message: string }> {
const tmpDir = path.join( const cwd = process.cwd(); // /app(容器内工作目录,含 docker-compose.yml
process.env.TEMP || "/tmp", const token = process.env.ONEDEV_ACCESS_TOKEN || "";
`recipe_tool_update_${Date.now()}` const repoUrl = process.env.REPOSITORY_URL || "https://git.hanhan.ltd/recipe_tool";
); const registryImage = process.env.REGISTRY_IMAGE || "recipe_tool:latest";
// 从 REPOSITORY_URL 提取 registry host 和 project path 以拼出完整镜像地址
// 例如: https://git.hanhan.ltd/recipe_tool → host=git.hanhan.ltd, path=recipe_tool
// 完整镜像: git.hanhan.ltd/recipe_tool/recipe_tool:latest
const repoUrlObj = new URL(repoUrl);
const registryHost = repoUrlObj.host;
const projectPath = repoUrlObj.pathname.replace(/^\/|\/$/g, "");
const fullImage = `${registryHost}/${projectPath}/${registryImage}`;
consoleLog("执行", "=== 开始执行更新 ==="); consoleLog("执行", "=== 开始执行更新 ===");
consoleLog("执行", `临时目录: ${tmpDir}`); consoleLog("执行", `工作目录: ${cwd}, 镜像: ${fullImage}`);
const logLine = (line: string) => onLog(line);
try { try {
onLog("开始更新流程"); onLog("开始更新流程");
@@ -320,7 +360,7 @@ export async function executeUpdate(
// 0. 检查 Docker 是否可用 // 0. 检查 Docker 是否可用
onLog("[0/4] 检查 Docker 环境"); onLog("[0/4] 检查 Docker 环境");
consoleLog("执行", "步骤 0/4 — 检查 Docker 环境"); consoleLog("执行", "步骤 0/4 — 检查 Docker 环境");
const dockerCheck = checkDockerAvailable(); const dockerCheck = await checkDockerAvailable();
if (!dockerCheck.ok) { if (!dockerCheck.ok) {
onLog(`${dockerCheck.message}`); onLog(`${dockerCheck.message}`);
consoleErr("执行", "Docker 检查未通过,更新终止"); consoleErr("执行", "Docker 检查未通过,更新终止");
@@ -328,18 +368,30 @@ export async function executeUpdate(
} }
onLog("✓ Docker 环境正常"); onLog("✓ Docker 环境正常");
// 1. 拉取最新代码 // 1. 登录 OneDev 镜像仓库并拉取最新镜像
const url = getAuthenticatedRemoteUrl(); onLog("[1/4] 登录镜像仓库");
onLog("[1/4] 克隆最新代码"); consoleLog("执行", "步骤 1/4 — 登录 OneDev 镜像仓库");
consoleLog("执行", "步骤 1/4 — 克隆代码"); if (token) {
execSyncAndLog( await execAsync(
`git clone --depth 1 "${url}" "${tmpDir}"`, "sh",
"git-clone", ["-c", `echo "${token}" | docker login ${registryHost} -u access-token --password-stdin`],
{ timeout: 120000, stdio: ["ignore", "pipe", "pipe"] } "docker-login",
{ timeout: 15000, onLine: logLine }
); );
onLog("✓ 代码拉取完成"); } else {
onLog("⚠ 未配置 ONEDEV_ACCESS_TOKEN,尝试匿名拉取");
consoleLog("执行", "未配置 access token,尝试匿名拉取");
}
// 2. 生成 .env 文件 onLog("[1/4] 拉取新镜像");
consoleLog("执行", "步骤 1/4 — 拉取镜像");
await execAsync("docker", ["pull", fullImage], "docker-pull", {
timeout: 300000,
onLine: logLine,
});
onLog("✓ 镜像拉取完成");
// 2. 生成 .env 配置文件(供 docker-compose 解析 ${VAR}
onLog("[2/4] 准备环境变量"); onLog("[2/4] 准备环境变量");
consoleLog("执行", "步骤 2/4 — 准备环境变量"); consoleLog("执行", "步骤 2/4 — 准备环境变量");
const envContent = [ const envContent = [
@@ -351,31 +403,38 @@ export async function executeUpdate(
`AI_API_KEY=${process.env.AI_API_KEY || ""}`, `AI_API_KEY=${process.env.AI_API_KEY || ""}`,
`AI_BASE_URL=${process.env.AI_BASE_URL || "https://api.openai.com/v1"}`, `AI_BASE_URL=${process.env.AI_BASE_URL || "https://api.openai.com/v1"}`,
`AI_MODEL=${process.env.AI_MODEL || "gpt-3.5-turbo"}`, `AI_MODEL=${process.env.AI_MODEL || "gpt-3.5-turbo"}`,
`ONEDEV_ACCESS_TOKEN=${process.env.ONEDEV_ACCESS_TOKEN || ""}`, `ONEDEV_ACCESS_TOKEN=${token}`,
`REGISTRY_IMAGE=${registryImage}`,
`REPOSITORY_URL=${repoUrl}`,
`GIT_REMOTE_URL=${process.env.GIT_REMOTE_URL || "https://git.hanhan.ltd/recipe_tool.git"}`,
].join("\n"); ].join("\n");
fs.writeFileSync(path.join(tmpDir, ".env"), envContent, "utf-8"); fs.writeFileSync(path.join(cwd, ".env"), envContent, "utf-8");
consoleLog("执行", `.env 已写入 ${envContent.split("\n").length}`); consoleLog("执行", `.env 已写入 ${envContent.split("\n").length}`);
onLog("✓ 环境变量已准备"); onLog("✓ 环境变量已准备");
// 3. 构建新镜像 // 3. 停止旧容器(释放端口),忽略失败
onLog("[3/4] 构建新 Docker 镜像"); onLog("[3/4] 停止旧容器");
consoleLog("执行", "步骤 3/4 — 构建镜像(可能耗时较长)"); consoleLog("执行", "步骤 3/4 — 停止旧容器");
execSyncAndLog( try {
`cd "${tmpDir}" && docker-compose -p recipe_tool build`, await execAsync("docker-compose", ["-p", "recipe_tool", "down", "--remove-orphans"], "docker-down", {
"docker-build", cwd,
{ timeout: 600000, stdio: ["ignore", "pipe", "pipe"] } timeout: 30000,
); onLine: logLine,
onLog("✓ 镜像构建完成"); });
} catch {
consoleLog("执行", "旧容器停止(可能不存在,忽略)");
}
onLog("✓ 旧容器已停止");
// 4. 重启服务 // 4. 启动新容器(使用拉取的镜像)
onLog("[4/4] 重启服务"); onLog("[4/4] 启动新容器");
consoleLog("执行", "步骤 4/4 — 重启服务"); consoleLog("执行", "步骤 4/4 — 启动新容器");
execSyncAndLog( await execAsync("docker-compose", ["-p", "recipe_tool", "up", "-d"], "docker-up", {
`cd "${tmpDir}" && docker-compose -p recipe_tool up -d`, cwd,
"docker-up", timeout: 60000,
{ timeout: 60000, stdio: ["ignore", "pipe", "pipe"] } onLine: logLine,
); });
onLog("✓ 服务已重启"); onLog("✓ 新容器已启动");
consoleLog("执行", "=== 更新成功 ==="); consoleLog("执行", "=== 更新成功 ===");
return { success: true, message: "更新完成" }; return { success: true, message: "更新完成" };
@@ -384,12 +443,5 @@ export async function executeUpdate(
consoleErr("执行", `=== 更新失败: ${errMsg} ===`); consoleErr("执行", `=== 更新失败: ${errMsg} ===`);
onLog(`✗ 更新失败: ${errMsg}`); onLog(`✗ 更新失败: ${errMsg}`);
return { success: false, message: errMsg }; return { success: false, message: errMsg };
} finally {
try {
fs.rmSync(tmpDir, { recursive: true, force: true });
consoleLog("执行", `临时目录已清理: ${tmpDir}`);
} catch {
// ignore cleanup errors
}
} }
} }
+4
View File
@@ -1,3 +1,7 @@
allowBuilds:
msw: true
sharp: true
unrs-resolver: true
ignoredBuiltDependencies: ignoredBuiltDependencies:
- sharp - sharp
- unrs-resolver - unrs-resolver