@wuyaos/pi-sync 1.2.1 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/extensions/sync/index.ts
CHANGED
|
@@ -1,9 +1,35 @@
|
|
|
1
1
|
import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import * as fs from "node:fs";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
|
|
4
|
-
import { isProjectAllowed, loadConfig, refreshFooterStatusFromConfig } from "./config";
|
|
5
|
+
import { AGENT_DIR, isProjectAllowed, loadConfig, refreshFooterStatusFromConfig } from "./config";
|
|
6
|
+
import { ensureDir } from "../_shared/json-io";
|
|
5
7
|
import { registerSyncCommand, uploadSessionProjectArchive } from "./menus";
|
|
6
8
|
|
|
9
|
+
/** 退出时自动上传会话归档的最小间隔:频繁重启不应耗尽 maxBackups 把含历史的老归档轮替掉。 */
|
|
10
|
+
export const EXIT_UPLOAD_MIN_INTERVAL_MS = 30 * 60 * 1000;
|
|
11
|
+
|
|
12
|
+
const exitUploadMarkerPath = path.join(AGENT_DIR, "state", "pi-sync-last-exit-upload.txt");
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 判断退出自动上传是否到期。marker 不存在或距上次上传超过 minIntervalMs 时到期。
|
|
16
|
+
* 独立导出便于测试。
|
|
17
|
+
*/
|
|
18
|
+
export function exitUploadDue(markerPath: string, nowMs: number, minIntervalMs = EXIT_UPLOAD_MIN_INTERVAL_MS): boolean {
|
|
19
|
+
try {
|
|
20
|
+
const last = Number(fs.readFileSync(markerPath, "utf8").trim());
|
|
21
|
+
if (!Number.isFinite(last) || last <= 0) return true;
|
|
22
|
+
return nowMs - last >= minIntervalMs;
|
|
23
|
+
} catch {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function recordExitUpload(markerPath: string, nowMs: number): void {
|
|
29
|
+
ensureDir(path.dirname(markerPath));
|
|
30
|
+
fs.writeFileSync(markerPath, String(nowMs));
|
|
31
|
+
}
|
|
32
|
+
|
|
7
33
|
export function projectDirFromSessionDir(sessionDir: string | undefined): string | undefined {
|
|
8
34
|
if (!sessionDir) return undefined;
|
|
9
35
|
const projectDir = path.basename(sessionDir);
|
|
@@ -29,7 +55,10 @@ export default function registerSyncExtension(pi: ExtensionAPI): void {
|
|
|
29
55
|
if (!config.webdavUrl || !config.webdavUser || !config.webdavPass) return;
|
|
30
56
|
const projectDir = projectDirFromSessionDir(ctx.sessionManager.getSessionDir());
|
|
31
57
|
if (!projectDir || !isProjectAllowed(projectDir, config)) return;
|
|
32
|
-
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
if (!exitUploadDue(exitUploadMarkerPath, now)) return;
|
|
60
|
+
const uploaded = await uploadSessionProjectArchive(ctx, config, projectDir, true);
|
|
61
|
+
if (uploaded) recordExitUpload(exitUploadMarkerPath, now);
|
|
33
62
|
});
|
|
34
63
|
|
|
35
64
|
registerSyncCommand(pi);
|
|
@@ -12,7 +12,15 @@ export const WEBDAV_AGENT_SKILLS_DIR = "backup/skills/";
|
|
|
12
12
|
export const WEBDAV_SESSIONS_ARCHIVE_DIR = "backup/sessions/";
|
|
13
13
|
|
|
14
14
|
export const ensureTrailingSlash = (url: string): string => url.endsWith("/") ? url : `${url}/`;
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* 拼接 WebDAV 目录 URL。remoteDir 逐段 encodeURIComponent,与 ensureWebdavDirectory
|
|
17
|
+
* 的编码一致;否则含中文/空格的 cwd 转义目录在严格 WebDAV 实现上会 400。
|
|
18
|
+
*/
|
|
19
|
+
export const webdavDirBase = (config: SyncConfig, remoteDir: string): string =>
|
|
20
|
+
remoteDir.split("/").filter(Boolean).reduce(
|
|
21
|
+
(url, segment) => `${url}${encodeURIComponent(segment)}/`,
|
|
22
|
+
ensureTrailingSlash(config.webdavUrl),
|
|
23
|
+
);
|
|
16
24
|
export const webdavAuth = (config: SyncConfig): string => "Basic " + Buffer.from(`${config.webdavUser}:${resolvePassword(config.webdavPass)}`).toString("base64");
|
|
17
25
|
|
|
18
26
|
export async function webdavList(url: string, auth: string, ctx: ExtensionContext, filter?: (name: string) => boolean): Promise<string[]> {
|