flower-trellis 0.5.2 → 0.5.4

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.
Files changed (50) hide show
  1. package/enhancements/0.6/.agents/skills/trellis-release/SKILL.md +1 -1
  2. package/enhancements/0.6/.claude/skills/trellis-release/SKILL.md +1 -1
  3. package/enhancements/MANIFEST.json +2 -2
  4. package/enhancements/common/.common/.claude/skills/craft-rpa/SKILL.md +82 -32
  5. package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/browser-controller.js +772 -0
  6. package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/config.js +54 -0
  7. package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/dashboard.html +16 -13
  8. package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/inject.js +84 -78
  9. package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/launch.js +103 -212
  10. package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/logger.js +256 -179
  11. package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/network-capture.js +339 -0
  12. package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/package.json +2 -1
  13. package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/test/browser-controller.test.js +90 -0
  14. package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/test/browser-smoke.test.js +121 -0
  15. package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/test/control-http.test.js +100 -0
  16. package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/test/logger.test.js +14 -0
  17. package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/test/network-capture.test.js +199 -0
  18. package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/test/runtime-layout.test.js +96 -0
  19. package/enhancements/common/.common/.claude/skills/craft-rpa/recorder/test/trace.test.js +36 -0
  20. package/enhancements/common/.common/.claude/skills/craft-rpa/scripts/control-client.js +95 -0
  21. package/enhancements/common/.common/.claude/skills/craft-rpa/scripts/jsonl-to-trace.js +122 -5
  22. package/enhancements/common/.common/.claude/skills/craft-rpa/scripts/package.json +3 -0
  23. package/enhancements/common/.common/.claude/skills/craft-rpa/scripts/run.sh +107 -40
  24. package/enhancements/common/.common/.codex/skills/craft-rpa/SKILL.md +82 -32
  25. package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/browser-controller.js +772 -0
  26. package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/config.js +54 -0
  27. package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/dashboard.html +16 -13
  28. package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/inject.js +84 -78
  29. package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/launch.js +103 -212
  30. package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/logger.js +256 -179
  31. package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/network-capture.js +339 -0
  32. package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/package.json +2 -1
  33. package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/test/browser-controller.test.js +90 -0
  34. package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/test/browser-smoke.test.js +121 -0
  35. package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/test/control-http.test.js +100 -0
  36. package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/test/logger.test.js +14 -0
  37. package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/test/network-capture.test.js +199 -0
  38. package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/test/runtime-layout.test.js +96 -0
  39. package/enhancements/common/.common/.codex/skills/craft-rpa/recorder/test/trace.test.js +36 -0
  40. package/enhancements/common/.common/.codex/skills/craft-rpa/scripts/control-client.js +95 -0
  41. package/enhancements/common/.common/.codex/skills/craft-rpa/scripts/jsonl-to-trace.js +122 -5
  42. package/enhancements/common/.common/.codex/skills/craft-rpa/scripts/package.json +3 -0
  43. package/enhancements/common/.common/.codex/skills/craft-rpa/scripts/run.sh +107 -40
  44. package/package.json +3 -3
  45. package/src/builtin-plugins/skill-garden/content-adapter.js +97 -4
  46. package/src/commands/init.js +13 -2
  47. package/src/commands/plugin.js +8 -4
  48. package/src/commands/update.js +7 -1
  49. package/src/lib/banner.js +10 -1
  50. package/src/lib/command-completion.js +43 -0
