























@@ -38,6 +38,11 @@ export const DEFAULT_MEMORY_FILENAME = CANONICAL_ROOT_MEMORY_FILENAME;
3838const WORKSPACE_STATE_DIRNAME = ".openclaw";
3939const WORKSPACE_STATE_FILENAME = "workspace-state.json";
4040const WORKSPACE_STATE_VERSION = 1;
41+const WORKSPACE_ONBOARDING_PROFILE_FILENAMES = [
42+DEFAULT_SOUL_FILENAME,
43+DEFAULT_IDENTITY_FILENAME,
44+DEFAULT_USER_FILENAME,
45+] as const;
41464247const workspaceTemplateCache = new Map<string, Promise<string>>();
4348let gitAvailabilityPromise: Promise<boolean> | null = null;
@@ -205,6 +210,111 @@ async function fileExists(filePath: string): Promise<boolean> {
205210}
206211}
207212213+async function fileContentDiffersFromTemplate(
214+filePath: string,
215+template: string,
216+): Promise<boolean> {
217+try {
218+return (await fs.readFile(filePath, "utf-8")) !== template;
219+} catch (err) {
220+const anyErr = err as { code?: string };
221+if (anyErr.code !== "ENOENT") {
222+throw err;
223+}
224+return false;
225+}
226+}
227+228+async function hasWorkspaceUserContentEvidence(
229+dir: string,
230+opts?: { includeGit?: boolean },
231+): Promise<boolean> {
232+const indicators = [path.join(dir, "memory")];
233+if (opts?.includeGit) {
234+indicators.push(path.join(dir, ".git"));
235+}
236+for (const indicator of indicators) {
237+try {
238+await fs.access(indicator);
239+return true;
240+} catch {
241+// continue
242+}
243+}
244+return await exactWorkspaceEntryExists(dir, DEFAULT_MEMORY_FILENAME);
245+}
246+247+async function workspaceProfileLooksConfigured(params: {
248+dir: string;
249+includeGitEvidence?: boolean;
250+}): Promise<boolean> {
251+const profileFileDiffs = await Promise.all(
252+WORKSPACE_ONBOARDING_PROFILE_FILENAMES.map(async (fileName) =>
253+fileContentDiffersFromTemplate(path.join(params.dir, fileName), await loadTemplate(fileName)),
254+),
255+);
256+return (
257+profileFileDiffs.some(Boolean) ||
258+(await hasWorkspaceUserContentEvidence(params.dir, {
259+includeGit: params.includeGitEvidence,
260+}))
261+);
262+}
263+264+async function workspaceHasBootstrapCompletionEvidence(params: { dir: string }): Promise<boolean> {
265+return await workspaceProfileLooksConfigured(params);
266+}
267+268+type WorkspaceBootstrapCompletionReconcileResult = {
269+repaired: boolean;
270+bootstrapExists: boolean;
271+state: WorkspaceSetupState;
272+};
273+274+async function reconcileWorkspaceBootstrapCompletionState(params: {
275+dir: string;
276+bootstrapPath: string;
277+statePath: string;
278+state: WorkspaceSetupState;
279+bootstrapExists?: boolean;
280+}): Promise<WorkspaceBootstrapCompletionReconcileResult> {
281+const bootstrapExists = params.bootstrapExists ?? (await fileExists(params.bootstrapPath));
282+if (
283+typeof params.state.setupCompletedAt === "string" &&
284+params.state.setupCompletedAt.trim().length > 0
285+) {
286+return { repaired: false, bootstrapExists, state: params.state };
287+}
288+289+if (params.state.bootstrapSeededAt && !bootstrapExists) {
290+const completedState: WorkspaceSetupState = {
291+ ...params.state,
292+setupCompletedAt: new Date().toISOString(),
293+};
294+await writeWorkspaceSetupState(params.statePath, completedState);
295+return { repaired: true, bootstrapExists: false, state: completedState };
296+}
297+298+if (
299+!bootstrapExists ||
300+!(await workspaceHasBootstrapCompletionEvidence({
301+dir: params.dir,
302+}))
303+) {
304+return { repaired: false, bootstrapExists, state: params.state };
305+}
306+307+const now = new Date().toISOString();
308+const repairedState: WorkspaceSetupState = {
309+ ...params.state,
310+bootstrapSeededAt: params.state.bootstrapSeededAt ?? now,
311+setupCompletedAt: now,
312+};
313+await fs.rm(params.bootstrapPath, { force: true });
314+await writeWorkspaceSetupState(params.statePath, repairedState);
315+return { repaired: true, bootstrapExists: false, state: repairedState };
316+}
317+208318function resolveWorkspaceStatePath(dir: string): string {
209319return path.join(dir, WORKSPACE_STATE_DIRNAME, WORKSPACE_STATE_FILENAME);
210320}
@@ -230,11 +340,15 @@ function parseWorkspaceSetupState(raw: string): WorkspaceSetupState | null {
230340}
231341}
232342233-async function readWorkspaceSetupState(statePath: string): Promise<WorkspaceSetupState> {
343+async function readWorkspaceSetupState(
344+statePath: string,
345+opts?: { persistLegacyMigration?: boolean },
346+): Promise<WorkspaceSetupState> {
234347try {
235348const raw = await fs.readFile(statePath, "utf-8");
236349const parsed = parseWorkspaceSetupState(raw);
237350if (
351+opts?.persistLegacyMigration &&
238352parsed &&
239353raw.includes('"onboardingCompletedAt"') &&
240354!raw.includes('"setupCompletedAt"') &&
@@ -268,18 +382,40 @@ export async function resolveWorkspaceBootstrapStatus(
268382dir: string,
269383): Promise<"pending" | "complete"> {
270384const resolvedDir = resolveUserPath(dir);
271-const state = await readWorkspaceSetupStateForDir(resolvedDir);
385+const statePath = resolveWorkspaceStatePath(resolvedDir);
386+const state = await readWorkspaceSetupState(statePath);
272387if (typeof state.setupCompletedAt === "string" && state.setupCompletedAt.trim().length > 0) {
273388return "complete";
274389}
275-const bootstrapExists = await fileExists(path.join(resolvedDir, DEFAULT_BOOTSTRAP_FILENAME));
276-return bootstrapExists ? "pending" : "complete";
390+const bootstrapPath = path.join(resolvedDir, DEFAULT_BOOTSTRAP_FILENAME);
391+const bootstrapExists = await fileExists(bootstrapPath);
392+if (!bootstrapExists) {
393+return "complete";
394+}
395+return "pending";
277396}
278397279398export async function isWorkspaceBootstrapPending(dir: string): Promise<boolean> {
280399return (await resolveWorkspaceBootstrapStatus(dir)) === "pending";
281400}
282401402+export async function reconcileWorkspaceBootstrapCompletion(
403+dir: string,
404+): Promise<WorkspaceBootstrapCompletionReconcileResult> {
405+const resolvedDir = resolveUserPath(dir);
406+const statePath = resolveWorkspaceStatePath(resolvedDir);
407+const bootstrapPath = path.join(resolvedDir, DEFAULT_BOOTSTRAP_FILENAME);
408+const state = await readWorkspaceSetupState(statePath, {
409+persistLegacyMigration: true,
410+});
411+return await reconcileWorkspaceBootstrapCompletionState({
412+dir: resolvedDir,
413+ bootstrapPath,
414+ statePath,
415+ state,
416+});
417+}
418+283419async function writeWorkspaceSetupState(
284420statePath: string,
285421state: WorkspaceSetupState,
@@ -401,7 +537,9 @@ export async function ensureAgentWorkspace(params?: {
401537await writeFileIfMissing(userPath, userTemplate);
402538await writeFileIfMissing(heartbeatPath, heartbeatTemplate);
403539404-let state = await readWorkspaceSetupState(statePath);
540+let state = await readWorkspaceSetupState(statePath, {
541+persistLegacyMigration: true,
542+});
405543let stateDirty = false;
406544const markState = (next: Partial<WorkspaceSetupState>) => {
407545state = { ...state, ...next };
@@ -414,33 +552,31 @@ export async function ensureAgentWorkspace(params?: {
414552markState({ bootstrapSeededAt: nowIso() });
415553}
416554417-if (!state.setupCompletedAt && state.bootstrapSeededAt && !bootstrapExists) {
418-markState({ setupCompletedAt: nowIso() });
555+if (!state.setupCompletedAt) {
556+const repair = await reconcileWorkspaceBootstrapCompletionState({
557+ dir,
558+ bootstrapPath,
559+ statePath,
560+ state,
561+ bootstrapExists,
562+});
563+if (repair.repaired) {
564+state = repair.state;
565+stateDirty = false;
566+bootstrapExists = repair.bootstrapExists;
567+}
419568}
420569421570if (!state.bootstrapSeededAt && !state.setupCompletedAt && !bootstrapExists) {
422571// Legacy migration path: if USER/IDENTITY diverged from templates, or if user-content
423572// indicators exist, treat setup as complete and avoid recreating BOOTSTRAP for
424573// already-configured workspaces.
425-const [identityContent, userContent] = await Promise.all([
426-fs.readFile(identityPath, "utf-8"),
427-fs.readFile(userPath, "utf-8"),
428-]);
429-const hasUserContent = await (async () => {
430-const indicators = [path.join(dir, "memory"), path.join(dir, ".git")];
431-for (const indicator of indicators) {
432-try {
433-await fs.access(indicator);
434-return true;
435-} catch {
436-// continue
437-}
438-}
439-return await exactWorkspaceEntryExists(dir, DEFAULT_MEMORY_FILENAME);
440-})();
441-const legacySetupCompleted =
442-identityContent !== identityTemplate || userContent !== userTemplate || hasUserContent;
443-if (legacySetupCompleted) {
574+if (
575+await workspaceProfileLooksConfigured({
576+ dir,
577+includeGitEvidence: true,
578+})
579+) {
444580markState({ setupCompletedAt: nowIso() });
445581} else {
446582const bootstrapTemplate = await loadTemplate(DEFAULT_BOOTSTRAP_FILENAME);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。