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
@@ -0,0 +1,54 @@
1
+ const DEFAULT_PORT = 7777;
2
+ const DEFAULT_HOST = '0.0.0.0';
3
+ const DEFAULT_CLIENT_HOST = 'localhost';
4
+ const MAX_NETWORK_BODY_BYTES = 20 * 1024 * 1024;
5
+ const MAX_FALLBACK_PAYLOAD_BYTES = 60 * 1024;
6
+ const MAX_OBSERVE_TEXT_BYTES = 64 * 1024;
7
+ const MAX_OBSERVE_ARIA_BYTES = 64 * 1024;
8
+ const MAX_OBSERVE_METADATA_BYTES = 4 * 1024;
9
+ const MAX_OBSERVE_DOM_BYTES = 5 * 1024 * 1024;
10
+ const MAX_OBSERVE_SCREENSHOT_BYTES = 20 * 1024 * 1024;
11
+ const MAX_OBSERVE_ELEMENTS = 500;
12
+ const MAX_OBSERVE_FRAMES = 100;
13
+
14
+ /**
15
+ * 解析 recorder 端口,非法值回退到默认端口。
16
+ *
17
+ * @param {string|number|undefined} value 候选端口。
18
+ * @return {number} 可用端口。
19
+ */
20
+ function resolvePort(value = process.env.CRAFT_RPA_PORT) {
21
+ const port = Number.parseInt(String(value || ''), 10);
22
+ return Number.isInteger(port) && port > 0 && port <= 65535 ? port : DEFAULT_PORT;
23
+ }
24
+
25
+ /**
26
+ * 返回浏览器或本机客户端访问 logger 的 HTTP origin。
27
+ *
28
+ * @param {Object} [options] 配置项。
29
+ * @param {string} [options.host] 客户端访问主机名。
30
+ * @param {number} [options.port] logger 端口。
31
+ * @return {string} HTTP origin。
32
+ */
33
+ function getLoggerOrigin(options = {}) {
34
+ const host = options.host || process.env.CRAFT_RPA_CLIENT_HOST || DEFAULT_CLIENT_HOST;
35
+ const port = resolvePort(options.port);
36
+ return `http://${host}:${port}`;
37
+ }
38
+
39
+ module.exports = {
40
+ DEFAULT_PORT,
41
+ DEFAULT_HOST,
42
+ DEFAULT_CLIENT_HOST,
43
+ MAX_NETWORK_BODY_BYTES,
44
+ MAX_FALLBACK_PAYLOAD_BYTES,
45
+ MAX_OBSERVE_TEXT_BYTES,
46
+ MAX_OBSERVE_ARIA_BYTES,
47
+ MAX_OBSERVE_METADATA_BYTES,
48
+ MAX_OBSERVE_DOM_BYTES,
49
+ MAX_OBSERVE_SCREENSHOT_BYTES,
50
+ MAX_OBSERVE_ELEMENTS,
51
+ MAX_OBSERVE_FRAMES,
52
+ resolvePort,
53
+ getLoggerOrigin,
54
+ };
@@ -1347,7 +1347,7 @@ body::before {
1347
1347
  <span id="last-update"></span>
1348
1348
  </div>
1349
1349
  <div class="center" id="filter-info">显示 0 / 0</div>
1350
- <div class="right" id="endpoint-info">localhost:7777 · session.jsonl</div>
1350
+ <div class="right" id="endpoint-info">本机 recorder · session.jsonl</div>
1351
1351
  </footer>
1352
1352
 
1353
1353
  <div class="toast" id="toast"></div>
@@ -1358,6 +1358,7 @@ body::before {
1358
1358
  初次进入:优先读取 localStorage;没有则跟随系统 prefers-color-scheme
1359
1359
  ============================================================ */
1360
1360
  const THEME_KEY = 'recorder-theme';
1361
+ document.getElementById('endpoint-info').textContent = `${location.host} · session.jsonl`;
1361
1362
 
1362
1363
  /**
1363
1364
  * 应用主题到 <html> 上
@@ -1394,7 +1395,7 @@ const state = {
1394
1395
  events: [], // 全部已收事件,按 serverTime 顺序
1395
1396
  pages: [], // 浏览器当前打开的 tab 列表
1396
1397
  activeTabIdx: 0, // 当前活跃 tab(focus 后更新)
1397
- lastSeen: '', // 增量拉取游标(最后一条 serverTime)
1398
+ lastCursor: 0, // 增量拉取游标(logger 生成的单调序号)
1398
1399
  filter: 'all', // kind 过滤
1399
1400
  search: '', // 关键词搜索
1400
1401
  paused: false, // 是否暂停轮询
@@ -1506,7 +1507,7 @@ async function pollOnce() {
1506
1507
  if (state.paused) return;
1507
1508
  try {
1508
1509
  const [eventsResp, pagesResp] = await Promise.all([
1509
- fetch(state.lastSeen ? `/events?since=${encodeURIComponent(state.lastSeen)}` : '/events'),
1510
+ fetch(state.lastCursor ? `/events?cursor=${encodeURIComponent(state.lastCursor)}` : '/events'),
1510
1511
  fetch('/control/pages').catch(() => null),
1511
1512
  ]);
1512
1513
 
@@ -1516,7 +1517,7 @@ async function pollOnce() {
1516
1517
  if (events.length > 0) {
1517
1518
  const startIdx = state.events.length;
1518
1519
  state.events.push(...events);
1519
- state.lastSeen = events[events.length - 1].serverTime || state.lastSeen;
1520
+ state.lastCursor = events[events.length - 1]._cursor || state.lastCursor;
1520
1521
  // 标记本批为"新事件"用于入场动画
1521
1522
  for (let i = 0; i < events.length; i++) {
1522
1523
  state.newEventIdxs.add(startIdx + i);
@@ -1846,10 +1847,15 @@ function selectEvent(idx) {
1846
1847
  if (e.kind === 'network') {
1847
1848
  const items = [];
1848
1849
  items.push(['METHOD', e.method || '?']);
1849
- items.push(['STATUS', String(e.status || 'ERR')]);
1850
+ items.push(['STATUS', String(e.status ?? 'ERR')]);
1850
1851
  if (e.requestUrl) items.push(['URL', e.requestUrl]);
1851
1852
  if (e.durationMs) items.push(['DURATION', e.durationMs + ' ms']);
1852
- if (e.error) items.push(['ERROR', e.error]);
1853
+ if (e.source) items.push(['SOURCE', e.source]);
1854
+ if (e.requestContentType) items.push(['REQUEST TYPE', e.requestContentType]);
1855
+ if (e.requestSize !== undefined) items.push(['REQUEST SIZE', e.requestSize + ' bytes']);
1856
+ if (e.responseContentType) items.push(['RESPONSE TYPE', e.responseContentType]);
1857
+ if (e.responseSize !== undefined) items.push(['RESPONSE SIZE', e.responseSize + ' bytes']);
1858
+ if (e.failure?.errorText || e.error) items.push(['ERROR', e.failure?.errorText || e.error]);
1853
1859
  html += `
1854
1860
  <div class="detail-section">
1855
1861
  <div class="section-label">请求信息</div>
@@ -1912,7 +1918,7 @@ async function doControl(path, body) {
1912
1918
  const resp = await fetch('/control/' + path, opts);
1913
1919
  if (!resp.ok) {
1914
1920
  const err = await resp.json().catch(() => ({}));
1915
- throw new Error(err.error || `HTTP ${resp.status}`);
1921
+ throw new Error(err.error?.message || err.error || `HTTP ${resp.status}`);
1916
1922
  }
1917
1923
  return await resp.json();
1918
1924
  } catch (e) {
@@ -2010,17 +2016,14 @@ $('btn-export').onclick = () => {
2010
2016
  toast('暂无事件可导出');
2011
2017
  return;
2012
2018
  }
2013
- const jsonl = state.events.map(e => JSON.stringify(e)).join('\n');
2014
- const blob = new Blob([jsonl], { type: 'application/x-ndjson' });
2015
2019
  const a = document.createElement('a');
2016
- a.href = URL.createObjectURL(blob);
2020
+ a.href = '/session';
2017
2021
  a.download = `recorder-${new Date().toISOString().replace(/[:.]/g, '-')}.jsonl`;
2018
2022
  a.click();
2019
- URL.revokeObjectURL(a.href);
2020
- toast(`⬇ 已导出 ${state.events.length} 条事件`, 'success');
2023
+ toast(`已请求导出完整 JSONL(${state.events.length} 条摘要)`, 'success');
2021
2024
  };
2022
2025
 
2023
- // 清空显示(仅前端,磁盘文件不变;保留 lastSeen 以免刷出旧事件)
2026
+ // 清空显示(仅前端,磁盘文件不变;保留 lastCursor 以免刷出旧事件)
2024
2027
  $('btn-clear').onclick = () => {
2025
2028
  if (!confirm('清空当前显示?\n\n磁盘文件不受影响,只有新事件会出现。')) return;
2026
2029
  state.events = [];
@@ -2,7 +2,7 @@
2
2
  * 浏览器端事件监听注入脚本 —— RPA 增强版
3
3
  *
4
4
  * 通过 Playwright 的 addInitScript 在每个页面加载前注入。
5
- * 监听用户的所有交互、网络请求、路由变化和异常,实时 POST 给本地 logger。
5
+ * 监听用户交互、路由变化和异常,优先通过 Playwright binding 写入本地 logger。
6
6
  *
7
7
  * RPA 友好的元素详情包括:
8
8
  * - 多种选择器策略(id / data-testid / aria / role+name / text / css / xpath)
@@ -18,7 +18,7 @@
18
18
  * 1. 幂等:window.__loggerInjected 防重复绑定。
19
19
  * 2. 跳过 about:blank:浏览器内嵌的空白 iframe 没有有用事件,避免噪音。
20
20
  * 3. 批量+延迟发送:500ms 或攒满 10 条触发一次 flush。
21
- * 4. 页面卸载兜底:beforeunload / pagehide 时 sendBeacon 强制刷出。
21
+ * 4. 传输主路径:Playwright binding;不可用时才用 sendBeacon / 原生 fetch 回退。
22
22
  * 5. 敏感字段标记:命中 password / 含 password|secret|token|captcha 字段名的元素在 target.sensitive=true;
23
23
  * value 默认透传原值(RPA 流程参考所需);设 REDACT_SENSITIVE=true 才替换成 [REDACTED len=N]。
24
24
  */
@@ -34,7 +34,10 @@
34
34
 
35
35
  // ============ 常量 ============
36
36
 
37
- const LOGGER = 'http://localhost:7777/log';
37
+ const CONFIG = globalThis.__CRAFT_RPA_CONFIG__ || {};
38
+ const LOGGER = CONFIG.loggerUrl || 'http://localhost:7777/log';
39
+ const MAX_FALLBACK_PAYLOAD_BYTES = CONFIG.maxFallbackPayloadBytes || 60 * 1024;
40
+ const NATIVE_FETCH = typeof window.fetch === 'function' ? window.fetch.bind(window) : null;
38
41
 
39
42
  /** 会话 ID:用于把同一个页面 / SPA 流程的事件串起来。整页跳转会产生新的 sessionId(属正常) */
40
43
  const SESSION_ID =
@@ -92,35 +95,98 @@
92
95
  }
93
96
 
94
97
  /**
95
- * 把队列里的事件全部发送
96
- * 优先 sendBeacon(卸载时也能发),失败时退回 fetch keepalive
98
+ * 通过 HTTP 回退发送事件,并按 keepalive/beacon 的实际限制拆批。
99
+ *
100
+ * @param {Object[]} events 待发送事件。
101
+ */
102
+ function sendHttpFallback(events) {
103
+ const batches = [];
104
+ let current = [];
105
+ for (const event of events) {
106
+ const candidate = current.concat(event);
107
+ const candidateBody = JSON.stringify(candidate);
108
+ if (new TextEncoder().encode(candidateBody).length <= MAX_FALLBACK_PAYLOAD_BYTES) {
109
+ current = candidate;
110
+ continue;
111
+ }
112
+ if (current.length > 0) batches.push(current);
113
+ const singleBody = JSON.stringify([event]);
114
+ if (new TextEncoder().encode(singleBody).length <= MAX_FALLBACK_PAYLOAD_BYTES) {
115
+ current = [event];
116
+ } else {
117
+ console.warn('[inject] dropped oversized fallback event:', event.kind, event.type);
118
+ current = [{
119
+ sessionId: SESSION_ID,
120
+ clientTime: new Date().toISOString(),
121
+ url: location.href,
122
+ frame: getFrameContext(),
123
+ kind: 'error',
124
+ type: 'transport.drop',
125
+ message: 'fallback event exceeded payload limit',
126
+ droppedKind: event.kind,
127
+ droppedType: event.type,
128
+ }];
129
+ }
130
+ }
131
+ if (current.length > 0) batches.push(current);
132
+
133
+ for (const batch of batches) {
134
+ const body = JSON.stringify(batch);
135
+ try {
136
+ const sent = navigator.sendBeacon
137
+ ? navigator.sendBeacon(LOGGER, new Blob([body], { type: 'application/json' }))
138
+ : false;
139
+ if (!sent && NATIVE_FETCH) {
140
+ void NATIVE_FETCH(LOGGER, {
141
+ method: 'POST',
142
+ headers: { 'Content-Type': 'application/json' },
143
+ body,
144
+ keepalive: true,
145
+ }).catch(error => {
146
+ console.warn('[inject] fallback fetch failed:', error.message);
147
+ });
148
+ } else if (!sent) {
149
+ console.warn('[inject] fallback transport unavailable:', batch.length, 'events');
150
+ }
151
+ } catch (e) {
152
+ // 回退失败只能写浏览器控制台,不能把异常抛进业务页面。
153
+ console.warn('[inject] fallback transport failed:', e.message);
154
+ }
155
+ }
156
+ }
157
+
158
+ /**
159
+ * 把队列里的事件发送到 Playwright binding,失败或卸载时走 HTTP 回退。
160
+ *
161
+ * @param {{unloading?:boolean}} [options] 卸载阶段必须使用同步发起的回退通道。
97
162
  */
98
- function flush() {
163
+ function flush(options = {}) {
99
164
  if (queue.length === 0) return;
100
165
  const batch = queue.splice(0);
101
166
  if (flushTimer) {
102
167
  clearTimeout(flushTimer);
103
168
  flushTimer = null;
104
169
  }
105
- const body = JSON.stringify(batch);
106
170
  try {
107
- if (navigator.sendBeacon) {
108
- navigator.sendBeacon(LOGGER, new Blob([body], { type: 'application/json' }));
109
- } else {
110
- fetch(LOGGER, {
111
- method: 'POST',
112
- headers: { 'Content-Type': 'application/json' },
113
- body,
114
- keepalive: true,
171
+ if (options.unloading) {
172
+ sendHttpFallback(batch);
173
+ return;
174
+ }
175
+ if (typeof window.__craftRpaAppendEvents === 'function') {
176
+ void Promise.resolve(window.__craftRpaAppendEvents(batch)).catch(error => {
177
+ console.warn('[inject] binding transport failed:', error.message);
178
+ sendHttpFallback(batch);
115
179
  });
180
+ } else {
181
+ sendHttpFallback(batch);
116
182
  }
117
183
  } catch (e) {
118
- // 发送失败不抛出,避免影响业务页面
184
+ sendHttpFallback(batch);
119
185
  }
120
186
  }
121
187
 
122
- window.addEventListener('beforeunload', flush);
123
- window.addEventListener('pagehide', flush);
188
+ window.addEventListener('beforeunload', () => flush({ unloading: true }));
189
+ window.addEventListener('pagehide', () => flush({ unloading: true }));
124
190
 
125
191
  // ============ 上下文:iframe / 视口 ============
126
192
 
@@ -653,66 +719,6 @@
653
719
  });
654
720
  }, true);
655
721
 
656
- // ============ 网络请求拦截 ============
657
-
658
- const origFetch = window.fetch;
659
- window.fetch = async function (...args) {
660
- const [input, init = {}] = args;
661
- const url = typeof input === 'string' ? input : input.url;
662
- const method = init.method || (typeof input !== 'string' && input.method) || 'GET';
663
- const start = performance.now();
664
- try {
665
- const resp = await origFetch.apply(this, args);
666
- send({
667
- kind: 'network',
668
- type: 'fetch',
669
- method,
670
- requestUrl: url,
671
- status: resp.status,
672
- durationMs: Math.round(performance.now() - start),
673
- });
674
- return resp;
675
- } catch (err) {
676
- send({
677
- kind: 'network',
678
- type: 'fetch',
679
- method,
680
- requestUrl: url,
681
- error: err.message,
682
- durationMs: Math.round(performance.now() - start),
683
- });
684
- throw err;
685
- }
686
- };
687
-
688
- const OrigXHR = window.XMLHttpRequest;
689
- window.XMLHttpRequest = function () {
690
- const xhr = new OrigXHR();
691
- let _method, _url, _start;
692
- const origOpen = xhr.open;
693
- xhr.open = function (method, url, ...rest) {
694
- _method = method;
695
- _url = url;
696
- return origOpen.call(this, method, url, ...rest);
697
- };
698
- const origSend = xhr.send;
699
- xhr.send = function (...args) {
700
- _start = performance.now();
701
- xhr.addEventListener('loadend', () => {
702
- send({
703
- kind: 'network',
704
- type: 'xhr',
705
- method: _method,
706
- requestUrl: _url,
707
- status: xhr.status,
708
- durationMs: Math.round(performance.now() - _start),
709
- });
710
- });
711
- return origSend.apply(this, args);
712
- };
713
- return xhr;
714
- };
715
-
716
722
  // ============ SPA 路由 ============
717
723
 
718
724
  ['pushState', 'replaceState'].forEach(type => {