@@ -1,255 +1,146 @@
1
1
  /**
2
- * 一键启动器:日志服务 + 持久化 profile 的 Chrome + 自动注入监听脚本
3
- *
4
- * 内部直接 require 了 logger.js 并启动它,所以一条命令就能起完整套环境,
5
- * 不再需要"另开终端跑 logger.js"。
6
- *
7
- * 用 Playwright 的 launchPersistentContext 启动一个带独立用户数据目录的浏览器实例,
8
- * 通过 addInitScript 让 inject.js 在每个页面加载前自动执行——
9
- * 跳转、新标签页、iframe 全部自动覆盖,无需任何手动重注入。
10
- *
11
- * 使用方式:
12
- * node launch.js # 打开空白页,自己输 URL
13
- * node launch.js https://target.com # 直接打开指定起始网址
14
- *
15
- * 前置条件(一次性):
16
- * 1. 安装依赖:npm install
17
- * 2. 浏览器:默认用系统已装 Chrome(USE_SYSTEM_CHROME=true)
18
- * 没有系统 Chrome 时改成 false,并执行 npx playwright install chromium
19
- *
20
- * 数据存放:
21
- * - profile/ : 用户数据目录(cookie / localStorage / 登录态等),下次启动自动复用
22
- * - session.jsonl : 事件日志(由内置 logger 写入)
2
+ * 一键启动日志服务、持久化浏览器、网络采集和页面控制。
23
3
  */
24
- const { chromium } = require('playwright');
25
- const path = require('path');
26
4
  const fs = require('fs');
5
+ const path = require('path');
6
+ const PLAYWRIGHT_MODULE = process.env.CRAFT_RPA_PLAYWRIGHT_MODULE || 'playwright';
7
+ const { chromium } = require(PLAYWRIGHT_MODULE);
8
+ const { createBrowserController } = require('./browser-controller');
9
+ const {
10
+ MAX_FALLBACK_PAYLOAD_BYTES,
11
+ MAX_NETWORK_BODY_BYTES,
12
+ getLoggerOrigin,
13
+ resolvePort,
14
+ } = require('./config');
27
15
  const { startLogger } = require('./logger');
16
+ const { installNetworkCapture } = require('./network-capture');
28
17
 
29
- // 启动参数:第一个 CLI 参数作为起始 URL,没传就开空白页
30
18
  const START_URL = process.argv[2] || 'about:blank';
31
-
32
- // profile 目录放在脚本同级 ./profile/,与日常 Chrome 完全隔离
33
- const PROFILE_DIR = path.join(__dirname, 'profile');
34
-
35
- // 注入脚本路径
19
+ const PROFILE_DIR = path.resolve(process.env.CRAFT_RPA_PROFILE_DIR || path.join(__dirname, 'profile'));
20
+ const SESSION_LOG = path.resolve(process.env.CRAFT_RPA_SESSION_FILE || path.join(__dirname, 'session.jsonl'));
36
21
  const INJECT_SCRIPT = path.join(__dirname, 'inject.js');
22
+ const PORT = resolvePort();
23
+ const USE_SYSTEM_CHROME = process.env.CRAFT_RPA_USE_SYSTEM_CHROME !== 'false';
24
+ const HEADLESS = process.env.CRAFT_RPA_HEADLESS === 'true';
37
25
 
38
- // 是否使用系统 Chrome
39
- // 在 Windows / macOS 上跑:true(用真实系统 Chrome,更真实的 UA、扩展、字体)
40
- // 在 WSL2 Linux / Docker 容器里跑:false(用 Playwright 内置 Chromium,避免找不到 Chrome)
41
- const USE_SYSTEM_CHROME = true;
42
-
43
- // 前置检查:inject.js 必须存在,避免起完浏览器才发现没注入
44
26
  if (!fs.existsSync(INJECT_SCRIPT)) {
45
27
  console.error(`[launch] inject.js not found: ${INJECT_SCRIPT}`);
46
28
  process.exit(1);
47
29
  }
48
30
 
