@okclaw-build/cli 1.0.0-beta.50 → 1.0.0-beta.52
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/dist/commands/backup.js +11 -3
- package/dist/commands/backup.js.map +1 -1
- package/dist/commands/install.js +30 -17
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/restore.js +11 -3
- package/dist/commands/restore.js.map +1 -1
- package/dist/commands/skill.js +37 -14
- package/dist/commands/skill.js.map +1 -1
- package/dist/index.js +4 -4
- package/dist/installers/base.d.ts +2 -1
- package/dist/installers/channel.d.ts +3 -2
- package/dist/installers/channel.js +32 -26
- package/dist/installers/channel.js.map +1 -1
- package/dist/installers/openclaw.js +25 -12
- package/dist/installers/openclaw.js.map +1 -1
- package/dist/installers/skill.d.ts +3 -2
- package/dist/installers/skill.js +39 -20
- package/dist/installers/skill.js.map +1 -1
- package/dist/openclaw-user-data.d.ts +17 -2
- package/dist/openclaw-user-data.js +149 -21
- package/dist/openclaw-user-data.js.map +1 -1
- package/dist/utils/constants.d.ts +0 -8
- package/dist/utils/constants.js +0 -14
- package/dist/utils/constants.js.map +1 -1
- package/dist/utils/install-safety.d.ts +25 -0
- package/dist/utils/install-safety.js +95 -0
- package/dist/utils/install-safety.js.map +1 -0
- package/dist/utils/shell.d.ts +11 -0
- package/dist/utils/shell.js +58 -0
- package/dist/utils/shell.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, cpSync, mkdirSync, rmSync } from 'node:fs';
|
|
2
|
-
import {
|
|
2
|
+
import { runCommand, runCommandRetry, shell, shellCapture } from '../utils/shell.js';
|
|
3
3
|
import { ensureNodeVersion, checkDiskSpace } from '../utils/deps.js';
|
|
4
4
|
import { info, warn } from '../utils/logger.js';
|
|
5
5
|
import { stopDaemonForExclusiveAccess, stopGateway, cleanupGatewayServiceFiles, } from '../utils/openclaw-daemon.js';
|
|
@@ -24,7 +24,16 @@ export class OpenclawInstaller {
|
|
|
24
24
|
info(`Installing openclaw@${version}...`);
|
|
25
25
|
// --ignore-scripts: skip postinstall to avoid @matrix-org/matrix-sdk-crypto-nodejs
|
|
26
26
|
// which downloads ~40MB binary from GitHub (blocked/slow in China)
|
|
27
|
-
await
|
|
27
|
+
await runCommandRetry('npm', [
|
|
28
|
+
'install',
|
|
29
|
+
'-g',
|
|
30
|
+
`openclaw@${version}`,
|
|
31
|
+
'--registry',
|
|
32
|
+
NPM_REGISTRY,
|
|
33
|
+
'--no-audit',
|
|
34
|
+
'--no-fund',
|
|
35
|
+
'--ignore-scripts',
|
|
36
|
+
]);
|
|
28
37
|
// Remove the problematic postinstall script, then rebuild everything else
|
|
29
38
|
info('Running native module builds...');
|
|
30
39
|
await shell(`rm -f $(npm root -g)/openclaw/node_modules/@matrix-org/matrix-sdk-crypto-nodejs/download-lib.js 2>/dev/null || true`);
|
|
@@ -53,7 +62,7 @@ export class OpenclawInstaller {
|
|
|
53
62
|
// after the full install pipeline completes.
|
|
54
63
|
await stopDaemonForExclusiveAccess('openclaw deployment defaults');
|
|
55
64
|
info('Applying openclaw deployment defaults...');
|
|
56
|
-
await
|
|
65
|
+
await runCommand('openclaw', ['config', 'unset', 'plugins.slots.contextEngine']).catch(() => {
|
|
57
66
|
warn('Failed to unset legacy contextEngine slot, continuing...');
|
|
58
67
|
});
|
|
59
68
|
// openclaw 2026.4.26 起,bundled plugin 注册路径变了:dist/extensions/memory-core
|
|
@@ -61,15 +70,15 @@ export class OpenclawInstaller {
|
|
|
61
70
|
// "plugin not found: memory-core" 失败。当前没找到 v2026.4.26 下注册它的官方手段
|
|
62
71
|
// (doctor/onboard/postinstall 都不补),先 catch 住不让整个安装挂掉,让 onboard
|
|
63
72
|
// 留下的默认 memory slot 跑起来;后续找到正解再去掉这层兜底。
|
|
64
|
-
await
|
|
73
|
+
await runCommand('openclaw', ['config', 'set', 'plugins.slots.memory', '--', 'memory-core']).catch(() => {
|
|
65
74
|
warn('Failed to set memory slot to memory-core (plugin not registered), continuing...');
|
|
66
75
|
});
|
|
67
76
|
// OKClaw 部署的 OpenClaw 实例不需要主动 agent heartbeat;设为 0m
|
|
68
77
|
// 是持久关闭,必须在 gateway 启动前写入配置。
|
|
69
|
-
await
|
|
78
|
+
await runCommand('openclaw', ['config', 'set', 'agents.defaults.heartbeat.every', '--', '0m']).catch(() => {
|
|
70
79
|
warn('Failed to disable OpenClaw heartbeat, continuing...');
|
|
71
80
|
});
|
|
72
|
-
await
|
|
81
|
+
await runCommand('openclaw', ['config', 'set', 'tools.profile', '--', 'full']);
|
|
73
82
|
info(`openclaw@${version} installed.`);
|
|
74
83
|
}
|
|
75
84
|
/**
|
|
@@ -113,14 +122,18 @@ export class OpenclawInstaller {
|
|
|
113
122
|
}
|
|
114
123
|
}
|
|
115
124
|
// Full replace via `openclaw config set ... --replace`. --strict-json
|
|
116
|
-
// forces JSON parsing (no raw-string fallback).
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
125
|
+
// forces JSON parsing (no raw-string fallback).
|
|
126
|
+
await runCommand('openclaw', [
|
|
127
|
+
'config',
|
|
128
|
+
'set',
|
|
129
|
+
'models.providers',
|
|
130
|
+
JSON.stringify(config),
|
|
131
|
+
'--strict-json',
|
|
132
|
+
'--replace',
|
|
133
|
+
]);
|
|
121
134
|
info(`Replaced models.providers with: ${Object.keys(config).join(', ')}`);
|
|
122
135
|
if (defaultModel) {
|
|
123
|
-
await
|
|
136
|
+
await runCommand('openclaw', ['config', 'set', 'agents.defaults.model.primary', '--', defaultModel]);
|
|
124
137
|
info(`Default model: ${defaultModel}`);
|
|
125
138
|
}
|
|
126
139
|
info('openclaw configured.');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openclaw.js","sourceRoot":"","sources":["../../src/installers/openclaw.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEhE,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"openclaw.js","sourceRoot":"","sources":["../../src/installers/openclaw.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { Installer } from './base.js';
|
|
2
|
+
import type { PackageSource } from '../utils/install-safety.js';
|
|
2
3
|
export declare function findEffectiveSkillRoot(extractDir: string): string;
|
|
3
4
|
export declare class SkillInstaller implements Installer {
|
|
4
|
-
private skillName;
|
|
5
|
+
private readonly skillName;
|
|
5
6
|
constructor(skillName: string);
|
|
6
7
|
checkDeps(): Promise<void>;
|
|
7
|
-
install(_version: string,
|
|
8
|
+
install(_version: string, packageSource?: PackageSource): Promise<void>;
|
|
8
9
|
configure(_config: Record<string, unknown>): Promise<void>;
|
|
9
10
|
uninstall(): Promise<void>;
|
|
10
11
|
}
|
package/dist/installers/skill.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
2
|
-
import {
|
|
1
|
+
import { copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { runCommand, runCommandCapture } from '../utils/shell.js';
|
|
3
4
|
import { checkDiskSpace, ensureCommand } from '../utils/deps.js';
|
|
4
5
|
import { info, warn } from '../utils/logger.js';
|
|
5
6
|
import { OKCLAW_TMP_DIR, OPENCLAW_WORKSPACE } from '../utils/constants.js';
|
|
7
|
+
import { assertSafeInstallName, skillInstallDir, tmpInstallPath } from '../utils/install-safety.js';
|
|
6
8
|
const SKILLS_DIR = `${OPENCLAW_WORKSPACE}/skills`;
|
|
7
9
|
// 与 okclaw-skill-api SkillUploadServiceImpl.hasSkillMdAtAcceptedRoot 对齐:上传端接受
|
|
8
10
|
// (a) SKILL.md 在 zip 根;(b) SKILL.md 在唯一顶层目录下。CLI 解压必须同样支持,
|
|
@@ -10,8 +12,8 @@ const SKILLS_DIR = `${OPENCLAW_WORKSPACE}/skills`;
|
|
|
10
12
|
export function findEffectiveSkillRoot(extractDir) {
|
|
11
13
|
const entries = readdirSync(extractDir);
|
|
12
14
|
if (entries.length === 1) {
|
|
13
|
-
const candidate =
|
|
14
|
-
if (statSync(candidate).isDirectory() && existsSync(
|
|
15
|
+
const candidate = join(extractDir, entries[0]);
|
|
16
|
+
if (statSync(candidate).isDirectory() && existsSync(join(candidate, 'SKILL.md'))) {
|
|
15
17
|
return candidate;
|
|
16
18
|
}
|
|
17
19
|
}
|
|
@@ -20,59 +22,59 @@ export function findEffectiveSkillRoot(extractDir) {
|
|
|
20
22
|
export class SkillInstaller {
|
|
21
23
|
skillName;
|
|
22
24
|
constructor(skillName) {
|
|
23
|
-
this.skillName = skillName;
|
|
25
|
+
this.skillName = assertSafeInstallName('skill', skillName);
|
|
24
26
|
}
|
|
25
27
|
async checkDeps() {
|
|
26
28
|
await checkDiskSpace(100);
|
|
27
29
|
await ensureCommand('unzip');
|
|
28
30
|
}
|
|
29
|
-
async install(_version,
|
|
30
|
-
if (!
|
|
31
|
+
async install(_version, packageSource) {
|
|
32
|
+
if (!packageSource) {
|
|
31
33
|
throw new Error('--package-url is required for skill installation');
|
|
32
34
|
}
|
|
33
35
|
mkdirSync(OKCLAW_TMP_DIR, { recursive: true });
|
|
34
36
|
mkdirSync(SKILLS_DIR, { recursive: true });
|
|
35
|
-
const archive = `${
|
|
37
|
+
const archive = tmpInstallPath(`${this.skillName}-skill.zip`);
|
|
36
38
|
// Download
|
|
37
|
-
if (
|
|
38
|
-
info(`Downloading skill ${this.skillName} from ${
|
|
39
|
-
await
|
|
39
|
+
if (packageSource.kind === 'remote') {
|
|
40
|
+
info(`Downloading skill ${this.skillName} from ${packageSource.url}...`);
|
|
41
|
+
await runCommand('curl', ['-fsSL', '--connect-timeout', '30', '--retry', '3', '-o', archive, packageSource.url]);
|
|
40
42
|
}
|
|
41
|
-
else if (existsSync(
|
|
42
|
-
|
|
43
|
+
else if (existsSync(packageSource.path)) {
|
|
44
|
+
copyFileSync(packageSource.path, archive);
|
|
43
45
|
}
|
|
44
46
|
else {
|
|
45
|
-
throw new Error(`Package not found: ${
|
|
47
|
+
throw new Error(`Package not found: ${packageSource.path}`);
|
|
46
48
|
}
|
|
47
49
|
// Remove existing skill directory if present (overwrite)
|
|
48
|
-
const skillDir =
|
|
50
|
+
const skillDir = skillInstallDir(this.skillName);
|
|
49
51
|
if (existsSync(skillDir)) {
|
|
50
52
|
info(`Overwriting existing skill at ${skillDir}...`);
|
|
51
53
|
rmSync(skillDir, { recursive: true, force: true });
|
|
52
54
|
}
|
|
53
55
|
// Extract to staging dir, then move effective root into skillDir
|
|
54
56
|
// (handles ClaudeCode-style 和 ClawHub-style 两种布局,与后端上传接受范围一致)
|
|
55
|
-
const stageDir = `${
|
|
57
|
+
const stageDir = tmpInstallPath(`${this.skillName}-extract`);
|
|
56
58
|
rmSync(stageDir, { recursive: true, force: true });
|
|
57
59
|
mkdirSync(stageDir, { recursive: true });
|
|
58
60
|
info(`Extracting skill to ${skillDir}...`);
|
|
59
|
-
const unzipResult = await
|
|
61
|
+
const unzipResult = await runCommandCapture('unzip', ['-o', archive, '-d', stageDir]);
|
|
60
62
|
if (unzipResult.code > 1) {
|
|
61
63
|
throw new Error(`unzip failed (exit ${unzipResult.code}): ${unzipResult.stderr}`);
|
|
62
64
|
}
|
|
63
65
|
const sourceDir = findEffectiveSkillRoot(stageDir);
|
|
64
66
|
mkdirSync(skillDir, { recursive: true });
|
|
65
|
-
|
|
67
|
+
moveDirectoryContents(sourceDir, skillDir);
|
|
66
68
|
rmSync(stageDir, { recursive: true, force: true });
|
|
67
69
|
// Cleanup
|
|
68
|
-
|
|
70
|
+
rmSync(archive, { force: true });
|
|
69
71
|
info(`Skill ${this.skillName} installed at ${skillDir}`);
|
|
70
72
|
}
|
|
71
73
|
async configure(_config) {
|
|
72
74
|
// Skills don't need extra configuration
|
|
73
75
|
}
|
|
74
76
|
async uninstall() {
|
|
75
|
-
const skillDir =
|
|
77
|
+
const skillDir = skillInstallDir(this.skillName);
|
|
76
78
|
if (!existsSync(skillDir)) {
|
|
77
79
|
warn(`Skill ${this.skillName} not found at ${skillDir}, skipping`);
|
|
78
80
|
return;
|
|
@@ -82,4 +84,21 @@ export class SkillInstaller {
|
|
|
82
84
|
info(`Skill ${this.skillName} uninstalled`);
|
|
83
85
|
}
|
|
84
86
|
}
|
|
87
|
+
function moveDirectoryContents(sourceDir, targetDir) {
|
|
88
|
+
for (const entry of readdirSync(sourceDir)) {
|
|
89
|
+
const src = join(sourceDir, entry);
|
|
90
|
+
const dest = join(targetDir, entry);
|
|
91
|
+
rmSync(dest, { recursive: true, force: true });
|
|
92
|
+
try {
|
|
93
|
+
renameSync(src, dest);
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
if (e.code !== 'EXDEV') {
|
|
97
|
+
throw e;
|
|
98
|
+
}
|
|
99
|
+
cpSync(src, dest, { recursive: true, force: true });
|
|
100
|
+
rmSync(src, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
85
104
|
//# sourceMappingURL=skill.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skill.js","sourceRoot":"","sources":["../../src/installers/skill.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"skill.js","sourceRoot":"","sources":["../../src/installers/skill.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
export declare const OPENCLAW_USER_DATA_FILES: readonly ["MEMORY.md", "USER.md", "IDENTITY.md", "SOUL.md", "TOOLS.md", "AGENTS.md", "HEARTBEAT.md"];
|
|
2
2
|
export declare const LEGACY_OPENCLAW_WORKSPACE = "/root/openclaw/workspace";
|
|
3
|
+
type SkipReporter = (relativePath: string, reason: string) => void;
|
|
4
|
+
type BackupOptions = {
|
|
5
|
+
includeWorkspaceExtra?: boolean;
|
|
6
|
+
onSkip?: SkipReporter;
|
|
7
|
+
};
|
|
8
|
+
type RestoreOptions = {
|
|
9
|
+
includeWorkspaceExtra?: boolean;
|
|
10
|
+
};
|
|
3
11
|
export type BackupFs = {
|
|
4
12
|
existsSync(path: string): boolean;
|
|
5
13
|
mkdirSync(path: string, options?: {
|
|
@@ -11,6 +19,10 @@ export type BackupFs = {
|
|
|
11
19
|
statSync(path: string): {
|
|
12
20
|
isDirectory(): boolean;
|
|
13
21
|
};
|
|
22
|
+
readdirSync?: (path: string, options?: {
|
|
23
|
+
withFileTypes?: boolean;
|
|
24
|
+
}) => Array<DirentLike>;
|
|
25
|
+
readlinkSync?: (path: string) => string;
|
|
14
26
|
};
|
|
15
27
|
export type ArchiveFs = BackupFs & {
|
|
16
28
|
readdirSync(path: string, options?: {
|
|
@@ -34,6 +46,7 @@ type DirentLike = {
|
|
|
34
46
|
name: string;
|
|
35
47
|
isDirectory(): boolean;
|
|
36
48
|
isFile(): boolean;
|
|
49
|
+
isSymbolicLink?(): boolean;
|
|
37
50
|
};
|
|
38
51
|
export type ArchiveResult = {
|
|
39
52
|
backupDir: string | null;
|
|
@@ -50,17 +63,19 @@ type ShellOps = {
|
|
|
50
63
|
capture?(command: string): Promise<string>;
|
|
51
64
|
};
|
|
52
65
|
export declare function formatBackupTimestamp(now: Date): string;
|
|
53
|
-
export declare function backupOpenclawUserData(now?: Date, fsOps?: BackupFs): string | null;
|
|
66
|
+
export declare function backupOpenclawUserData(now?: Date, fsOps?: BackupFs, options?: BackupOptions): string | null;
|
|
54
67
|
export declare function createOpenclawUserDataArchive(options?: {
|
|
55
68
|
archivePath?: string;
|
|
56
69
|
now?: Date;
|
|
57
70
|
fsOps?: ArchiveFs;
|
|
58
71
|
shellOps?: ShellOps;
|
|
72
|
+
includeWorkspaceExtra?: boolean;
|
|
73
|
+
onSkip?: SkipReporter;
|
|
59
74
|
}): Promise<ArchiveResult>;
|
|
60
75
|
export declare function restoreOpenclawUserDataArchive(options: {
|
|
61
76
|
archivePath: string;
|
|
62
77
|
now?: Date;
|
|
63
78
|
fsOps?: RestoreFs;
|
|
64
79
|
shellOps?: ShellOps;
|
|
65
|
-
}): Promise<RestoreResult>;
|
|
80
|
+
} & RestoreOptions): Promise<RestoreResult>;
|
|
66
81
|
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { existsSync, cpSync, mkdirSync, rmSync, statSync, lstatSync, chmodSync, readdirSync, } from 'node:fs';
|
|
1
|
+
import { existsSync, cpSync, mkdirSync, rmSync, statSync, lstatSync, chmodSync, readdirSync, readlinkSync, } from 'node:fs';
|
|
2
2
|
import { posix as pathPosix } from 'node:path';
|
|
3
3
|
import { shell, shellCapture } from './utils/shell.js';
|
|
4
|
+
import { warn } from './utils/logger.js';
|
|
4
5
|
import { OKCLAW_BACKUP_DIR, OPENCLAW_WORKSPACE } from './utils/constants.js';
|
|
5
6
|
export const OPENCLAW_USER_DATA_FILES = [
|
|
6
7
|
'MEMORY.md',
|
|
@@ -15,8 +16,14 @@ export const LEGACY_OPENCLAW_WORKSPACE = '/root/openclaw/workspace';
|
|
|
15
16
|
const WORKSPACE_BACKUP_DIR = 'workspace';
|
|
16
17
|
const LEGACY_WORKSPACE_BACKUP_DIR = 'workspace-root-openclaw';
|
|
17
18
|
const SKILLS_DIR = 'skills';
|
|
18
|
-
const
|
|
19
|
-
|
|
19
|
+
const realBackupFs = {
|
|
20
|
+
existsSync,
|
|
21
|
+
mkdirSync,
|
|
22
|
+
cpSync,
|
|
23
|
+
statSync,
|
|
24
|
+
readdirSync: readdirSync,
|
|
25
|
+
readlinkSync,
|
|
26
|
+
};
|
|
20
27
|
const realArchiveFs = { ...realBackupFs, readdirSync: readdirSync };
|
|
21
28
|
const realRestoreFs = {
|
|
22
29
|
...realBackupFs,
|
|
@@ -34,7 +41,7 @@ function copyIfExists(fsOps, src, dest, recursive = false) {
|
|
|
34
41
|
fsOps.cpSync(src, dest, recursive ? { recursive: true } : undefined);
|
|
35
42
|
return true;
|
|
36
43
|
}
|
|
37
|
-
export function backupOpenclawUserData(now = new Date(), fsOps = realBackupFs) {
|
|
44
|
+
export function backupOpenclawUserData(now = new Date(), fsOps = realBackupFs, options = {}) {
|
|
38
45
|
const backupDir = `${OKCLAW_BACKUP_DIR}/openclaw-user-data-${formatBackupTimestamp(now)}`;
|
|
39
46
|
const workspaces = Array.from(new Set([OPENCLAW_WORKSPACE, LEGACY_OPENCLAW_WORKSPACE]));
|
|
40
47
|
let copiedItems = 0;
|
|
@@ -42,9 +49,12 @@ export function backupOpenclawUserData(now = new Date(), fsOps = realBackupFs) {
|
|
|
42
49
|
for (const workspace of workspaces) {
|
|
43
50
|
if (!fsOps.existsSync(workspace))
|
|
44
51
|
continue;
|
|
45
|
-
const workspaceBackupName =
|
|
52
|
+
const workspaceBackupName = options.includeWorkspaceExtra
|
|
53
|
+
? (workspace === OPENCLAW_WORKSPACE ? WORKSPACE_BACKUP_DIR : LEGACY_WORKSPACE_BACKUP_DIR)
|
|
54
|
+
: (copiedWorkspaces === 0 ? WORKSPACE_BACKUP_DIR : LEGACY_WORKSPACE_BACKUP_DIR);
|
|
46
55
|
const workspaceBackupDir = `${backupDir}/${workspaceBackupName}`;
|
|
47
56
|
let workspaceDirCreated = false;
|
|
57
|
+
const copiedRootEntries = new Set();
|
|
48
58
|
const ensureWorkspaceBackupDir = () => {
|
|
49
59
|
if (workspaceDirCreated)
|
|
50
60
|
return;
|
|
@@ -58,6 +68,7 @@ export function backupOpenclawUserData(now = new Date(), fsOps = realBackupFs) {
|
|
|
58
68
|
ensureWorkspaceBackupDir();
|
|
59
69
|
if (copyIfExists(fsOps, src, `${workspaceBackupDir}/${file}`)) {
|
|
60
70
|
copiedItems += 1;
|
|
71
|
+
copiedRootEntries.add(file);
|
|
61
72
|
}
|
|
62
73
|
}
|
|
63
74
|
const skillsDir = `${workspace}/${SKILLS_DIR}`;
|
|
@@ -65,19 +76,107 @@ export function backupOpenclawUserData(now = new Date(), fsOps = realBackupFs) {
|
|
|
65
76
|
ensureWorkspaceBackupDir();
|
|
66
77
|
if (copyIfExists(fsOps, skillsDir, `${workspaceBackupDir}/${SKILLS_DIR}`, true)) {
|
|
67
78
|
copiedItems += 1;
|
|
79
|
+
copiedRootEntries.add(SKILLS_DIR);
|
|
68
80
|
}
|
|
69
81
|
}
|
|
82
|
+
if (options.includeWorkspaceExtra) {
|
|
83
|
+
copiedItems += copyWorkspaceExtraEntries(fsOps, workspace, workspaceBackupDir, copiedRootEntries, ensureWorkspaceBackupDir, options.onSkip ?? defaultBackupSkip);
|
|
84
|
+
}
|
|
70
85
|
if (workspaceDirCreated) {
|
|
71
86
|
copiedWorkspaces += 1;
|
|
72
87
|
}
|
|
73
88
|
}
|
|
74
89
|
return copiedItems > 0 ? backupDir : null;
|
|
75
90
|
}
|
|
91
|
+
function defaultBackupSkip(relativePath, reason) {
|
|
92
|
+
warn(`全量迁移跳过不可移植条目 ${relativePath}: ${reason}`);
|
|
93
|
+
}
|
|
94
|
+
// 全量模式只把"可移植"条目纳入归档,让归档天生可在目标主机还原:
|
|
95
|
+
// 普通文件、目录、以及目标仍落在 workspace 内的软链都保留;指向 workspace 之外的软链
|
|
96
|
+
// 和 socket/fifo 等特殊文件在 backup 阶段就跳过并告警,而不是搬进去等 restore 整包失败。
|
|
97
|
+
function copyWorkspaceExtraEntries(fsOps, workspace, workspaceBackupDir, copiedRootEntries, ensureWorkspaceBackupDir, onSkip) {
|
|
98
|
+
if (!fsOps.readdirSync) {
|
|
99
|
+
throw new Error('backup fs does not support readdirSync for full workspace backup');
|
|
100
|
+
}
|
|
101
|
+
let copied = 0;
|
|
102
|
+
for (const entry of fsOps.readdirSync(workspace, { withFileTypes: true })) {
|
|
103
|
+
if (copiedRootEntries.has(entry.name))
|
|
104
|
+
continue;
|
|
105
|
+
const src = `${workspace}/${entry.name}`;
|
|
106
|
+
const dest = `${workspaceBackupDir}/${entry.name}`;
|
|
107
|
+
const rel = entry.name;
|
|
108
|
+
if (entry.isSymbolicLink?.()) {
|
|
109
|
+
if (!isPortableSymlink(fsOps, src, rel, onSkip))
|
|
110
|
+
continue;
|
|
111
|
+
ensureWorkspaceBackupDir();
|
|
112
|
+
fsOps.cpSync(src, dest);
|
|
113
|
+
copied += 1;
|
|
114
|
+
copiedRootEntries.add(entry.name);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (entry.isDirectory()) {
|
|
118
|
+
ensureWorkspaceBackupDir();
|
|
119
|
+
copyPortableTree(fsOps, src, dest, rel, onSkip);
|
|
120
|
+
copied += 1;
|
|
121
|
+
copiedRootEntries.add(entry.name);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (entry.isFile()) {
|
|
125
|
+
ensureWorkspaceBackupDir();
|
|
126
|
+
fsOps.cpSync(src, dest);
|
|
127
|
+
copied += 1;
|
|
128
|
+
copiedRootEntries.add(entry.name);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
onSkip(rel, '不支持的文件类型');
|
|
132
|
+
}
|
|
133
|
+
return copied;
|
|
134
|
+
}
|
|
135
|
+
// 递归拷贝目录,逐项套用与 restore 校验一致的"可移植"判定,使嵌套软链/特殊文件也被过滤。
|
|
136
|
+
function copyPortableTree(fsOps, srcDir, destDir, relDir, onSkip) {
|
|
137
|
+
if (!fsOps.readdirSync) {
|
|
138
|
+
throw new Error('backup fs does not support readdirSync for full workspace backup');
|
|
139
|
+
}
|
|
140
|
+
fsOps.mkdirSync(destDir, { recursive: true });
|
|
141
|
+
for (const entry of fsOps.readdirSync(srcDir, { withFileTypes: true })) {
|
|
142
|
+
const src = `${srcDir}/${entry.name}`;
|
|
143
|
+
const dest = `${destDir}/${entry.name}`;
|
|
144
|
+
const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
145
|
+
if (entry.isSymbolicLink?.()) {
|
|
146
|
+
if (isPortableSymlink(fsOps, src, rel, onSkip)) {
|
|
147
|
+
fsOps.cpSync(src, dest);
|
|
148
|
+
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (entry.isDirectory()) {
|
|
152
|
+
copyPortableTree(fsOps, src, dest, rel, onSkip);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (entry.isFile()) {
|
|
156
|
+
fsOps.cpSync(src, dest);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
onSkip(rel, '不支持的文件类型');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function isPortableSymlink(fsOps, src, rel, onSkip) {
|
|
163
|
+
if (!fsOps.readlinkSync) {
|
|
164
|
+
throw new Error('backup fs does not support readlinkSync for full workspace backup');
|
|
165
|
+
}
|
|
166
|
+
const target = fsOps.readlinkSync(src);
|
|
167
|
+
if (symlinkTargetWithinWorkspace(rel, target))
|
|
168
|
+
return true;
|
|
169
|
+
onSkip(rel, `软链目标逃逸 workspace: ${target}`);
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
76
172
|
export async function createOpenclawUserDataArchive(options = {}) {
|
|
77
173
|
const now = options.now ?? new Date();
|
|
78
174
|
const fsOps = options.fsOps ?? realArchiveFs;
|
|
79
175
|
const shellOps = options.shellOps ?? { shell };
|
|
80
|
-
const backupDir = backupOpenclawUserData(now, fsOps
|
|
176
|
+
const backupDir = backupOpenclawUserData(now, fsOps, {
|
|
177
|
+
includeWorkspaceExtra: options.includeWorkspaceExtra,
|
|
178
|
+
onSkip: options.onSkip,
|
|
179
|
+
});
|
|
81
180
|
if (!backupDir) {
|
|
82
181
|
return { backupDir: null, archivePath: null, includedFiles: [] };
|
|
83
182
|
}
|
|
@@ -109,16 +208,21 @@ export async function restoreOpenclawUserDataArchive(options) {
|
|
|
109
208
|
validateArchiveEntries(entries);
|
|
110
209
|
fsOps.mkdirSync(OPENCLAW_WORKSPACE, { recursive: true });
|
|
111
210
|
fsOps.chmodSync(OPENCLAW_WORKSPACE, 0o755);
|
|
112
|
-
const restored = new
|
|
211
|
+
const restored = new Map();
|
|
113
212
|
restoreWorkspaceBackup(fsOps, `${stagingDir}/${WORKSPACE_BACKUP_DIR}`, restored);
|
|
114
213
|
restoreWorkspaceBackup(fsOps, `${stagingDir}/${LEGACY_WORKSPACE_BACKUP_DIR}`, restored);
|
|
115
|
-
|
|
214
|
+
if (options.includeWorkspaceExtra) {
|
|
215
|
+
restoreWorkspaceExtraEntries(fsOps, `${stagingDir}/${WORKSPACE_BACKUP_DIR}`, restored);
|
|
216
|
+
restoreWorkspaceExtraEntries(fsOps, `${stagingDir}/${LEGACY_WORKSPACE_BACKUP_DIR}`, restored);
|
|
217
|
+
}
|
|
218
|
+
const restoredFiles = Array.from(restored.keys()).sort();
|
|
116
219
|
for (const file of restoredFiles) {
|
|
117
220
|
const path = `${OPENCLAW_WORKSPACE}/${file}`;
|
|
118
|
-
|
|
221
|
+
const type = restored.get(file);
|
|
222
|
+
if (type === 'directory') {
|
|
119
223
|
applyRestoredPermissions(fsOps, path);
|
|
120
224
|
}
|
|
121
|
-
else {
|
|
225
|
+
else if (type === 'file') {
|
|
122
226
|
fsOps.chmodSync(path, 0o644);
|
|
123
227
|
}
|
|
124
228
|
}
|
|
@@ -149,13 +253,13 @@ function parseTarVerboseEntries(stdout) {
|
|
|
149
253
|
const modeType = line[0];
|
|
150
254
|
const tokens = line.split(/\s+/);
|
|
151
255
|
const rawPath = tokens.slice(5).join(' ');
|
|
152
|
-
const linkIndex = rawPath.indexOf(' -> ');
|
|
153
|
-
const path = linkIndex >= 0 ? rawPath.slice(0, linkIndex) : rawPath;
|
|
154
|
-
const linkTarget = linkIndex >= 0 ? rawPath.slice(linkIndex + ' -> '.length) : undefined;
|
|
155
256
|
const type = modeType === '-' ? 'file'
|
|
156
257
|
: modeType === 'd' ? 'directory'
|
|
157
258
|
: modeType === 'l' ? 'symlink'
|
|
158
259
|
: 'unsupported';
|
|
260
|
+
const linkIndex = type === 'symlink' ? rawPath.indexOf(' -> ') : -1;
|
|
261
|
+
const path = linkIndex >= 0 ? rawPath.slice(0, linkIndex) : rawPath;
|
|
262
|
+
const linkTarget = linkIndex >= 0 ? rawPath.slice(linkIndex + ' -> '.length) : undefined;
|
|
159
263
|
return { type, path, linkTarget, raw: line };
|
|
160
264
|
});
|
|
161
265
|
}
|
|
@@ -177,13 +281,20 @@ function assertSymlinkWithinWorkspace(entry) {
|
|
|
177
281
|
if (relative === null || !target) {
|
|
178
282
|
throw new Error(`unsafe archive symlink: ${entry.raw}`);
|
|
179
283
|
}
|
|
180
|
-
|
|
284
|
+
if (!symlinkTargetWithinWorkspace(relative, target)) {
|
|
285
|
+
throw new Error(`unsafe archive symlink target: ${entry.raw}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// backup 过滤与 restore 校验共用的判定:软链在还原到 OPENCLAW_WORKSPACE 后,
|
|
289
|
+
// 其目标是否仍落在 workspace 内。relativePath 是软链相对 workspace 根的路径。
|
|
290
|
+
function symlinkTargetWithinWorkspace(relativePath, target) {
|
|
291
|
+
if (!target)
|
|
292
|
+
return false;
|
|
293
|
+
const linkDir = pathPosix.dirname(`${OPENCLAW_WORKSPACE}/${relativePath}`);
|
|
181
294
|
const resolvedTarget = target.startsWith('/')
|
|
182
295
|
? pathPosix.normalize(target)
|
|
183
296
|
: pathPosix.normalize(pathPosix.join(linkDir, target));
|
|
184
|
-
|
|
185
|
-
throw new Error(`unsafe archive symlink target: ${entry.raw}`);
|
|
186
|
-
}
|
|
297
|
+
return resolvedTarget === OPENCLAW_WORKSPACE || resolvedTarget.startsWith(`${OPENCLAW_WORKSPACE}/`);
|
|
187
298
|
}
|
|
188
299
|
function workspaceRelativePath(entryPath) {
|
|
189
300
|
const normalized = normalizeArchiveEntry(entryPath);
|
|
@@ -205,13 +316,31 @@ function restoreWorkspaceBackup(fsOps, backupWorkspaceDir, restored) {
|
|
|
205
316
|
if (!fsOps.existsSync(src))
|
|
206
317
|
continue;
|
|
207
318
|
fsOps.cpSync(src, `${OPENCLAW_WORKSPACE}/${file}`);
|
|
208
|
-
restored.
|
|
319
|
+
restored.set(file, 'file');
|
|
209
320
|
}
|
|
210
321
|
const skillsSrc = `${backupWorkspaceDir}/${SKILLS_DIR}`;
|
|
211
322
|
if (!restored.has(SKILLS_DIR) && fsOps.existsSync(skillsSrc) && fsOps.statSync(skillsSrc).isDirectory()) {
|
|
212
323
|
fsOps.rmSync(`${OPENCLAW_WORKSPACE}/${SKILLS_DIR}`, { recursive: true, force: true });
|
|
213
324
|
fsOps.cpSync(skillsSrc, `${OPENCLAW_WORKSPACE}/${SKILLS_DIR}`, { recursive: true });
|
|
214
|
-
restored.
|
|
325
|
+
restored.set(SKILLS_DIR, 'directory');
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function restoreWorkspaceExtraEntries(fsOps, backupWorkspaceDir, restored) {
|
|
329
|
+
if (!fsOps.existsSync(backupWorkspaceDir))
|
|
330
|
+
return;
|
|
331
|
+
for (const entry of fsOps.readdirSync(backupWorkspaceDir, { withFileTypes: true })) {
|
|
332
|
+
if (restored.has(entry.name))
|
|
333
|
+
continue;
|
|
334
|
+
const src = `${backupWorkspaceDir}/${entry.name}`;
|
|
335
|
+
const dest = `${OPENCLAW_WORKSPACE}/${entry.name}`;
|
|
336
|
+
fsOps.rmSync(dest, { recursive: true, force: true });
|
|
337
|
+
if (entry.isDirectory()) {
|
|
338
|
+
fsOps.cpSync(src, dest, { recursive: true });
|
|
339
|
+
restored.set(entry.name, 'directory');
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
fsOps.cpSync(src, dest);
|
|
343
|
+
restored.set(entry.name, entry.isSymbolicLink?.() ? 'symlink' : 'file');
|
|
215
344
|
}
|
|
216
345
|
}
|
|
217
346
|
function applyRestoredPermissions(fsOps, path) {
|
|
@@ -256,8 +385,7 @@ function validateArchiveEntries(entries) {
|
|
|
256
385
|
continue;
|
|
257
386
|
const parts = normalized.split('/');
|
|
258
387
|
if (entry.startsWith('/') ||
|
|
259
|
-
parts.includes('..')
|
|
260
|
-
parts.includes(OPENCLAW_CONFIG_FILE)) {
|
|
388
|
+
parts.includes('..')) {
|
|
261
389
|
throw new Error(`unsafe archive entry: ${entry}`);
|
|
262
390
|
}
|
|
263
391
|
}
|