flower-trellis 0.5.2 → 0.5.3
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/enhancements/MANIFEST.json +2 -2
- package/enhancements/common/.common/.claude/skills/craft-rpa/SKILL.md +6 -7
- package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/launch.js +8 -6
- package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/logger.js +3 -1
- package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/test/runtime-layout.test.js +96 -0
- package/enhancements/common/.common/.claude/skills/craft-rpa/scripts/run.sh +72 -36
- package/enhancements/common/.common/.codex/skills/craft-rpa/SKILL.md +6 -7
- package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/launch.js +8 -6
- package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/logger.js +3 -1
- package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/test/runtime-layout.test.js +96 -0
- package/enhancements/common/.common/.codex/skills/craft-rpa/scripts/run.sh +72 -36
- package/package.json +3 -3
- package/src/builtin-plugins/skill-garden/content-adapter.js +97 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"syncedAt": "2026-07-
|
|
2
|
+
"syncedAt": "2026-07-29T12:22:03.171Z",
|
|
3
3
|
"syncedFrom": "vendor/skill-garden",
|
|
4
|
-
"sourceCommit": "
|
|
4
|
+
"sourceCommit": "1916ef11ba3cb40bead9ad827957224af850bd4f",
|
|
5
5
|
"common": {
|
|
6
6
|
"codexSkills": [
|
|
7
7
|
"craft-rpa",
|
|
@@ -70,7 +70,7 @@ bash "$SKILL_DIR/scripts/run.sh" craft [--session <ts>] [OUT]
|
|
|
70
70
|
# 转 jsonl → trace.md;--session 默认 = 当前/最新;OUT 默认 ./trace.md
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
运行时状态 / sessions / profile
|
|
73
|
+
运行时状态 / sessions / profile / 依赖全部在 `$(pwd)/.craft-rpa/`(可用 `CRAFT_RPA_HOME` env 覆盖到任意路径)。首次 `start` 自动安装依赖(跳浏览器下载,~10s),受管 skill 目录只保留静态代码资产。
|
|
74
74
|
|
|
75
75
|
**AI 行为约定**(看到下列触发就跑对应子命令,不要手敲底层 cd / nohup):
|
|
76
76
|
|
|
@@ -106,24 +106,23 @@ bash "$SKILL_DIR/scripts/run.sh" craft [--session <ts>] [OUT]
|
|
|
106
106
|
│ ├── 2026-05-18_14-22-15/
|
|
107
107
|
│ └── legacy-2026-05-17_... ← 老版本遗留 / 升级时自动归档
|
|
108
108
|
├── profile/ ← Chrome 持久 profile(登录态,项目独立)
|
|
109
|
+
├── runtime/recorder/ ← package manifest + node_modules,可重建
|
|
109
110
|
├── .launch.pid / .launch.log ← 进程管理 + 日志
|
|
110
111
|
└── .current-session ← 最近一次 start 的会话 ts
|
|
111
112
|
|
|
112
|
-
.claude/skills/craft-rpa/recorder/ ← skill
|
|
113
|
-
├── session.jsonl → <repo>/.craft-rpa/sessions/<latest>/session.jsonl
|
|
114
|
-
└── profile → <repo>/.craft-rpa/profile
|
|
113
|
+
.claude/skills/craft-rpa/recorder/ ← skill 内仅静态代码资产,不写 profile/session/node_modules
|
|
115
114
|
```
|
|
116
115
|
|
|
117
116
|
**为什么这样**:
|
|
118
117
|
- 录制 jsonl 是**项目业务数据**,跟着仓库走(每个仓库独立 session 池,不串)
|
|
119
118
|
- skill 代码可装 `~/.claude/skills/craft-rpa/` 全局,所有仓库共用同一份代码
|
|
120
|
-
-
|
|
119
|
+
- run.sh 通过显式环境变量把 session/profile/Playwright 模块路径传给 launch.js,避免运行时产物进入 Plugin 管理树
|
|
121
120
|
|
|
122
121
|
**关键性质**:
|
|
123
122
|
|
|
124
123
|
- 每次 `run.sh start` 创建新时间戳目录,**不覆盖**历史
|
|
125
|
-
- `recorder
|
|
126
|
-
- 老版本遗留的 `recorder/session.jsonl`
|
|
124
|
+
- 新版不会在 `recorder/` 创建软链或 `node_modules`;依赖位于 `.craft-rpa/runtime/recorder/`
|
|
125
|
+
- 老版本遗留的 `recorder/session.jsonl` / `recorder/profile` 软链只删除链接本身,真实目标数据保留;普通文件/目录仍自动归档
|
|
127
126
|
- `run.sh craft` 默认转最新;`--session <ts>` 可转任意历史
|
|
128
127
|
- 删历史:手动 `rm -rf .craft-rpa/sessions/<ts>/`,run.sh 不管删
|
|
129
128
|
|
|
@@ -18,19 +18,21 @@
|
|
|
18
18
|
* 没有系统 Chrome 时改成 false,并执行 npx playwright install chromium
|
|
19
19
|
*
|
|
20
20
|
* 数据存放:
|
|
21
|
-
* -
|
|
22
|
-
* -
|
|
21
|
+
* - CRAFT_RPA_PROFILE_DIR : 用户数据目录(cookie / localStorage / 登录态等)
|
|
22
|
+
* - CRAFT_RPA_SESSION_FILE : 事件日志(由内置 logger 写入)
|
|
23
23
|
*/
|
|
24
|
-
const { chromium } = require('playwright');
|
|
25
24
|
const path = require('path');
|
|
26
25
|
const fs = require('fs');
|
|
26
|
+
const PLAYWRIGHT_MODULE = process.env.CRAFT_RPA_PLAYWRIGHT_MODULE || 'playwright';
|
|
27
|
+
const { chromium } = require(PLAYWRIGHT_MODULE);
|
|
27
28
|
const { startLogger } = require('./logger');
|
|
28
29
|
|
|
29
30
|
// 启动参数:第一个 CLI 参数作为起始 URL,没传就开空白页
|
|
30
31
|
const START_URL = process.argv[2] || 'about:blank';
|
|
31
32
|
|
|
32
|
-
//
|
|
33
|
-
const PROFILE_DIR = path.join(__dirname, 'profile');
|
|
33
|
+
// run.sh 显式传入运行时目录;直接执行时保留原有默认值。
|
|
34
|
+
const PROFILE_DIR = path.resolve(process.env.CRAFT_RPA_PROFILE_DIR || path.join(__dirname, 'profile'));
|
|
35
|
+
const SESSION_LOG = path.resolve(process.env.CRAFT_RPA_SESSION_FILE || path.join(__dirname, 'session.jsonl'));
|
|
34
36
|
|
|
35
37
|
// 注入脚本路径
|
|
36
38
|
const INJECT_SCRIPT = path.join(__dirname, 'inject.js');
|
|
@@ -164,7 +166,7 @@ if (!fs.existsSync(INJECT_SCRIPT)) {
|
|
|
164
166
|
|
|
165
167
|
// 先启动日志 + 控制服务,再启动浏览器
|
|
166
168
|
// 浏览器一打开页面,注入脚本立刻就能 POST 成功,不会丢首批事件
|
|
167
|
-
const loggerServer = startLogger({ browserController });
|
|
169
|
+
const loggerServer = startLogger({ browserController, logFile: SESSION_LOG });
|
|
168
170
|
|
|
169
171
|
console.log(`[launch] profile dir : ${PROFILE_DIR}`);
|
|
170
172
|
console.log(`[launch] start url : ${START_URL}`);
|
|
@@ -16,7 +16,9 @@ const fs = require('fs');
|
|
|
16
16
|
const path = require('path');
|
|
17
17
|
|
|
18
18
|
const DEFAULT_PORT = 7777;
|
|
19
|
-
const DEFAULT_LOG_FILE = path.
|
|
19
|
+
const DEFAULT_LOG_FILE = path.resolve(
|
|
20
|
+
process.env.CRAFT_RPA_SESSION_FILE || path.join(__dirname, 'session.jsonl'),
|
|
21
|
+
);
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* 启动本地日志收集服务
|
package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/test/runtime-layout.test.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
const test = require('node:test');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 写入可执行的测试替身。
|
|
10
|
+
*
|
|
11
|
+
* @param {string} target 文件路径。
|
|
12
|
+
* @param {string} content 脚本内容。
|
|
13
|
+
* @return {void}
|
|
14
|
+
*/
|
|
15
|
+
function writeExecutable(target, content) {
|
|
16
|
+
fs.writeFileSync(target, content, { mode: 0o755 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test('run.sh 将旧运行时产物迁出 Plugin 管理树', { skip: process.platform === 'win32' }, t => {
|
|
20
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'craft-rpa-runtime-layout-'));
|
|
21
|
+
const sourceSkill = path.resolve(__dirname, '..', '..');
|
|
22
|
+
const skillRoot = path.join(tempRoot, 'skill');
|
|
23
|
+
const recorderRoot = path.join(skillRoot, 'recorder');
|
|
24
|
+
const projectRoot = path.join(tempRoot, 'project');
|
|
25
|
+
const dataRoot = path.join(projectRoot, '.craft-rpa');
|
|
26
|
+
const fakeBin = path.join(tempRoot, 'bin');
|
|
27
|
+
let launchPid = null;
|
|
28
|
+
|
|
29
|
+
t.after(() => {
|
|
30
|
+
if (launchPid !== null) {
|
|
31
|
+
try { process.kill(launchPid, 'SIGTERM'); } catch (error) { /* 测试进程可能已结束。 */ }
|
|
32
|
+
}
|
|
33
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
fs.cpSync(sourceSkill, skillRoot, { recursive: true });
|
|
37
|
+
fs.mkdirSync(projectRoot, { recursive: true });
|
|
38
|
+
fs.mkdirSync(fakeBin, { recursive: true });
|
|
39
|
+
|
|
40
|
+
const profileTarget = path.join(dataRoot, 'profile');
|
|
41
|
+
const sessionTarget = path.join(dataRoot, 'sessions', 'old', 'session.jsonl');
|
|
42
|
+
fs.mkdirSync(profileTarget, { recursive: true });
|
|
43
|
+
fs.mkdirSync(path.dirname(sessionTarget), { recursive: true });
|
|
44
|
+
fs.writeFileSync(path.join(profileTarget, 'preserved.txt'), 'profile\n');
|
|
45
|
+
fs.writeFileSync(sessionTarget, '{"kind":"legacy"}\n');
|
|
46
|
+
fs.symlinkSync(profileTarget, path.join(recorderRoot, 'profile'), 'dir');
|
|
47
|
+
fs.symlinkSync(sessionTarget, path.join(recorderRoot, 'session.jsonl'));
|
|
48
|
+
fs.mkdirSync(path.join(recorderRoot, 'node_modules', 'playwright'), { recursive: true });
|
|
49
|
+
fs.writeFileSync(path.join(recorderRoot, 'node_modules', 'playwright', 'package.json'), '{}\n');
|
|
50
|
+
|
|
51
|
+
writeExecutable(path.join(fakeBin, 'npm'), `#!/usr/bin/env bash
|
|
52
|
+
mkdir -p "$PWD/node_modules/playwright"
|
|
53
|
+
printf '{}\\n' > "$PWD/node_modules/playwright/package.json"
|
|
54
|
+
`);
|
|
55
|
+
writeExecutable(path.join(fakeBin, 'node'), `#!/usr/bin/env bash
|
|
56
|
+
printf '%s\\n' "$CRAFT_RPA_SESSION_FILE" > "$CRAFT_RPA_HOME/fake-session-file"
|
|
57
|
+
printf '%s\\n' "$CRAFT_RPA_PROFILE_DIR" > "$CRAFT_RPA_HOME/fake-profile-dir"
|
|
58
|
+
printf '%s\\n' "$CRAFT_RPA_PLAYWRIGHT_MODULE" > "$CRAFT_RPA_HOME/fake-playwright-module"
|
|
59
|
+
while true; do /bin/sleep 1; done
|
|
60
|
+
`);
|
|
61
|
+
|
|
62
|
+
const result = spawnSync('bash', [path.join(skillRoot, 'scripts', 'run.sh'), 'start', 'about:blank'], {
|
|
63
|
+
cwd: projectRoot,
|
|
64
|
+
env: {
|
|
65
|
+
...process.env,
|
|
66
|
+
CRAFT_RPA_HOME: dataRoot,
|
|
67
|
+
PATH: `${fakeBin}${path.delimiter}${process.env.PATH}`,
|
|
68
|
+
},
|
|
69
|
+
encoding: 'utf8',
|
|
70
|
+
timeout: 10000,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
74
|
+
launchPid = Number.parseInt(fs.readFileSync(path.join(dataRoot, '.launch.pid'), 'utf8'), 10);
|
|
75
|
+
assert.equal(Number.isInteger(launchPid), true);
|
|
76
|
+
assert.equal(fs.existsSync(path.join(recorderRoot, 'profile')), false);
|
|
77
|
+
assert.equal(fs.existsSync(path.join(recorderRoot, 'session.jsonl')), false);
|
|
78
|
+
assert.equal(fs.existsSync(path.join(recorderRoot, 'node_modules')), false);
|
|
79
|
+
assert.equal(fs.readFileSync(path.join(profileTarget, 'preserved.txt'), 'utf8'), 'profile\n');
|
|
80
|
+
assert.equal(fs.readFileSync(sessionTarget, 'utf8'), '{"kind":"legacy"}\n');
|
|
81
|
+
|
|
82
|
+
const runtimeRoot = path.join(dataRoot, 'runtime', 'recorder');
|
|
83
|
+
assert.equal(fs.existsSync(path.join(runtimeRoot, 'node_modules', 'playwright', 'package.json')), true);
|
|
84
|
+
assert.equal(
|
|
85
|
+
fs.readFileSync(path.join(dataRoot, 'fake-profile-dir'), 'utf8').trim(),
|
|
86
|
+
profileTarget,
|
|
87
|
+
);
|
|
88
|
+
assert.equal(
|
|
89
|
+
fs.readFileSync(path.join(dataRoot, 'fake-playwright-module'), 'utf8').trim(),
|
|
90
|
+
path.join(runtimeRoot, 'node_modules', 'playwright'),
|
|
91
|
+
);
|
|
92
|
+
const sessionFile = fs.readFileSync(path.join(dataRoot, 'fake-session-file'), 'utf8').trim();
|
|
93
|
+
assert.equal(sessionFile.startsWith(path.join(dataRoot, 'sessions')), true);
|
|
94
|
+
assert.equal(sessionFile.endsWith(path.join('', 'session.jsonl')), true);
|
|
95
|
+
assert.equal(fs.existsSync(sessionFile), true);
|
|
96
|
+
});
|
|
@@ -5,13 +5,11 @@
|
|
|
5
5
|
# ${CRAFT_RPA_HOME:-<cwd>/.craft-rpa}/
|
|
6
6
|
# ├── sessions/<YYYY-MM-DD_HH-MM-SS>/session.jsonl 每次 start 新建,不覆盖
|
|
7
7
|
# ├── profile/ Chrome 持久 profile(登录态)
|
|
8
|
+
# ├── runtime/recorder/node_modules/ 可重建运行时依赖
|
|
8
9
|
# ├── .launch.{pid,log} 进程管理 + 日志
|
|
9
10
|
# └── .current-session 最近一次 start 的 ts
|
|
10
11
|
#
|
|
11
|
-
# skill 内
|
|
12
|
-
# ├── session.jsonl → $CRAFT_RPA_HOME/sessions/<ts>/session.jsonl
|
|
13
|
-
# └── profile → $CRAFT_RPA_HOME/profile
|
|
14
|
-
# launch.js 用 __dirname 解析 session.jsonl / profile,通过软链落到项目根 .craft-rpa/
|
|
12
|
+
# skill 内 recorder/ 只保留静态代码资产;运行时路径通过环境变量显式传给 launch.js。
|
|
15
13
|
#
|
|
16
14
|
# 用法:
|
|
17
15
|
# bash run.sh start [URL]
|
|
@@ -34,13 +32,73 @@ PROFILE_DIR="$CRAFT_RPA_HOME/profile"
|
|
|
34
32
|
PID_FILE="$CRAFT_RPA_HOME/.launch.pid"
|
|
35
33
|
LOG_FILE="$CRAFT_RPA_HOME/.launch.log"
|
|
36
34
|
CURRENT_FILE="$CRAFT_RPA_HOME/.current-session"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
RUNTIME_DIR="$CRAFT_RPA_HOME/runtime/recorder"
|
|
36
|
+
RUNTIME_MODULES_DIR="$RUNTIME_DIR/node_modules"
|
|
37
|
+
LEGACY_SESSION_PATH="$RECORDER_DIR/session.jsonl"
|
|
38
|
+
LEGACY_PROFILE_PATH="$RECORDER_DIR/profile"
|
|
39
|
+
LEGACY_MODULES_DIR="$RECORDER_DIR/node_modules"
|
|
41
40
|
|
|
42
41
|
DASHBOARD_URL="http://localhost:7777/dashboard"
|
|
43
42
|
|
|
43
|
+
prepare_runtime_dependencies() {
|
|
44
|
+
mkdir -p "$RUNTIME_DIR"
|
|
45
|
+
RUNTIME_CHANGED=false
|
|
46
|
+
for MANIFEST in package.json package-lock.json; do
|
|
47
|
+
if [ ! -f "$RECORDER_DIR/$MANIFEST" ]; then
|
|
48
|
+
continue
|
|
49
|
+
fi
|
|
50
|
+
if [ ! -f "$RUNTIME_DIR/$MANIFEST" ] || ! cmp -s "$RECORDER_DIR/$MANIFEST" "$RUNTIME_DIR/$MANIFEST"; then
|
|
51
|
+
cp "$RECORDER_DIR/$MANIFEST" "$RUNTIME_DIR/$MANIFEST"
|
|
52
|
+
RUNTIME_CHANGED=true
|
|
53
|
+
fi
|
|
54
|
+
done
|
|
55
|
+
if [ "$RUNTIME_CHANGED" = true ] || [ ! -d "$RUNTIME_MODULES_DIR/playwright" ]; then
|
|
56
|
+
echo "[craft-rpa] installing deps under $RUNTIME_DIR (one-time, ~30-60s) ..."
|
|
57
|
+
if [ -f "$RUNTIME_DIR/package-lock.json" ]; then
|
|
58
|
+
(cd "$RUNTIME_DIR" && PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm ci --no-audit --no-fund)
|
|
59
|
+
else
|
|
60
|
+
(cd "$RUNTIME_DIR" && PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm install --no-audit --no-fund)
|
|
61
|
+
fi
|
|
62
|
+
fi
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
cleanup_legacy_recorder_runtime() {
|
|
66
|
+
if [ -L "$LEGACY_SESSION_PATH" ]; then
|
|
67
|
+
# 只删除旧软链本身,真实 session 目标继续留在数据根。
|
|
68
|
+
rm -f "$LEGACY_SESSION_PATH"
|
|
69
|
+
echo "[craft-rpa] removed legacy recorder/session.jsonl symlink"
|
|
70
|
+
elif [ -e "$LEGACY_SESSION_PATH" ]; then
|
|
71
|
+
if [ -s "$LEGACY_SESSION_PATH" ]; then
|
|
72
|
+
LEGACY_TS=$(date +"%Y-%m-%d_%H-%M-%S")
|
|
73
|
+
LEGACY_DIR="$SESSIONS_DIR/legacy-$LEGACY_TS"
|
|
74
|
+
mkdir -p "$LEGACY_DIR"
|
|
75
|
+
mv "$LEGACY_SESSION_PATH" "$LEGACY_DIR/session.jsonl"
|
|
76
|
+
echo "[craft-rpa] migrated legacy recorder/session.jsonl → sessions/legacy-$LEGACY_TS/"
|
|
77
|
+
else
|
|
78
|
+
rm -f "$LEGACY_SESSION_PATH"
|
|
79
|
+
fi
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
if [ -L "$LEGACY_PROFILE_PATH" ]; then
|
|
83
|
+
# 只删除旧软链本身,真实 profile 目标继续留在数据根。
|
|
84
|
+
rm -f "$LEGACY_PROFILE_PATH"
|
|
85
|
+
echo "[craft-rpa] removed legacy recorder/profile symlink"
|
|
86
|
+
elif [ -e "$LEGACY_PROFILE_PATH" ]; then
|
|
87
|
+
LEGACY_TS=$(date +"%Y-%m-%d_%H-%M-%S")
|
|
88
|
+
mv "$LEGACY_PROFILE_PATH" "$CRAFT_RPA_HOME/profile-legacy-$LEGACY_TS"
|
|
89
|
+
echo "[craft-rpa] migrated legacy recorder/profile → .craft-rpa/profile-legacy-$LEGACY_TS"
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
if [ -L "$LEGACY_MODULES_DIR" ]; then
|
|
93
|
+
rm -f "$LEGACY_MODULES_DIR"
|
|
94
|
+
echo "[craft-rpa] removed legacy recorder/node_modules symlink"
|
|
95
|
+
elif [ -e "$LEGACY_MODULES_DIR" ]; then
|
|
96
|
+
# node_modules 只是可重建缓存,精确删除受管目录中的旧副本,避免 Plugin 重放再次扫描它。
|
|
97
|
+
rm -rf "$LEGACY_MODULES_DIR"
|
|
98
|
+
echo "[craft-rpa] removed legacy recorder/node_modules cache"
|
|
99
|
+
fi
|
|
100
|
+
}
|
|
101
|
+
|
|
44
102
|
cmd="${1:-status}"
|
|
45
103
|
|
|
46
104
|
case "$cmd" in
|
|
@@ -60,44 +118,22 @@ case "$cmd" in
|
|
|
60
118
|
exit 0
|
|
61
119
|
fi
|
|
62
120
|
|
|
63
|
-
if [ ! -d "$RECORDER_DIR/node_modules" ]; then
|
|
64
|
-
echo "[craft-rpa] installing deps (one-time, ~30-60s) ..."
|
|
65
|
-
(cd "$RECORDER_DIR" && PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm install --no-audit --no-fund)
|
|
66
|
-
fi
|
|
67
|
-
|
|
68
121
|
mkdir -p "$CRAFT_RPA_HOME" "$SESSIONS_DIR" "$PROFILE_DIR"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if [ -e "$SESSION_LINK" ] && [ ! -L "$SESSION_LINK" ]; then
|
|
72
|
-
if [ -s "$SESSION_LINK" ]; then
|
|
73
|
-
LEGACY_TS=$(date +"%Y-%m-%d_%H-%M-%S")
|
|
74
|
-
LEGACY_DIR="$SESSIONS_DIR/legacy-$LEGACY_TS"
|
|
75
|
-
mkdir -p "$LEGACY_DIR"
|
|
76
|
-
mv "$SESSION_LINK" "$LEGACY_DIR/session.jsonl"
|
|
77
|
-
echo "[craft-rpa] migrated legacy recorder/session.jsonl → sessions/legacy-$LEGACY_TS/"
|
|
78
|
-
else
|
|
79
|
-
rm -f "$SESSION_LINK"
|
|
80
|
-
fi
|
|
81
|
-
fi
|
|
82
|
-
# 旧 profile 是普通目录(老版本遗留)→ 归档到数据根
|
|
83
|
-
if [ -e "$PROFILE_LINK" ] && [ ! -L "$PROFILE_LINK" ]; then
|
|
84
|
-
LEGACY_TS=$(date +"%Y-%m-%d_%H-%M-%S")
|
|
85
|
-
mv "$PROFILE_LINK" "$CRAFT_RPA_HOME/profile-legacy-$LEGACY_TS"
|
|
86
|
-
echo "[craft-rpa] migrated legacy recorder/profile → .craft-rpa/profile-legacy-$LEGACY_TS"
|
|
87
|
-
fi
|
|
122
|
+
cleanup_legacy_recorder_runtime
|
|
123
|
+
prepare_runtime_dependencies
|
|
88
124
|
|
|
89
125
|
TS=$(date +"%Y-%m-%d_%H-%M-%S")
|
|
90
126
|
NEW_SESSION_DIR="$SESSIONS_DIR/$TS"
|
|
91
127
|
mkdir -p "$NEW_SESSION_DIR"
|
|
92
128
|
: > "$NEW_SESSION_DIR/session.jsonl"
|
|
93
129
|
|
|
94
|
-
# 建两个软链:launch.js / logger.js 通过 __dirname 读到的就是项目根的数据
|
|
95
|
-
ln -sfn "$NEW_SESSION_DIR/session.jsonl" "$SESSION_LINK"
|
|
96
|
-
ln -sfn "$PROFILE_DIR" "$PROFILE_LINK"
|
|
97
130
|
echo "$TS" > "$CURRENT_FILE"
|
|
98
131
|
|
|
99
132
|
cd "$RECORDER_DIR"
|
|
100
|
-
|
|
133
|
+
CRAFT_RPA_SESSION_FILE="$NEW_SESSION_DIR/session.jsonl" \
|
|
134
|
+
CRAFT_RPA_PROFILE_DIR="$PROFILE_DIR" \
|
|
135
|
+
CRAFT_RPA_PLAYWRIGHT_MODULE="$RUNTIME_MODULES_DIR/playwright" \
|
|
136
|
+
nohup node launch.js "$URL" > "$LOG_FILE" 2>&1 &
|
|
101
137
|
PID=$!
|
|
102
138
|
echo "$PID" > "$PID_FILE"
|
|
103
139
|
sleep 1
|
|
@@ -70,7 +70,7 @@ bash "$SKILL_DIR/scripts/run.sh" craft [--session <ts>] [OUT]
|
|
|
70
70
|
# 转 jsonl → trace.md;--session 默认 = 当前/最新;OUT 默认 ./trace.md
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
运行时状态 / sessions / profile
|
|
73
|
+
运行时状态 / sessions / profile / 依赖全部在 `$(pwd)/.craft-rpa/`(可用 `CRAFT_RPA_HOME` env 覆盖到任意路径)。首次 `start` 自动安装依赖(跳浏览器下载,~10s),受管 skill 目录只保留静态代码资产。
|
|
74
74
|
|
|
75
75
|
**AI 行为约定**(看到下列触发就跑对应子命令,不要手敲底层 cd / nohup):
|
|
76
76
|
|
|
@@ -106,24 +106,23 @@ bash "$SKILL_DIR/scripts/run.sh" craft [--session <ts>] [OUT]
|
|
|
106
106
|
│ ├── 2026-05-18_14-22-15/
|
|
107
107
|
│ └── legacy-2026-05-17_... ← 老版本遗留 / 升级时自动归档
|
|
108
108
|
├── profile/ ← Chrome 持久 profile(登录态,项目独立)
|
|
109
|
+
├── runtime/recorder/ ← package manifest + node_modules,可重建
|
|
109
110
|
├── .launch.pid / .launch.log ← 进程管理 + 日志
|
|
110
111
|
└── .current-session ← 最近一次 start 的会话 ts
|
|
111
112
|
|
|
112
|
-
.claude/skills/craft-rpa/recorder/ ← skill
|
|
113
|
-
├── session.jsonl → <repo>/.craft-rpa/sessions/<latest>/session.jsonl
|
|
114
|
-
└── profile → <repo>/.craft-rpa/profile
|
|
113
|
+
.claude/skills/craft-rpa/recorder/ ← skill 内仅静态代码资产,不写 profile/session/node_modules
|
|
115
114
|
```
|
|
116
115
|
|
|
117
116
|
**为什么这样**:
|
|
118
117
|
- 录制 jsonl 是**项目业务数据**,跟着仓库走(每个仓库独立 session 池,不串)
|
|
119
118
|
- skill 代码可装 `~/.claude/skills/craft-rpa/` 全局,所有仓库共用同一份代码
|
|
120
|
-
-
|
|
119
|
+
- run.sh 通过显式环境变量把 session/profile/Playwright 模块路径传给 launch.js,避免运行时产物进入 Plugin 管理树
|
|
121
120
|
|
|
122
121
|
**关键性质**:
|
|
123
122
|
|
|
124
123
|
- 每次 `run.sh start` 创建新时间戳目录,**不覆盖**历史
|
|
125
|
-
- `recorder
|
|
126
|
-
- 老版本遗留的 `recorder/session.jsonl`
|
|
124
|
+
- 新版不会在 `recorder/` 创建软链或 `node_modules`;依赖位于 `.craft-rpa/runtime/recorder/`
|
|
125
|
+
- 老版本遗留的 `recorder/session.jsonl` / `recorder/profile` 软链只删除链接本身,真实目标数据保留;普通文件/目录仍自动归档
|
|
127
126
|
- `run.sh craft` 默认转最新;`--session <ts>` 可转任意历史
|
|
128
127
|
- 删历史:手动 `rm -rf .craft-rpa/sessions/<ts>/`,run.sh 不管删
|
|
129
128
|
|
|
@@ -18,19 +18,21 @@
|
|
|
18
18
|
* 没有系统 Chrome 时改成 false,并执行 npx playwright install chromium
|
|
19
19
|
*
|
|
20
20
|
* 数据存放:
|
|
21
|
-
* -
|
|
22
|
-
* -
|
|
21
|
+
* - CRAFT_RPA_PROFILE_DIR : 用户数据目录(cookie / localStorage / 登录态等)
|
|
22
|
+
* - CRAFT_RPA_SESSION_FILE : 事件日志(由内置 logger 写入)
|
|
23
23
|
*/
|
|
24
|
-
const { chromium } = require('playwright');
|
|
25
24
|
const path = require('path');
|
|
26
25
|
const fs = require('fs');
|
|
26
|
+
const PLAYWRIGHT_MODULE = process.env.CRAFT_RPA_PLAYWRIGHT_MODULE || 'playwright';
|
|
27
|
+
const { chromium } = require(PLAYWRIGHT_MODULE);
|
|
27
28
|
const { startLogger } = require('./logger');
|
|
28
29
|
|
|
29
30
|
// 启动参数:第一个 CLI 参数作为起始 URL,没传就开空白页
|
|
30
31
|
const START_URL = process.argv[2] || 'about:blank';
|
|
31
32
|
|
|
32
|
-
//
|
|
33
|
-
const PROFILE_DIR = path.join(__dirname, 'profile');
|
|
33
|
+
// run.sh 显式传入运行时目录;直接执行时保留原有默认值。
|
|
34
|
+
const PROFILE_DIR = path.resolve(process.env.CRAFT_RPA_PROFILE_DIR || path.join(__dirname, 'profile'));
|
|
35
|
+
const SESSION_LOG = path.resolve(process.env.CRAFT_RPA_SESSION_FILE || path.join(__dirname, 'session.jsonl'));
|
|
34
36
|
|
|
35
37
|
// 注入脚本路径
|
|
36
38
|
const INJECT_SCRIPT = path.join(__dirname, 'inject.js');
|
|
@@ -164,7 +166,7 @@ if (!fs.existsSync(INJECT_SCRIPT)) {
|
|
|
164
166
|
|
|
165
167
|
// 先启动日志 + 控制服务,再启动浏览器
|
|
166
168
|
// 浏览器一打开页面,注入脚本立刻就能 POST 成功,不会丢首批事件
|
|
167
|
-
const loggerServer = startLogger({ browserController });
|
|
169
|
+
const loggerServer = startLogger({ browserController, logFile: SESSION_LOG });
|
|
168
170
|
|
|
169
171
|
console.log(`[launch] profile dir : ${PROFILE_DIR}`);
|
|
170
172
|
console.log(`[launch] start url : ${START_URL}`);
|
|
@@ -16,7 +16,9 @@ const fs = require('fs');
|
|
|
16
16
|
const path = require('path');
|
|
17
17
|
|
|
18
18
|
const DEFAULT_PORT = 7777;
|
|
19
|
-
const DEFAULT_LOG_FILE = path.
|
|
19
|
+
const DEFAULT_LOG_FILE = path.resolve(
|
|
20
|
+
process.env.CRAFT_RPA_SESSION_FILE || path.join(__dirname, 'session.jsonl'),
|
|
21
|
+
);
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* 启动本地日志收集服务
|
package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/test/runtime-layout.test.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
const test = require('node:test');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 写入可执行的测试替身。
|
|
10
|
+
*
|
|
11
|
+
* @param {string} target 文件路径。
|
|
12
|
+
* @param {string} content 脚本内容。
|
|
13
|
+
* @return {void}
|
|
14
|
+
*/
|
|
15
|
+
function writeExecutable(target, content) {
|
|
16
|
+
fs.writeFileSync(target, content, { mode: 0o755 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test('run.sh 将旧运行时产物迁出 Plugin 管理树', { skip: process.platform === 'win32' }, t => {
|
|
20
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'craft-rpa-runtime-layout-'));
|
|
21
|
+
const sourceSkill = path.resolve(__dirname, '..', '..');
|
|
22
|
+
const skillRoot = path.join(tempRoot, 'skill');
|
|
23
|
+
const recorderRoot = path.join(skillRoot, 'recorder');
|
|
24
|
+
const projectRoot = path.join(tempRoot, 'project');
|
|
25
|
+
const dataRoot = path.join(projectRoot, '.craft-rpa');
|
|
26
|
+
const fakeBin = path.join(tempRoot, 'bin');
|
|
27
|
+
let launchPid = null;
|
|
28
|
+
|
|
29
|
+
t.after(() => {
|
|
30
|
+
if (launchPid !== null) {
|
|
31
|
+
try { process.kill(launchPid, 'SIGTERM'); } catch (error) { /* 测试进程可能已结束。 */ }
|
|
32
|
+
}
|
|
33
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
fs.cpSync(sourceSkill, skillRoot, { recursive: true });
|
|
37
|
+
fs.mkdirSync(projectRoot, { recursive: true });
|
|
38
|
+
fs.mkdirSync(fakeBin, { recursive: true });
|
|
39
|
+
|
|
40
|
+
const profileTarget = path.join(dataRoot, 'profile');
|
|
41
|
+
const sessionTarget = path.join(dataRoot, 'sessions', 'old', 'session.jsonl');
|
|
42
|
+
fs.mkdirSync(profileTarget, { recursive: true });
|
|
43
|
+
fs.mkdirSync(path.dirname(sessionTarget), { recursive: true });
|
|
44
|
+
fs.writeFileSync(path.join(profileTarget, 'preserved.txt'), 'profile\n');
|
|
45
|
+
fs.writeFileSync(sessionTarget, '{"kind":"legacy"}\n');
|
|
46
|
+
fs.symlinkSync(profileTarget, path.join(recorderRoot, 'profile'), 'dir');
|
|
47
|
+
fs.symlinkSync(sessionTarget, path.join(recorderRoot, 'session.jsonl'));
|
|
48
|
+
fs.mkdirSync(path.join(recorderRoot, 'node_modules', 'playwright'), { recursive: true });
|
|
49
|
+
fs.writeFileSync(path.join(recorderRoot, 'node_modules', 'playwright', 'package.json'), '{}\n');
|
|
50
|
+
|
|
51
|
+
writeExecutable(path.join(fakeBin, 'npm'), `#!/usr/bin/env bash
|
|
52
|
+
mkdir -p "$PWD/node_modules/playwright"
|
|
53
|
+
printf '{}\\n' > "$PWD/node_modules/playwright/package.json"
|
|
54
|
+
`);
|
|
55
|
+
writeExecutable(path.join(fakeBin, 'node'), `#!/usr/bin/env bash
|
|
56
|
+
printf '%s\\n' "$CRAFT_RPA_SESSION_FILE" > "$CRAFT_RPA_HOME/fake-session-file"
|
|
57
|
+
printf '%s\\n' "$CRAFT_RPA_PROFILE_DIR" > "$CRAFT_RPA_HOME/fake-profile-dir"
|
|
58
|
+
printf '%s\\n' "$CRAFT_RPA_PLAYWRIGHT_MODULE" > "$CRAFT_RPA_HOME/fake-playwright-module"
|
|
59
|
+
while true; do /bin/sleep 1; done
|
|
60
|
+
`);
|
|
61
|
+
|
|
62
|
+
const result = spawnSync('bash', [path.join(skillRoot, 'scripts', 'run.sh'), 'start', 'about:blank'], {
|
|
63
|
+
cwd: projectRoot,
|
|
64
|
+
env: {
|
|
65
|
+
...process.env,
|
|
66
|
+
CRAFT_RPA_HOME: dataRoot,
|
|
67
|
+
PATH: `${fakeBin}${path.delimiter}${process.env.PATH}`,
|
|
68
|
+
},
|
|
69
|
+
encoding: 'utf8',
|
|
70
|
+
timeout: 10000,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
74
|
+
launchPid = Number.parseInt(fs.readFileSync(path.join(dataRoot, '.launch.pid'), 'utf8'), 10);
|
|
75
|
+
assert.equal(Number.isInteger(launchPid), true);
|
|
76
|
+
assert.equal(fs.existsSync(path.join(recorderRoot, 'profile')), false);
|
|
77
|
+
assert.equal(fs.existsSync(path.join(recorderRoot, 'session.jsonl')), false);
|
|
78
|
+
assert.equal(fs.existsSync(path.join(recorderRoot, 'node_modules')), false);
|
|
79
|
+
assert.equal(fs.readFileSync(path.join(profileTarget, 'preserved.txt'), 'utf8'), 'profile\n');
|
|
80
|
+
assert.equal(fs.readFileSync(sessionTarget, 'utf8'), '{"kind":"legacy"}\n');
|
|
81
|
+
|
|
82
|
+
const runtimeRoot = path.join(dataRoot, 'runtime', 'recorder');
|
|
83
|
+
assert.equal(fs.existsSync(path.join(runtimeRoot, 'node_modules', 'playwright', 'package.json')), true);
|
|
84
|
+
assert.equal(
|
|
85
|
+
fs.readFileSync(path.join(dataRoot, 'fake-profile-dir'), 'utf8').trim(),
|
|
86
|
+
profileTarget,
|
|
87
|
+
);
|
|
88
|
+
assert.equal(
|
|
89
|
+
fs.readFileSync(path.join(dataRoot, 'fake-playwright-module'), 'utf8').trim(),
|
|
90
|
+
path.join(runtimeRoot, 'node_modules', 'playwright'),
|
|
91
|
+
);
|
|
92
|
+
const sessionFile = fs.readFileSync(path.join(dataRoot, 'fake-session-file'), 'utf8').trim();
|
|
93
|
+
assert.equal(sessionFile.startsWith(path.join(dataRoot, 'sessions')), true);
|
|
94
|
+
assert.equal(sessionFile.endsWith(path.join('', 'session.jsonl')), true);
|
|
95
|
+
assert.equal(fs.existsSync(sessionFile), true);
|
|
96
|
+
});
|
|
@@ -5,13 +5,11 @@
|
|
|
5
5
|
# ${CRAFT_RPA_HOME:-<cwd>/.craft-rpa}/
|
|
6
6
|
# ├── sessions/<YYYY-MM-DD_HH-MM-SS>/session.jsonl 每次 start 新建,不覆盖
|
|
7
7
|
# ├── profile/ Chrome 持久 profile(登录态)
|
|
8
|
+
# ├── runtime/recorder/node_modules/ 可重建运行时依赖
|
|
8
9
|
# ├── .launch.{pid,log} 进程管理 + 日志
|
|
9
10
|
# └── .current-session 最近一次 start 的 ts
|
|
10
11
|
#
|
|
11
|
-
# skill 内
|
|
12
|
-
# ├── session.jsonl → $CRAFT_RPA_HOME/sessions/<ts>/session.jsonl
|
|
13
|
-
# └── profile → $CRAFT_RPA_HOME/profile
|
|
14
|
-
# launch.js 用 __dirname 解析 session.jsonl / profile,通过软链落到项目根 .craft-rpa/
|
|
12
|
+
# skill 内 recorder/ 只保留静态代码资产;运行时路径通过环境变量显式传给 launch.js。
|
|
15
13
|
#
|
|
16
14
|
# 用法:
|
|
17
15
|
# bash run.sh start [URL]
|
|
@@ -34,13 +32,73 @@ PROFILE_DIR="$CRAFT_RPA_HOME/profile"
|
|
|
34
32
|
PID_FILE="$CRAFT_RPA_HOME/.launch.pid"
|
|
35
33
|
LOG_FILE="$CRAFT_RPA_HOME/.launch.log"
|
|
36
34
|
CURRENT_FILE="$CRAFT_RPA_HOME/.current-session"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
RUNTIME_DIR="$CRAFT_RPA_HOME/runtime/recorder"
|
|
36
|
+
RUNTIME_MODULES_DIR="$RUNTIME_DIR/node_modules"
|
|
37
|
+
LEGACY_SESSION_PATH="$RECORDER_DIR/session.jsonl"
|
|
38
|
+
LEGACY_PROFILE_PATH="$RECORDER_DIR/profile"
|
|
39
|
+
LEGACY_MODULES_DIR="$RECORDER_DIR/node_modules"
|
|
41
40
|
|
|
42
41
|
DASHBOARD_URL="http://localhost:7777/dashboard"
|
|
43
42
|
|
|
43
|
+
prepare_runtime_dependencies() {
|
|
44
|
+
mkdir -p "$RUNTIME_DIR"
|
|
45
|
+
RUNTIME_CHANGED=false
|
|
46
|
+
for MANIFEST in package.json package-lock.json; do
|
|
47
|
+
if [ ! -f "$RECORDER_DIR/$MANIFEST" ]; then
|
|
48
|
+
continue
|
|
49
|
+
fi
|
|
50
|
+
if [ ! -f "$RUNTIME_DIR/$MANIFEST" ] || ! cmp -s "$RECORDER_DIR/$MANIFEST" "$RUNTIME_DIR/$MANIFEST"; then
|
|
51
|
+
cp "$RECORDER_DIR/$MANIFEST" "$RUNTIME_DIR/$MANIFEST"
|
|
52
|
+
RUNTIME_CHANGED=true
|
|
53
|
+
fi
|
|
54
|
+
done
|
|
55
|
+
if [ "$RUNTIME_CHANGED" = true ] || [ ! -d "$RUNTIME_MODULES_DIR/playwright" ]; then
|
|
56
|
+
echo "[craft-rpa] installing deps under $RUNTIME_DIR (one-time, ~30-60s) ..."
|
|
57
|
+
if [ -f "$RUNTIME_DIR/package-lock.json" ]; then
|
|
58
|
+
(cd "$RUNTIME_DIR" && PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm ci --no-audit --no-fund)
|
|
59
|
+
else
|
|
60
|
+
(cd "$RUNTIME_DIR" && PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm install --no-audit --no-fund)
|
|
61
|
+
fi
|
|
62
|
+
fi
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
cleanup_legacy_recorder_runtime() {
|
|
66
|
+
if [ -L "$LEGACY_SESSION_PATH" ]; then
|
|
67
|
+
# 只删除旧软链本身,真实 session 目标继续留在数据根。
|
|
68
|
+
rm -f "$LEGACY_SESSION_PATH"
|
|
69
|
+
echo "[craft-rpa] removed legacy recorder/session.jsonl symlink"
|
|
70
|
+
elif [ -e "$LEGACY_SESSION_PATH" ]; then
|
|
71
|
+
if [ -s "$LEGACY_SESSION_PATH" ]; then
|
|
72
|
+
LEGACY_TS=$(date +"%Y-%m-%d_%H-%M-%S")
|
|
73
|
+
LEGACY_DIR="$SESSIONS_DIR/legacy-$LEGACY_TS"
|
|
74
|
+
mkdir -p "$LEGACY_DIR"
|
|
75
|
+
mv "$LEGACY_SESSION_PATH" "$LEGACY_DIR/session.jsonl"
|
|
76
|
+
echo "[craft-rpa] migrated legacy recorder/session.jsonl → sessions/legacy-$LEGACY_TS/"
|
|
77
|
+
else
|
|
78
|
+
rm -f "$LEGACY_SESSION_PATH"
|
|
79
|
+
fi
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
if [ -L "$LEGACY_PROFILE_PATH" ]; then
|
|
83
|
+
# 只删除旧软链本身,真实 profile 目标继续留在数据根。
|
|
84
|
+
rm -f "$LEGACY_PROFILE_PATH"
|
|
85
|
+
echo "[craft-rpa] removed legacy recorder/profile symlink"
|
|
86
|
+
elif [ -e "$LEGACY_PROFILE_PATH" ]; then
|
|
87
|
+
LEGACY_TS=$(date +"%Y-%m-%d_%H-%M-%S")
|
|
88
|
+
mv "$LEGACY_PROFILE_PATH" "$CRAFT_RPA_HOME/profile-legacy-$LEGACY_TS"
|
|
89
|
+
echo "[craft-rpa] migrated legacy recorder/profile → .craft-rpa/profile-legacy-$LEGACY_TS"
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
if [ -L "$LEGACY_MODULES_DIR" ]; then
|
|
93
|
+
rm -f "$LEGACY_MODULES_DIR"
|
|
94
|
+
echo "[craft-rpa] removed legacy recorder/node_modules symlink"
|
|
95
|
+
elif [ -e "$LEGACY_MODULES_DIR" ]; then
|
|
96
|
+
# node_modules 只是可重建缓存,精确删除受管目录中的旧副本,避免 Plugin 重放再次扫描它。
|
|
97
|
+
rm -rf "$LEGACY_MODULES_DIR"
|
|
98
|
+
echo "[craft-rpa] removed legacy recorder/node_modules cache"
|
|
99
|
+
fi
|
|
100
|
+
}
|
|
101
|
+
|
|
44
102
|
cmd="${1:-status}"
|
|
45
103
|
|
|
46
104
|
case "$cmd" in
|
|
@@ -60,44 +118,22 @@ case "$cmd" in
|
|
|
60
118
|
exit 0
|
|
61
119
|
fi
|
|
62
120
|
|
|
63
|
-
if [ ! -d "$RECORDER_DIR/node_modules" ]; then
|
|
64
|
-
echo "[craft-rpa] installing deps (one-time, ~30-60s) ..."
|
|
65
|
-
(cd "$RECORDER_DIR" && PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm install --no-audit --no-fund)
|
|
66
|
-
fi
|
|
67
|
-
|
|
68
121
|
mkdir -p "$CRAFT_RPA_HOME" "$SESSIONS_DIR" "$PROFILE_DIR"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if [ -e "$SESSION_LINK" ] && [ ! -L "$SESSION_LINK" ]; then
|
|
72
|
-
if [ -s "$SESSION_LINK" ]; then
|
|
73
|
-
LEGACY_TS=$(date +"%Y-%m-%d_%H-%M-%S")
|
|
74
|
-
LEGACY_DIR="$SESSIONS_DIR/legacy-$LEGACY_TS"
|
|
75
|
-
mkdir -p "$LEGACY_DIR"
|
|
76
|
-
mv "$SESSION_LINK" "$LEGACY_DIR/session.jsonl"
|
|
77
|
-
echo "[craft-rpa] migrated legacy recorder/session.jsonl → sessions/legacy-$LEGACY_TS/"
|
|
78
|
-
else
|
|
79
|
-
rm -f "$SESSION_LINK"
|
|
80
|
-
fi
|
|
81
|
-
fi
|
|
82
|
-
# 旧 profile 是普通目录(老版本遗留)→ 归档到数据根
|
|
83
|
-
if [ -e "$PROFILE_LINK" ] && [ ! -L "$PROFILE_LINK" ]; then
|
|
84
|
-
LEGACY_TS=$(date +"%Y-%m-%d_%H-%M-%S")
|
|
85
|
-
mv "$PROFILE_LINK" "$CRAFT_RPA_HOME/profile-legacy-$LEGACY_TS"
|
|
86
|
-
echo "[craft-rpa] migrated legacy recorder/profile → .craft-rpa/profile-legacy-$LEGACY_TS"
|
|
87
|
-
fi
|
|
122
|
+
cleanup_legacy_recorder_runtime
|
|
123
|
+
prepare_runtime_dependencies
|
|
88
124
|
|
|
89
125
|
TS=$(date +"%Y-%m-%d_%H-%M-%S")
|
|
90
126
|
NEW_SESSION_DIR="$SESSIONS_DIR/$TS"
|
|
91
127
|
mkdir -p "$NEW_SESSION_DIR"
|
|
92
128
|
: > "$NEW_SESSION_DIR/session.jsonl"
|
|
93
129
|
|
|
94
|
-
# 建两个软链:launch.js / logger.js 通过 __dirname 读到的就是项目根的数据
|
|
95
|
-
ln -sfn "$NEW_SESSION_DIR/session.jsonl" "$SESSION_LINK"
|
|
96
|
-
ln -sfn "$PROFILE_DIR" "$PROFILE_LINK"
|
|
97
130
|
echo "$TS" > "$CURRENT_FILE"
|
|
98
131
|
|
|
99
132
|
cd "$RECORDER_DIR"
|
|
100
|
-
|
|
133
|
+
CRAFT_RPA_SESSION_FILE="$NEW_SESSION_DIR/session.jsonl" \
|
|
134
|
+
CRAFT_RPA_PROFILE_DIR="$PROFILE_DIR" \
|
|
135
|
+
CRAFT_RPA_PLAYWRIGHT_MODULE="$RUNTIME_MODULES_DIR/playwright" \
|
|
136
|
+
nohup node launch.js "$URL" > "$LOG_FILE" 2>&1 &
|
|
101
137
|
PID=$!
|
|
102
138
|
echo "$PID" > "$PID_FILE"
|
|
103
139
|
sleep 1
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flower-trellis",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "一键安装/升级 Trellis 并自动融合 skill-garden 强化包(默认 Claude + agents)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -68,9 +68,9 @@
|
|
|
68
68
|
"commit-and-tag-version": "^12.7.3"
|
|
69
69
|
},
|
|
70
70
|
"flowerReleaseNotes": {
|
|
71
|
-
"version": "0.5.
|
|
71
|
+
"version": "0.5.3",
|
|
72
72
|
"source": "CHANGELOG.md",
|
|
73
|
-
"body": "###
|
|
73
|
+
"body": "### 🐛 修复 Bug Fixes\n\n* **plugin:** 修复 Craft RPA 运行时软链重放 ([3a68641](https://github.com/SilentFlower/flower-trellis/commit/3a686414c0941ca365b9443328064ab6143cd6f8))",
|
|
74
74
|
"truncated": false
|
|
75
75
|
},
|
|
76
76
|
"optionalDependencies": {
|
|
@@ -5,6 +5,11 @@ import { ENHANCEMENT_SKILL_TARGETS } from "../../constants.js";
|
|
|
5
5
|
import { readLegacyManifestStatus } from "../../lib/manifest.js";
|
|
6
6
|
import { shouldInstallName } from "../../lib/skill-filter.js";
|
|
7
7
|
import { listCanonicalTreeFiles } from "../../plugin/integrity/canonical-tree.js";
|
|
8
|
+
import {
|
|
9
|
+
PluginIntegrityError,
|
|
10
|
+
PluginIoError,
|
|
11
|
+
PluginPathError,
|
|
12
|
+
} from "../../plugin/errors.js";
|
|
8
13
|
import { assertSafePosixRelativePath } from "../../plugin/schemas/shared.js";
|
|
9
14
|
import { compareUtf8 } from "../../plugin/stable-order.js";
|
|
10
15
|
import {
|
|
@@ -48,16 +53,97 @@ const SCRIPT_ALIASES = Object.freeze({
|
|
|
48
53
|
],
|
|
49
54
|
});
|
|
50
55
|
const TRELLIS_TEXT_EXTENSIONS = new Set([".json", ".md", ".toml", ".txt", ".yaml", ".yml"]);
|
|
56
|
+
const COMMON_SKILL_RUNTIME_EXCLUDES = Object.freeze({
|
|
57
|
+
"craft-rpa": Object.freeze([
|
|
58
|
+
"recorder/node_modules",
|
|
59
|
+
"recorder/profile",
|
|
60
|
+
"recorder/session.jsonl",
|
|
61
|
+
]),
|
|
62
|
+
});
|
|
51
63
|
|
|
52
64
|
/**
|
|
53
65
|
* 列出目标目录中的普通文件。
|
|
54
66
|
*
|
|
67
|
+
* 只有 builtin common skill 明确登记的旧运行时路径可以跳过;遍历在读取条目
|
|
68
|
+
* 类型前先匹配排除项,确保不会跟随旧软链。其它路径继续使用 canonical 规则。
|
|
69
|
+
*
|
|
55
70
|
* @param {string} directory 目标目录
|
|
56
|
-
* @
|
|
71
|
+
* @param {string[]} [excludedPaths] 允许跳过的精确相对路径及其子树
|
|
72
|
+
* @returns {Array<{path:string,absolutePath:string,size:number}>} 稳定文件列表
|
|
57
73
|
*/
|
|
58
|
-
function listExistingFiles(directory) {
|
|
74
|
+
function listExistingFiles(directory, excludedPaths = []) {
|
|
59
75
|
if (!fs.existsSync(directory)) return [];
|
|
60
|
-
return listCanonicalTreeFiles(directory);
|
|
76
|
+
if (excludedPaths.length === 0) return listCanonicalTreeFiles(directory);
|
|
77
|
+
|
|
78
|
+
const excluded = new Set(excludedPaths);
|
|
79
|
+
const absoluteRoot = path.resolve(directory);
|
|
80
|
+
let rootStat;
|
|
81
|
+
try {
|
|
82
|
+
rootStat = fs.lstatSync(absoluteRoot);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
throw new PluginIoError(`无法读取 Plugin 根目录:${absoluteRoot}`, { path: absoluteRoot, cause: error });
|
|
85
|
+
}
|
|
86
|
+
if (rootStat.isSymbolicLink()) {
|
|
87
|
+
throw new PluginPathError(`Plugin 根目录不能是软链:${absoluteRoot}`, { path: absoluteRoot });
|
|
88
|
+
}
|
|
89
|
+
if (!rootStat.isDirectory()) {
|
|
90
|
+
throw new PluginIntegrityError(`Plugin 根目录必须是目录:${absoluteRoot}`, { path: absoluteRoot });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const files = [];
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 判断路径是否属于已登记运行时边界。
|
|
97
|
+
*
|
|
98
|
+
* @param {string} relativePath POSIX 相对路径
|
|
99
|
+
* @returns {boolean} 是否跳过该路径
|
|
100
|
+
*/
|
|
101
|
+
function isExcluded(relativePath) {
|
|
102
|
+
for (const candidate of excluded) {
|
|
103
|
+
if (relativePath === candidate || relativePath.startsWith(`${candidate}/`)) return true;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 遍历已安装目标树,但不读取已登记运行时条目的类型或目标。
|
|
110
|
+
*
|
|
111
|
+
* @param {string} current 当前绝对目录
|
|
112
|
+
* @param {string} relativeDirectory 当前 POSIX 相对目录
|
|
113
|
+
* @returns {void}
|
|
114
|
+
*/
|
|
115
|
+
function visit(current, relativeDirectory) {
|
|
116
|
+
let entries;
|
|
117
|
+
try {
|
|
118
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
119
|
+
} catch (error) {
|
|
120
|
+
throw new PluginIoError(`无法读取 Plugin 目录:${current}`, { path: current, cause: error });
|
|
121
|
+
}
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
const relativePath = relativeDirectory
|
|
124
|
+
? `${relativeDirectory}/${entry.name}`
|
|
125
|
+
: entry.name;
|
|
126
|
+
assertSafePosixRelativePath(relativePath, "Plugin tree 路径");
|
|
127
|
+
if (isExcluded(relativePath)) continue;
|
|
128
|
+
|
|
129
|
+
const absolutePath = path.join(current, entry.name);
|
|
130
|
+
let stat;
|
|
131
|
+
try {
|
|
132
|
+
stat = fs.lstatSync(absolutePath);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
throw new PluginIoError(`无法读取 Plugin 条目:${relativePath}`, { path: relativePath, cause: error });
|
|
135
|
+
}
|
|
136
|
+
if (stat.isSymbolicLink()) {
|
|
137
|
+
throw new PluginPathError(`Plugin tree 不允许软链:${relativePath}`, { path: relativePath });
|
|
138
|
+
}
|
|
139
|
+
if (stat.isDirectory()) visit(absolutePath, relativePath);
|
|
140
|
+
else if (stat.isFile()) files.push({ path: relativePath, absolutePath, size: stat.size });
|
|
141
|
+
else throw new PluginIntegrityError(`Plugin tree 不允许特殊文件:${relativePath}`, { path: relativePath });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
visit(absoluteRoot, "");
|
|
146
|
+
return files.sort((left, right) => compareUtf8(left.path, right.path));
|
|
61
147
|
}
|
|
62
148
|
|
|
63
149
|
/**
|
|
@@ -123,6 +209,7 @@ export function projectSkillGardenContent(options) {
|
|
|
123
209
|
* @param {string} sourceLabel 来源标签
|
|
124
210
|
* @param {"exclusive"|"shared"} ownership 所有权
|
|
125
211
|
* @param {boolean} materializePython 是否按目标项目命令物化明确文本载荷
|
|
212
|
+
* @param {string[]} runtimeExcludes 已登记的目标运行时路径
|
|
126
213
|
*/
|
|
127
214
|
function addTree(
|
|
128
215
|
sourceRoot,
|
|
@@ -130,6 +217,7 @@ export function projectSkillGardenContent(options) {
|
|
|
130
217
|
sourceLabel,
|
|
131
218
|
ownership = "exclusive",
|
|
132
219
|
materializePython = false,
|
|
220
|
+
runtimeExcludes = [],
|
|
133
221
|
) {
|
|
134
222
|
const desired = new Set();
|
|
135
223
|
for (const file of listCanonicalTreeFiles(sourceRoot)) {
|
|
@@ -143,7 +231,10 @@ export function projectSkillGardenContent(options) {
|
|
|
143
231
|
materializePython,
|
|
144
232
|
);
|
|
145
233
|
}
|
|
146
|
-
for (const file of listExistingFiles(
|
|
234
|
+
for (const file of listExistingFiles(
|
|
235
|
+
path.join(projectRoot, ...targetRoot.split("/")),
|
|
236
|
+
runtimeExcludes,
|
|
237
|
+
)) {
|
|
147
238
|
if (desired.has(file.path)) continue;
|
|
148
239
|
const target = `${targetRoot}/${file.path}`;
|
|
149
240
|
mutations.push({
|
|
@@ -266,6 +357,8 @@ export function projectSkillGardenContent(options) {
|
|
|
266
357
|
common.target,
|
|
267
358
|
`skill-garden:common:${common.name}`,
|
|
268
359
|
"shared",
|
|
360
|
+
false,
|
|
361
|
+
COMMON_SKILL_RUNTIME_EXCLUDES[common.name] || [],
|
|
269
362
|
);
|
|
270
363
|
}
|
|
271
364
|
for (const removedTarget of commonSync.removedTargets) {
|