49
- (async () => {
50
- // BrowserContext 引用,先声明,后续 launchPersistentContext 完成后赋值
51
- // 通过闭包供 browserController 使用,从而让 Dashboard 能控制浏览器
52
- let ctx = null;
53
-
54
- /**
55
- * 浏览器控制器:暴露给 Dashboard 调用的一组操作
56
- *
57
- * 所有方法都包装成 async,错误会被 logger 的 handleControl 捕获并返回 500。
58
- * 方法签名约定见 logger.js handleControl 函数。
59
- */
60
- const browserController = {
61
- /**
62
- * 打开 URL(默认在当前最后一个 tab,可选新开 tab)
63
- * @param {string} url
64
- * @param {{newTab?: boolean}} opts
65
- */
66
- async open(url, opts = {}) {
67
- if (!ctx) throw new Error('browser not ready yet');
68
- const pages = ctx.pages();
69
- const page = opts.newTab || pages.length === 0 ? await ctx.newPage() : pages[pages.length - 1];
70
- if (url && url !== 'about:blank') {
71
- await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
72
- }
73
- await page.bringToFront().catch(() => {});
74
- return { ok: true, url: page.url(), index: ctx.pages().indexOf(page) };
75
- },
76
-
77
- /**
78
- * 列出所有打开的 tab
79
- * @return {Array<{index:number, url:string, title:string}>}
80
- */
81
- async listPages() {
82
- if (!ctx) return [];
83
- const pages = ctx.pages();
84
- return Promise.all(pages.map(async (p, i) => ({
85
- index: i,
86
- url: p.url(),
87
- title: await p.title().catch(() => ''),
88
- })));
89
- },
90
-
91
- /**
92
- * 关闭指定 tab
93
- * @param {number} index tab 序号
94
- */
95
- async close(index) {
96
- if (!ctx) throw new Error('browser not ready');
97
- const pages = ctx.pages();
98
- if (!pages[index]) throw new Error('no such page: ' + index);
99
- await pages[index].close();
100
- return { ok: true };
101
- },
102
-
103
- /**
104
- * 把指定 tab 提到最前
105
- * @param {number} index tab 序号
106
- */
107
- async focus(index) {
108
- if (!ctx) throw new Error('browser not ready');
109
- const pages = ctx.pages();
110
- if (!pages[index]) throw new Error('no such page: ' + index);
111
- await pages[index].bringToFront();
112
- return { ok: true };
113
- },
114
-
115
- /**
116
- * 刷新指定 tab(不传则刷新第一个)
117
- * @param {number} [index]
118
- */
119
- async reload(index = 0) {
120
- if (!ctx) throw new Error('browser not ready');
121
- const p = ctx.pages()[index];
122
- if (!p) throw new Error('no such page: ' + index);
123
- await p.reload({ waitUntil: 'domcontentloaded' });
124
- return { ok: true, url: p.url() };
125
- },
126
-
127
- /**
128
- * 浏览器后退
129
- * @param {number} [index]
130
- */
131
- async back(index = 0) {
132
- if (!ctx) throw new Error('browser not ready');
133
- const p = ctx.pages()[index];
134
- if (!p) throw new Error('no such page: ' + index);
135
- await p.goBack({ waitUntil: 'domcontentloaded' }).catch(() => {});
136
- return { ok: true, url: p.url() };
137
- },
138
-
139
- /**
140
- * 浏览器前进
141
- * @param {number} [index]
142
- */
143
- async forward(index = 0) {
144
- if (!ctx) throw new Error('browser not ready');
145
- const p = ctx.pages()[index];
146
- if (!p) throw new Error('no such page: ' + index);
147
- await p.goForward({ waitUntil: 'domcontentloaded' }).catch(() => {});
148
- return { ok: true, url: p.url() };
149
- },
31
+ /**
32
+ * 获取当前会话目录,优先使用 run.sh 传入值。
33
+ *
34
+ * @return {string} 会话目录绝对路径。
35
+ */
36
+ function resolveSessionDir() {
37
+ if (process.env.CRAFT_RPA_SESSION_DIR) return path.resolve(process.env.CRAFT_RPA_SESSION_DIR);
38
+ try {
39
+ return path.dirname(fs.realpathSync(SESSION_LOG));
40
+ } catch (error) {
41
+ return path.dirname(SESSION_LOG);
42
+ }
43
+ }
150
44
 
151
- /**
152
- * 在指定 tab 跳转到新 URL
153
- * @param {number} index
154
- * @param {string} url
155
- */
156
- async navigate(index, url) {
157
- if (!ctx) throw new Error('browser not ready');
158
- const p = ctx.pages()[index];
159
- if (!p) throw new Error('no such page: ' + index);
160
- await p.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
161
- return { ok: true, url: p.url() };
162
- },
163
- };
45
+ /**
46
+ * 给页面挂调试和主 frame 导航监听。
47
+ *
48
+ * @param {import('playwright').Page} page Playwright 页面。
49
+ * @return {void}
50
+ */
51
+ function attachPageListeners(page) {
52
+ page.on('console', message => console.log(`[browser:${message.type()}] ${message.text()}`));
53
+ page.on('pageerror', error => console.log(`[browser:pageerror] ${error.message}`));
54
+ page.on('framenavigated', frame => {
55
+ if (frame === page.mainFrame()) console.log(`[launch] navigated : ${frame.url()}`);
56
+ });
57
+ }
164
58
 
165
- // 先启动日志 + 控制服务,再启动浏览器
166
- // 浏览器一打开页面,注入脚本立刻就能 POST 成功,不会丢首批事件
167
- const loggerServer = startLogger({ browserController });
59
+ (async () => {
60
+ let context = null;
61
+ let shuttingDown = false;
62
+ const sessionDir = resolveSessionDir();
63
+ const controller = createBrowserController({ getContext: () => context, sessionDir });
64
+ const loggerServer = startLogger({ port: PORT, logFile: SESSION_LOG, browserController: controller });
168
65
 
169
66
  console.log(`[launch] profile dir : ${PROFILE_DIR}`);
67
+ console.log(`[launch] session dir : ${sessionDir}`);
170
68
  console.log(`[launch] start url : ${START_URL}`);
171
69
  console.log(`[launch] browser : ${USE_SYSTEM_CHROME ? 'system chrome' : 'bundled chromium'}`);
172
70
 
173
- // 启动浏览器(带持久化 profile)
174
71
  const launchOptions = {
175
- headless: false,
176
- viewport: null, // 用窗口实际大小,不强制 1280x720
177
- args: ['--start-maximized'], // 启动即最大化
178
- ignoreHTTPSErrors: true, // 忽略证书错误,方便内网/自签场景
179
- // 关键:禁用页面 CSP 检查
180
- // 目标站点(如 oracle.com)通常设置了严格的 connect-src,
181
- // 会拦截注入脚本向 http://localhost:7777 的 fetch,导致一条事件都发不出来
182
- bypassCSP: true,
72
+ headless: HEADLESS,
73
+ viewport: HEADLESS ? { width: 1440, height: 1000 } : null,
74
+ args: HEADLESS ? [] : ['--start-maximized'],
75
+ ignoreHTTPSErrors: true,
183
76
  };
184
- if (USE_SYSTEM_CHROME) {
185
- launchOptions.channel = 'chrome';
186
- }
187
- const context = await chromium.launchPersistentContext(PROFILE_DIR, launchOptions);
188
- ctx = context; // 绑定到上面 browserController 引用的闭包变量
189
-
190
- // 核心:每个页面加载前自动注入 inject.js
191
- // 含:首次打开、整页跳转、新标签页、iframe、history.back/forward 后的页面
192
- await context.addInitScript({ path: INJECT_SCRIPT });
77
+ if (USE_SYSTEM_CHROME) launchOptions.channel = 'chrome';
78
+ context = await chromium.launchPersistentContext(PROFILE_DIR, launchOptions);
193
79
 
194
- /**
195
- * 给一个 page 挂上调试/导航监听器
196
- * 抽成函数因为已存在的初始 page 和后续 context.on('page') 创建的新 page 都要挂
197
- * @param {import('playwright').Page} page
198
- */
199
- function attachPageListeners(page) {
200
- page.on('console', msg => {
201
- // 浏览器端 console.* 转发到终端,便于排查 inject.js 是否执行 / 是否报错
202
- console.log(`[browser:${msg.type()}] ${msg.text()}`);
203
- });
204
- page.on('pageerror', err => {
205
- console.log(`[browser:pageerror] ${err.message}`);
206
- });
207
- page.on('framenavigated', frame => {
208
- // 仅记录主框架导航,避免 iframe 噪音
209
- if (frame === page.mainFrame()) {
210
- console.log(`[launch] navigated : ${frame.url()}`);
211
- }
212
- });
213
- }
80
+ await context.exposeBinding('__craftRpaAppendEvents', async (source, payload) => {
81
+ const events = Array.isArray(payload) ? payload : [payload];
82
+ loggerServer.appendEvents(events);
83
+ return { ok: true, count: events.length };
84
+ });
85
+ const injectContent = fs.readFileSync(INJECT_SCRIPT, 'utf8');
86
+ await context.addInitScript({
87
+ content: `globalThis.__CRAFT_RPA_CONFIG__ = ${JSON.stringify({
88
+ loggerUrl: `${getLoggerOrigin({ port: PORT })}/log`,
89
+ maxFallbackPayloadBytes: MAX_FALLBACK_PAYLOAD_BYTES,
90
+ })};\n${injectContent}`,
91
+ });
92
+ const networkCapture = installNetworkCapture(context, {
93
+ appendEvents: events => loggerServer.appendEvents(events),
94
+ port: PORT,
95
+ maxBodyBytes: MAX_NETWORK_BODY_BYTES,
96
+ });
214
97
 
215
- // 新创建的 page(target=_blank / window.open)自动挂监听
216
98
  context.on('page', page => {
217
99
  console.log(`[launch] new page : ${page.url() || '(blank)'}`);
218
100
  attachPageListeners(page);
219
101
  });
220
102
 
221
- // 浏览器窗口被用户关掉时,连同 logger 一起结束
103
+ /**
104
+ * 关闭网络监听、浏览器和 logger,避免 profile 或 JSONL 半写入。
105
+ *
106
+ * @param {string} reason 退出原因。
107
+ * @return {Promise<void>} 关闭完成。
108
+ */
109
+ async function shutdown(reason) {
110
+ if (shuttingDown) return;
111
+ shuttingDown = true;
112
+ console.log(`[launch] ${reason}, shutting down...`);
113
+ networkCapture.dispose();
114
+ try { await context.close(); } catch (error) { /* 浏览器可能已关闭。 */ }
115
+ await new Promise(resolve => loggerServer.close(resolve));
116
+ }
117
+
222
118
  context.on('close', () => {
119
+ if (shuttingDown) return;
120
+ shuttingDown = true;
223
121
  console.log('[launch] browser closed, shutting down logger');
224
- loggerServer.close();
225
- process.exit(0);
122
+ loggerServer.close(() => process.exit(0));
226
123
  });
227
-
228
- // Ctrl+C 优雅退出,确保 profile 写入磁盘 + logger 正常关闭
229
- process.on('SIGINT', async () => {
230
- console.log('\n[launch] SIGINT received, closing browser & logger...');
231
- try {
232
- await context.close();
233
- } catch (e) {
234
- // 浏览器可能已关闭,忽略
235
- }
236
- loggerServer.close();
237
- process.exit(0);
124
+ process.on('SIGINT', () => {
125
+ void shutdown('SIGINT received').then(() => process.exit(0));
126
+ });
127
+ process.on('SIGTERM', () => {
128
+ void shutdown('SIGTERM received').then(() => process.exit(0));
238
129
  });
239
130
 
240
- // 打开起始页:复用已有的初始 about:blank 标签,避免开两个窗口
241
131
  const pages = context.pages();
242
132
  const page = pages.length > 0 ? pages[0] : await context.newPage();
243
- // 给初始 page 也挂上监听(context.on('page') 不会对已存在的 page 触发)
244
133
  attachPageListeners(page);
245
134
  if (START_URL !== 'about:blank') {
246
135
  try {
247
136
  await page.goto(START_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
248
- } catch (err) {
249
- console.warn(`[launch] initial goto failed: ${err.message}(可在浏览器里手动输地址)`);
137
+ } catch (error) {
138
+ console.warn(`[launch] initial goto failed: ${error.message}(可通过 control 或浏览器地址栏继续)`);
250
139
  }
251
140
  }
252
141
 
253
- console.log('[launch] ready, operate in the opened browser window');
254
- console.log('[launch] Ctrl+C in this terminal to stop, or just close the browser window');
255
- })();
142
+ console.log('[launch] ready, browser can be operated by user, Dashboard, or run.sh control');
143
+ })().catch(error => {
144
+ console.error('[launch] fatal:', error.stack || error.message);
145
+ process.exit(1);
146
+ });