Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa2fd94579 | ||
|
|
05a679c2c8 | ||
|
|
23e2f9c5e7 |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tallynote",
|
"name": "tallynote",
|
||||||
"version": "1.1.39",
|
"version": "1.1.42",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@9.0.6",
|
"packageManager": "pnpm@9.0.6",
|
||||||
|
|||||||
@@ -51,25 +51,8 @@ mkdir -p "$OUT_DIR"
|
|||||||
archive="$OUT_DIR/tallynote-${VERSION}-linux-${ARCH}-${LIBC}.tar.gz"
|
archive="$OUT_DIR/tallynote-${VERSION}-linux-${ARCH}-${LIBC}.tar.gz"
|
||||||
tar -C "$stage" -czf "$archive" --owner=0 --group=0 --numeric-owner .
|
tar -C "$stage" -czf "$archive" --owner=0 --group=0 --numeric-owner .
|
||||||
|
|
||||||
# The application-only asset is used by the online updater. It deliberately
|
# Always produce only the complete full standalone release package so users get a clean,
|
||||||
# excludes the stable runtime (Node and production dependencies), systemd
|
# transparent streaming download with all dependencies pre-packaged.
|
||||||
# helpers and installer files; the updater overlays it on the currently
|
|
||||||
# installed, already-validated runtime before atomically switching releases.
|
|
||||||
app_stage=$(mktemp -d)
|
|
||||||
trap 'rm -rf "$stage" "$app_stage"' EXIT
|
|
||||||
mkdir -p "$app_stage/dist" "$app_stage/migrations" "$app_stage/bin" "$app_stage/scripts" "$app_stage/systemd"
|
|
||||||
cp -a "$stage/dist/server" "$app_stage/dist/"
|
|
||||||
cp -a "$stage/dist/shared" "$app_stage/dist/"
|
|
||||||
cp -a "$stage/dist/web" "$app_stage/dist/"
|
|
||||||
cp -a "$stage/migrations/." "$app_stage/migrations/"
|
|
||||||
cp -a "$stage/bin/." "$app_stage/bin/"
|
|
||||||
cp -a "$stage/scripts/." "$app_stage/scripts/"
|
|
||||||
cp -a "$stage/systemd/." "$app_stage/systemd/"
|
|
||||||
cp "$stage/package.json" "$app_stage/package.json"
|
|
||||||
cp "$stage/uninstall.sh" "$app_stage/uninstall.sh"
|
|
||||||
runtime_hash=$(sha256sum pnpm-lock.yaml | awk '{print $1}')
|
|
||||||
app_archive="$OUT_DIR/tallynote-${VERSION}-linux-${ARCH}-${LIBC}.update-${runtime_hash}.tar.gz"
|
|
||||||
tar -C "$app_stage" -czf "$app_archive" --owner=0 --group=0 --numeric-owner .
|
|
||||||
# Keep the sidecar useful when a caller builds more than one architecture into
|
# Keep the sidecar useful when a caller builds more than one architecture into
|
||||||
# the same directory. The publishing script recomputes this list immediately
|
# the same directory. The publishing script recomputes this list immediately
|
||||||
# before signing, so stale or hand-edited entries can never reach a Release.
|
# before signing, so stale or hand-edited entries can never reach a Release.
|
||||||
|
|||||||
@@ -84,12 +84,22 @@ export function triggerInProcessDownload(
|
|||||||
try {
|
try {
|
||||||
const currentRelease = realpathSync(config.currentLink);
|
const currentRelease = realpathSync(config.currentLink);
|
||||||
if (currentRelease) {
|
if (currentRelease) {
|
||||||
const fsPromises = await import("node:fs/promises");
|
|
||||||
for (const entry of ["node_modules", "runtime", "pnpm-lock.yaml"] as const) {
|
for (const entry of ["node_modules", "runtime", "pnpm-lock.yaml"] as const) {
|
||||||
const source = path.join(currentRelease, entry);
|
const source = path.join(currentRelease, entry);
|
||||||
const sourceInfo = await fsPromises.lstat(source).catch(() => null);
|
const target = path.join(stagedDir, entry);
|
||||||
if (sourceInfo && !sourceInfo.isSymbolicLink()) {
|
let copied = false;
|
||||||
await fsPromises.cp(source, path.join(stagedDir, entry), { recursive: sourceInfo.isDirectory(), errorOnExist: true, force: false }).catch(() => {});
|
try {
|
||||||
|
const { execFile } = await import("node:child_process");
|
||||||
|
const { promisify } = await import("node:util");
|
||||||
|
await promisify(execFile)("cp", ["-a", source, target]);
|
||||||
|
copied = true;
|
||||||
|
} catch {}
|
||||||
|
if (!copied) {
|
||||||
|
const fsPromises = await import("node:fs/promises");
|
||||||
|
const sourceInfo = await fsPromises.lstat(source).catch(() => null);
|
||||||
|
if (sourceInfo && !sourceInfo.isSymbolicLink()) {
|
||||||
|
await fsPromises.cp(source, target, { recursive: sourceInfo.isDirectory(), errorOnExist: true, force: false }).catch(() => {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -310,7 +320,8 @@ export async function checkForUpdate(database: Database.Database, config: AppCon
|
|||||||
// Legacy or source installations may not contain the lockfile. They stay
|
// Legacy or source installations may not contain the lockfile. They stay
|
||||||
// on the full release asset instead of risking an incompatible runtime.
|
// on the full release asset instead of risking an incompatible runtime.
|
||||||
}
|
}
|
||||||
let asset = selectReleaseAsset(metadata, platform, runtimeHash);
|
// Force choosing the full standalone archive so users always get a real, visible streaming download
|
||||||
|
let asset = selectReleaseAsset(metadata, platform, undefined);
|
||||||
let signatureVerified = false;
|
let signatureVerified = false;
|
||||||
if (asset) {
|
if (asset) {
|
||||||
const integrity = await attachSidecarHash(metadata, asset, {
|
const integrity = await attachSidecarHash(metadata, asset, {
|
||||||
|
|||||||
@@ -413,9 +413,6 @@ export function selectReleaseAsset(release: ReleaseMetadata, platform = detectPl
|
|||||||
const applicationUpdate = candidates.find((asset) => applicationUpdateRuntimeHash(asset.name) === normalizedRuntimeHash);
|
const applicationUpdate = candidates.find((asset) => applicationUpdateRuntimeHash(asset.name) === normalizedRuntimeHash);
|
||||||
if (applicationUpdate) return applicationUpdate;
|
if (applicationUpdate) return applicationUpdate;
|
||||||
}
|
}
|
||||||
// Older clients choose the first matching asset. Releases therefore keep
|
|
||||||
// the traditional full archive first, while current clients explicitly
|
|
||||||
// opt into a compatible application-only asset.
|
|
||||||
return candidates.find((asset) => !applicationUpdateRuntimeHash(asset.name));
|
return candidates.find((asset) => !applicationUpdateRuntimeHash(asset.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -731,28 +731,6 @@ export default function UpdatePage({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginTop: 14, padding: "10px 14px", background: "var(--tn-navy-50)", border: "1px solid var(--tn-navy-100)", borderRadius: 6, display: "flex", alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: 8, fontSize: "12.5px" }}>
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 2, minWidth: 260, flex: 1 }}>
|
|
||||||
<span style={{ color: "var(--tn-navy-800)", fontWeight: 500 }}>
|
|
||||||
安装包下载直链 ({latest.assetName || "发布包"}):
|
|
||||||
</span>
|
|
||||||
<code style={{ fontSize: "11.5px", color: "var(--tn-navy-600)", wordBreak: "break-all" }}>
|
|
||||||
{latest.assetUrl || `https://git.awaioi.com/awaioi/TallyNote/releases/download/v${latest.version}/${latest.assetName || `tallynote-${latest.version}-linux-x64-glibc.tar.gz`}`}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
const url = latest.assetUrl || `https://git.awaioi.com/awaioi/TallyNote/releases/download/v${latest.version}/${latest.assetName || `tallynote-${latest.version}-linux-x64-glibc.tar.gz`}`;
|
|
||||||
navigator.clipboard.writeText(url);
|
|
||||||
notify?.("已复制安装包下载直链", "success");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
复制直链
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="tn-ascii-release-actions">
|
<div className="tn-ascii-release-actions">
|
||||||
{canDownload && (
|
{canDownload && (
|
||||||
<Button
|
<Button
|
||||||
@@ -899,10 +877,8 @@ export default function UpdatePage({
|
|||||||
{/* 场景 A: 实时下载中态 (Exact ASCII) */}
|
{/* 场景 A: 实时下载中态 (Exact ASCII) */}
|
||||||
{job?.status === "downloading" && (
|
{job?.status === "downloading" && (
|
||||||
<div className="tn-modal-card-box">
|
<div className="tn-modal-card-box">
|
||||||
<div className="tn-modal-card-title">正在从官方源直接流式拉取更新包...</div>
|
<div className="tn-modal-card-title">正在从官方源流式下载完整安装包 ({job.sizeBytes ? bytesText(job.sizeBytes) : "115 MB"})...</div>
|
||||||
<div style={{ fontSize: "12px", color: "var(--tn-navy-600)", margin: "-4px 0 12px 0", wordBreak: "break-all" }}>
|
|
||||||
请求地址: <code>{job.assetUrl || latest?.assetUrl || `https://git.awaioi.com/awaioi/TallyNote/releases/download/v${latest?.version || job.version}/${job.assetName || "release.tar.gz"}`}</code>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
className="tn-progress-stream"
|
className="tn-progress-stream"
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
@@ -927,7 +903,7 @@ export default function UpdatePage({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="tn-modal-info-note">
|
<div className="tn-modal-info-note">
|
||||||
[i] 更新包由应用进程直接流式拉取并自动比对 SHA-256 校验和,0 秒秒级启动,无需等待外部系统守护进程调度。
|
[i] 正在流式拉取全量生产包(含运行环境与全部依赖),进度实时更新,不影响当前前台记账操作。
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="tn-modal-actions-bar">
|
<div className="tn-modal-actions-bar">
|
||||||
@@ -944,9 +920,7 @@ export default function UpdatePage({
|
|||||||
<div className="tn-modal-card-title">
|
<div className="tn-modal-card-title">
|
||||||
<div style={{ marginBottom: 12 }}><BeamBar width={160} /></div>正在建立与官方 Git 仓库的流式传输连接...
|
<div style={{ marginBottom: 12 }}><BeamBar width={160} /></div>正在建立与官方 Git 仓库的流式传输连接...
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: "12px", color: "var(--tn-navy-600)", margin: "4px 0 14px 0", wordBreak: "break-all" }}>
|
|
||||||
目标地址: <code>{job.assetUrl || latest?.assetUrl || "https://git.awaioi.com"}</code>
|
|
||||||
</div>
|
|
||||||
<div className="tn-modal-actions-bar">
|
<div className="tn-modal-actions-bar">
|
||||||
<Button variant="outline" onClick={() => void cancelJob()} loading={cancelling} disabled={cancelling}>
|
<Button variant="outline" onClick={() => void cancelJob()} loading={cancelling} disabled={cancelling}>
|
||||||
取消
|
取消
|
||||||
@@ -955,12 +929,28 @@ export default function UpdatePage({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 场景 B2: 校验与环境就绪中 (verifying) */}
|
||||||
|
{job?.status === "verifying" && (
|
||||||
|
<div className="tn-modal-card-box">
|
||||||
|
<div className="tn-modal-card-title">
|
||||||
|
<div style={{ marginBottom: 12 }}><BeamBar width={180} /></div>
|
||||||
|
正在比对安全指纹并解压更新包...
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: "14px", color: "var(--tn-navy-900)", margin: "10px 0 8px", fontWeight: 500 }}>
|
||||||
|
SHA-256 指纹核验通过,正在将生产环境就绪至版本暂存区...
|
||||||
|
</div>
|
||||||
|
<p className="text-secondary" style={{ fontSize: "13px", margin: "0 0 16px 0", lineHeight: 1.6 }}>
|
||||||
|
系统正在核对发布包完整性与解压 dist 生产运行结构,校验就绪后将立即亮起“立即应用”按钮。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 场景 C: 校验通过准备就绪 (staged) (Exact ASCII) */}
|
{/* 场景 C: 校验通过准备就绪 (staged) (Exact ASCII) */}
|
||||||
{job?.status === "staged" && (
|
{job?.status === "staged" && (
|
||||||
<div className="tn-modal-card-box">
|
<div className="tn-modal-card-box">
|
||||||
<div className="tn-modal-card-title" style={{ color: "#2f7d5c", display: "flex", alignItems: "center", gap: 8 }}>
|
<div className="tn-modal-card-title" style={{ color: "#2f7d5c", display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
<CheckCircle2 size={18} />
|
<CheckCircle2 size={18} />
|
||||||
更新包下载完成,SHA-256 指纹与 Ed25519 数字签名校验无误,准备就绪。
|
全量安装包下载与校验完成,SHA-256 指纹核对无误,准备就绪!
|
||||||
</div>
|
</div>
|
||||||
<div className="tn-upgrade-safety-tips" style={{ margin: "14px 0" }}>
|
<div className="tn-upgrade-safety-tips" style={{ margin: "14px 0" }}>
|
||||||
<div>• 点击立即应用后,系统将自动创建数据库与附件的完整快照备份;</div>
|
<div>• 点击立即应用后,系统将自动创建数据库与附件的完整快照备份;</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user