@wuyaos/pi-sync 1.2.2 → 1.3.0
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 +73 -16
- package/package.json +1 -1
package/extensions/sync/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { type ExtensionAPI, type ExtensionContext, type SessionShutdownEvent } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
|
|
5
|
-
import { AGENT_DIR, isProjectAllowed, loadConfig, refreshFooterStatusFromConfig } from "./config";
|
|
5
|
+
import { AGENT_DIR, isProjectAllowed, loadConfig, refreshFooterStatusFromConfig, type SyncConfig } from "./config";
|
|
6
6
|
import { ensureDir } from "../_shared/json-io";
|
|
7
7
|
import { registerSyncCommand, uploadSessionProjectArchive } from "./menus";
|
|
8
8
|
|
|
@@ -37,28 +37,85 @@ export function projectDirFromSessionDir(sessionDir: string | undefined): string
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* Archive-only pi-sync entrypoint
|
|
40
|
+
* Archive-only pi-sync entrypoint。
|
|
41
41
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* timers, live uploads, or interval counters.
|
|
42
|
+
* 运行时工作仅限:启动时读一次缓存配置 + 关停时的会话归档(quit 同步等待,
|
|
43
|
+
* reload/new/resume/fork 转后台,不阻塞 reload)。无 per-turn 钩子、无定时器、无常驻上传。
|
|
45
44
|
*/
|
|
45
|
+
let exitUploadInFlight = false;
|
|
46
|
+
|
|
47
|
+
/** 测试用:重置后台任务标志。 */
|
|
48
|
+
export function resetExitUploadInFlight(): void {
|
|
49
|
+
exitUploadInFlight = false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ExitUploadDeps {
|
|
53
|
+
upload?: typeof uploadSessionProjectArchive;
|
|
54
|
+
config?: SyncConfig;
|
|
55
|
+
now?: number;
|
|
56
|
+
markerPath?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 退出自动上传会话归档。
|
|
61
|
+
* quit:进程即将终止,同步等待上传完成(fire-and-forget 会被终止)。
|
|
62
|
+
* reload/new/resume/fork:进程存活,上传转入后台,不阻塞 reload;
|
|
63
|
+
* 旧 runner 的 ctx.signal 在 teardown 后会失效,后台任务必须用独立 AbortController。
|
|
64
|
+
*/
|
|
65
|
+
export async function handleSessionShutdown(
|
|
66
|
+
event: Pick<SessionShutdownEvent, "reason">,
|
|
67
|
+
ctx: ExtensionContext,
|
|
68
|
+
deps: ExitUploadDeps = {},
|
|
69
|
+
): Promise<void> {
|
|
70
|
+
const config = deps.config ?? loadConfig();
|
|
71
|
+
if (!config.backupOnExit || !config.backupSessions) return;
|
|
72
|
+
if (!config.webdavUrl || !config.webdavUser || !config.webdavPass) return;
|
|
73
|
+
const projectDir = projectDirFromSessionDir(ctx.sessionManager.getSessionDir());
|
|
74
|
+
if (!projectDir || !isProjectAllowed(projectDir, config)) return;
|
|
75
|
+
const now = deps.now ?? Date.now();
|
|
76
|
+
const markerPath = deps.markerPath ?? exitUploadMarkerPath;
|
|
77
|
+
if (!exitUploadDue(markerPath, now)) return;
|
|
78
|
+
|
|
79
|
+
const upload = deps.upload ?? uploadSessionProjectArchive;
|
|
80
|
+
const finish = (uploaded: boolean): void => {
|
|
81
|
+
if (uploaded) recordExitUpload(markerPath, now);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (event.reason === "quit") {
|
|
85
|
+
finish(await upload(ctx, config, projectDir, true));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (exitUploadInFlight) return;
|
|
90
|
+
exitUploadInFlight = true;
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
const detachedCtx = {
|
|
93
|
+
signal: controller.signal,
|
|
94
|
+
ui: {
|
|
95
|
+
notify: (message: string, type?: "info" | "warning" | "error"): void => {
|
|
96
|
+
try { ctx.ui.notify(message, type); } catch { /* UI 可能已随 reload 重建 */ }
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
} as unknown as ExtensionContext;
|
|
100
|
+
void (async (): Promise<void> => {
|
|
101
|
+
try {
|
|
102
|
+
finish(await upload(detachedCtx, config, projectDir, false));
|
|
103
|
+
} catch {
|
|
104
|
+
// 后台归档失败只影响本次节流窗口,不影响会话;marker 不写入,下个窗口重试。
|
|
105
|
+
} finally {
|
|
106
|
+
exitUploadInFlight = false;
|
|
107
|
+
}
|
|
108
|
+
})();
|
|
109
|
+
}
|
|
110
|
+
|
|
46
111
|
export default function registerSyncExtension(pi: ExtensionAPI): void {
|
|
47
112
|
pi.on("session_start", async (_event, ctx) => {
|
|
48
113
|
const config = loadConfig();
|
|
49
114
|
refreshFooterStatusFromConfig(ctx, config);
|
|
50
115
|
});
|
|
51
116
|
|
|
52
|
-
pi.on("session_shutdown", async (
|
|
53
|
-
|
|
54
|
-
if (!config.backupOnExit || !config.backupSessions) return;
|
|
55
|
-
if (!config.webdavUrl || !config.webdavUser || !config.webdavPass) return;
|
|
56
|
-
const projectDir = projectDirFromSessionDir(ctx.sessionManager.getSessionDir());
|
|
57
|
-
if (!projectDir || !isProjectAllowed(projectDir, config)) return;
|
|
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);
|
|
117
|
+
pi.on("session_shutdown", async (event, ctx) => {
|
|
118
|
+
await handleSessionShutdown(event, ctx);
|
|
62
119
|
});
|
|
63
120
|
|
|
64
121
|
registerSyncCommand(pi);
|