@jackwener/opencli 1.7.14 → 1.7.15

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 (94) hide show
  1. package/cli-manifest.json +215 -45
  2. package/clis/bilibili/subtitle.js +1 -1
  3. package/clis/dianping/cityResolver.js +185 -0
  4. package/clis/dianping/dianping.test.js +154 -0
  5. package/clis/dianping/search.js +6 -3
  6. package/clis/douyin/_shared/browser-fetch.js +14 -2
  7. package/clis/douyin/_shared/browser-fetch.test.js +13 -0
  8. package/clis/douyin/stats.js +1 -1
  9. package/clis/douyin/update.js +1 -1
  10. package/clis/jike/search.js +1 -1
  11. package/clis/reddit/search.js +1 -1
  12. package/clis/reddit/subreddit.js +1 -1
  13. package/clis/reddit/user-comments.js +1 -1
  14. package/clis/reddit/user-posts.js +1 -1
  15. package/clis/reddit/user.js +1 -1
  16. package/clis/twitter/article.js +2 -1
  17. package/clis/twitter/bookmark-folder.js +189 -0
  18. package/clis/twitter/bookmark-folder.test.js +334 -0
  19. package/clis/twitter/bookmark-folders.js +117 -0
  20. package/clis/twitter/bookmark-folders.test.js +150 -0
  21. package/clis/twitter/bookmark.js +15 -6
  22. package/clis/twitter/bookmark.test.js +74 -0
  23. package/clis/twitter/bookmarks.js +7 -5
  24. package/clis/twitter/delete.js +11 -35
  25. package/clis/twitter/delete.test.js +21 -9
  26. package/clis/twitter/download.js +5 -5
  27. package/clis/twitter/followers.js +9 -3
  28. package/clis/twitter/following.js +11 -5
  29. package/clis/twitter/hide-reply.js +24 -5
  30. package/clis/twitter/hide-reply.test.js +76 -0
  31. package/clis/twitter/like.js +21 -11
  32. package/clis/twitter/like.test.js +73 -0
  33. package/clis/twitter/likes.js +8 -6
  34. package/clis/twitter/list-add.js +4 -4
  35. package/clis/twitter/list-remove.js +4 -4
  36. package/clis/twitter/list-tweets.js +6 -4
  37. package/clis/twitter/lists.js +3 -3
  38. package/clis/twitter/notifications.js +2 -2
  39. package/clis/twitter/profile.js +4 -3
  40. package/clis/twitter/quote.js +60 -32
  41. package/clis/twitter/quote.test.js +96 -8
  42. package/clis/twitter/reply.js +24 -178
  43. package/clis/twitter/reply.test.js +29 -11
  44. package/clis/twitter/retweet.js +9 -14
  45. package/clis/twitter/retweet.test.js +5 -1
  46. package/clis/twitter/search.js +175 -23
  47. package/clis/twitter/search.test.js +266 -1
  48. package/clis/twitter/shared.js +43 -0
  49. package/clis/twitter/shared.test.js +107 -1
  50. package/clis/twitter/thread.js +6 -4
  51. package/clis/twitter/timeline.js +8 -6
  52. package/clis/twitter/tweets.js +5 -3
  53. package/clis/twitter/unbookmark.js +13 -6
  54. package/clis/twitter/unbookmark.test.js +73 -0
  55. package/clis/twitter/unlike.js +6 -13
  56. package/clis/twitter/unlike.test.js +5 -2
  57. package/clis/twitter/unretweet.js +9 -14
  58. package/clis/twitter/unretweet.test.js +5 -1
  59. package/clis/twitter/utils.js +286 -0
  60. package/clis/twitter/utils.test.js +169 -0
  61. package/dist/src/browser/ax-snapshot.d.ts +37 -0
  62. package/dist/src/browser/ax-snapshot.js +217 -0
  63. package/dist/src/browser/ax-snapshot.test.d.ts +1 -0
  64. package/dist/src/browser/ax-snapshot.test.js +91 -0
  65. package/dist/src/browser/base-page.d.ts +51 -0
  66. package/dist/src/browser/base-page.js +545 -2
  67. package/dist/src/browser/base-page.test.js +520 -4
  68. package/dist/src/browser/cdp-click-fixture.test.d.ts +1 -0
  69. package/dist/src/browser/cdp-click-fixture.test.js +87 -0
  70. package/dist/src/browser/cdp.js +5 -0
  71. package/dist/src/browser/cdp.test.js +1 -0
  72. package/dist/src/browser/daemon-client.d.ts +3 -1
  73. package/dist/src/browser/find.d.ts +9 -1
  74. package/dist/src/browser/find.js +219 -0
  75. package/dist/src/browser/find.test.js +61 -1
  76. package/dist/src/browser/page.d.ts +2 -1
  77. package/dist/src/browser/page.js +13 -0
  78. package/dist/src/browser/page.test.js +28 -0
  79. package/dist/src/browser/target-errors.d.ts +3 -1
  80. package/dist/src/browser/target-errors.js +2 -0
  81. package/dist/src/browser/target-resolver.d.ts +14 -0
  82. package/dist/src/browser/target-resolver.js +28 -0
  83. package/dist/src/browser/visual-refs.d.ts +11 -0
  84. package/dist/src/browser/visual-refs.js +108 -0
  85. package/dist/src/build-manifest.d.ts +23 -0
  86. package/dist/src/build-manifest.js +34 -0
  87. package/dist/src/build-manifest.test.js +108 -1
  88. package/dist/src/cli.js +560 -58
  89. package/dist/src/cli.test.js +598 -0
  90. package/dist/src/help.d.ts +32 -0
  91. package/dist/src/help.js +145 -0
  92. package/dist/src/types.d.ts +82 -0
  93. package/package.json +1 -1
  94. package/scripts/typed-error-lint-baseline.json +18 -18
@@ -0,0 +1,217 @@
1
+ /**
2
+ * AX-backed browser snapshot prototype.
3
+ *
4
+ * This is intentionally additive to the current DOM snapshot. It learns from
5
+ * agent-browser's accessibility-tree refs without changing default `state`
6
+ * output until the AX path proves itself on fixtures and real SaaS workflows.
7
+ */
8
+ const INTERACTIVE_ROLES = new Set([
9
+ 'button',
10
+ 'link',
11
+ 'textbox',
12
+ 'searchbox',
13
+ 'checkbox',
14
+ 'radio',
15
+ 'combobox',
16
+ 'listbox',
17
+ 'menuitem',
18
+ 'menuitemcheckbox',
19
+ 'menuitemradio',
20
+ 'option',
21
+ 'slider',
22
+ 'spinbutton',
23
+ 'switch',
24
+ 'tab',
25
+ 'treeitem',
26
+ ]);
27
+ const CONTENT_ROLES = new Set([
28
+ 'article',
29
+ 'cell',
30
+ 'columnheader',
31
+ 'gridcell',
32
+ 'heading',
33
+ 'listitem',
34
+ 'main',
35
+ 'navigation',
36
+ 'region',
37
+ 'rowheader',
38
+ ]);
39
+ const STRUCTURAL_ROLES = new Set([
40
+ 'generic',
41
+ 'group',
42
+ 'list',
43
+ 'none',
44
+ 'presentation',
45
+ 'RootWebArea',
46
+ 'WebArea',
47
+ ]);
48
+ export function buildAxSnapshot(axTree, opts = {}) {
49
+ return buildAxSnapshotFromTrees([{ tree: axTree }], opts);
50
+ }
51
+ export function buildAxSnapshotFromTrees(trees, opts = {}) {
52
+ const lines = ['source: ax', '---'];
53
+ const refs = new Map();
54
+ let nextRef = 1;
55
+ for (const [index, entry] of trees.entries()) {
56
+ if (index > 0) {
57
+ const label = entry.frame?.url ? JSON.stringify(entry.frame.url) : JSON.stringify(entry.frame?.frameId ?? `frame:${index}`);
58
+ lines.push(`frame ${label}:`);
59
+ }
60
+ nextRef = renderAxTree(entry.tree, lines, refs, nextRef, {
61
+ ...opts,
62
+ frame: entry.frame,
63
+ baseDepth: index > 0 ? 1 : 0,
64
+ });
65
+ }
66
+ lines.push('---');
67
+ lines.push(`interactive: ${refs.size}`);
68
+ return { text: lines.join('\n'), refs };
69
+ }
70
+ function renderAxTree(axTree, lines, refs, nextRef, opts) {
71
+ const rawNodes = Array.isArray(axTree?.nodes)
72
+ ? axTree.nodes
73
+ : [];
74
+ const nodes = rawNodes.filter((node) => node && !node.ignored);
75
+ const byId = new Map();
76
+ const parentIds = new Set();
77
+ for (const node of nodes) {
78
+ if (typeof node.nodeId === 'string')
79
+ byId.set(node.nodeId, node);
80
+ for (const childId of node.childIds ?? [])
81
+ parentIds.add(childId);
82
+ }
83
+ const roots = nodes.filter((node) => {
84
+ if (!node.nodeId)
85
+ return false;
86
+ const role = axString(node.role);
87
+ return !parentIds.has(node.nodeId) || role === 'RootWebArea' || role === 'WebArea';
88
+ });
89
+ const root = roots[0] ?? nodes[0];
90
+ const maxDepth = Math.max(1, Math.min(Number(opts.maxDepth) || 50, 200));
91
+ const roleNameCounts = countRoleNames(nodes);
92
+ const roleNameSeen = new Map();
93
+ function render(node, depth) {
94
+ if (!node || depth > maxDepth)
95
+ return false;
96
+ const role = axString(node.role) || 'generic';
97
+ const name = cleanText(axString(node.name));
98
+ const value = cleanText(axString(node.value) || propertyValue(node, 'value'));
99
+ const disabled = propertyValue(node, 'disabled');
100
+ const checked = propertyValue(node, 'checked');
101
+ const expanded = propertyValue(node, 'expanded');
102
+ const selected = propertyValue(node, 'selected');
103
+ const refEligible = shouldRef(role, name, node.backendDOMNodeId);
104
+ const shouldShowSelf = refEligible
105
+ || !!name
106
+ || !!value
107
+ || CONTENT_ROLES.has(role)
108
+ || (!opts.interactiveOnly && !STRUCTURAL_ROLES.has(role));
109
+ const childStart = lines.length;
110
+ let hasVisibleChild = false;
111
+ for (const childId of node.childIds ?? []) {
112
+ if (render(byId.get(childId), depth + 1))
113
+ hasVisibleChild = true;
114
+ }
115
+ if (!shouldShowSelf && !hasVisibleChild) {
116
+ lines.length = childStart;
117
+ return false;
118
+ }
119
+ if (shouldShowSelf) {
120
+ const indent = ' '.repeat(depth);
121
+ const parts = [];
122
+ let prefix = '';
123
+ if (refEligible) {
124
+ const ref = String(nextRef++);
125
+ prefix = `[${ref}]`;
126
+ const key = roleNameKey(role, name);
127
+ const seen = roleNameSeen.get(key) ?? 0;
128
+ roleNameSeen.set(key, seen + 1);
129
+ refs.set(ref, {
130
+ ref,
131
+ backendNodeId: node.backendDOMNodeId,
132
+ role,
133
+ name,
134
+ ...(roleNameCounts.get(key) > 1 ? { nth: seen } : {}),
135
+ ...(opts.frame ? { frame: opts.frame } : {}),
136
+ });
137
+ }
138
+ if (name)
139
+ parts.push(JSON.stringify(name));
140
+ if (value && value !== name)
141
+ parts.push(`value=${JSON.stringify(value)}`);
142
+ if (checked)
143
+ parts.push(`checked=${checked}`);
144
+ if (expanded)
145
+ parts.push(`expanded=${expanded}`);
146
+ if (selected)
147
+ parts.push(`selected=${selected}`);
148
+ if (disabled === 'true')
149
+ parts.push('disabled');
150
+ lines.splice(childStart, 0, `${indent}${prefix}${role}${parts.length ? ` ${parts.join(' ')}` : ''}`);
151
+ }
152
+ return true;
153
+ }
154
+ render(root, opts.baseDepth);
155
+ return nextRef;
156
+ }
157
+ export function findAxRefReplacement(axTree, ref) {
158
+ const nodes = Array.isArray(axTree?.nodes)
159
+ ? axTree.nodes
160
+ : [];
161
+ const targetNth = ref.nth ?? 0;
162
+ let seen = 0;
163
+ for (const node of nodes) {
164
+ if (!node || node.ignored)
165
+ continue;
166
+ const role = axString(node.role);
167
+ const name = cleanText(axString(node.name));
168
+ if (role !== ref.role || name !== ref.name)
169
+ continue;
170
+ if (seen === targetNth) {
171
+ if (typeof node.backendDOMNodeId !== 'number')
172
+ return null;
173
+ return { ...ref, backendNodeId: node.backendDOMNodeId };
174
+ }
175
+ seen++;
176
+ }
177
+ return null;
178
+ }
179
+ function shouldRef(role, name, backendNodeId) {
180
+ if (typeof backendNodeId !== 'number')
181
+ return false;
182
+ if (INTERACTIVE_ROLES.has(role))
183
+ return true;
184
+ return CONTENT_ROLES.has(role) && !!name;
185
+ }
186
+ function countRoleNames(nodes) {
187
+ const counts = new Map();
188
+ for (const node of nodes) {
189
+ if (!node || node.ignored)
190
+ continue;
191
+ const role = axString(node.role);
192
+ const name = cleanText(axString(node.name));
193
+ if (!shouldRef(role, name, node.backendDOMNodeId))
194
+ continue;
195
+ const key = roleNameKey(role, name);
196
+ counts.set(key, (counts.get(key) ?? 0) + 1);
197
+ }
198
+ return counts;
199
+ }
200
+ function roleNameKey(role, name) {
201
+ return `${role}\u0000${name}`;
202
+ }
203
+ function axString(value) {
204
+ const raw = value?.value;
205
+ if (typeof raw === 'string')
206
+ return raw;
207
+ if (typeof raw === 'number' || typeof raw === 'boolean')
208
+ return String(raw);
209
+ return '';
210
+ }
211
+ function propertyValue(node, name) {
212
+ const prop = node.properties?.find((candidate) => candidate.name === name);
213
+ return axString(prop?.value);
214
+ }
215
+ function cleanText(value) {
216
+ return value.replace(/\s+/g, ' ').trim().slice(0, 160);
217
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,91 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildAxSnapshot, buildAxSnapshotFromTrees, findAxRefReplacement } from './ax-snapshot.js';
3
+ describe('AX snapshot prototype', () => {
4
+ it('builds compact refs from Accessibility.getFullAXTree output', () => {
5
+ const result = buildAxSnapshot({
6
+ nodes: [
7
+ { nodeId: '1', role: { value: 'RootWebArea' }, name: { value: 'Example' }, childIds: ['2', '3'] },
8
+ { nodeId: '2', role: { value: 'heading' }, name: { value: 'Expenses' }, backendDOMNodeId: 20 },
9
+ {
10
+ nodeId: '3',
11
+ role: { value: 'combobox' },
12
+ name: { value: 'Category' },
13
+ backendDOMNodeId: 30,
14
+ properties: [{ name: 'expanded', value: { value: false } }],
15
+ },
16
+ ],
17
+ });
18
+ expect(result.text).toContain('source: ax');
19
+ expect(result.text).toContain('[1]heading "Expenses"');
20
+ expect(result.text).toContain('[2]combobox "Category" expanded=false');
21
+ expect(result.refs.get('2')).toEqual({
22
+ ref: '2',
23
+ backendNodeId: 30,
24
+ role: 'combobox',
25
+ name: 'Category',
26
+ });
27
+ });
28
+ it('tracks nth only for duplicate role/name pairs', () => {
29
+ const result = buildAxSnapshot({
30
+ nodes: [
31
+ { nodeId: '1', role: { value: 'RootWebArea' }, childIds: ['2', '3'] },
32
+ { nodeId: '2', role: { value: 'button' }, name: { value: 'Save' }, backendDOMNodeId: 10 },
33
+ { nodeId: '3', role: { value: 'button' }, name: { value: 'Save' }, backendDOMNodeId: 11 },
34
+ ],
35
+ });
36
+ expect(result.refs.get('1')).toMatchObject({ role: 'button', name: 'Save', nth: 0 });
37
+ expect(result.refs.get('2')).toMatchObject({ role: 'button', name: 'Save', nth: 1 });
38
+ });
39
+ it('finds stale ref replacements by role/name/nth', () => {
40
+ const replacement = findAxRefReplacement({
41
+ nodes: [
42
+ { nodeId: '1', role: { value: 'button' }, name: { value: 'Save' }, backendDOMNodeId: 10 },
43
+ { nodeId: '2', role: { value: 'button' }, name: { value: 'Save' }, backendDOMNodeId: 42 },
44
+ ],
45
+ }, {
46
+ ref: '2',
47
+ role: 'button',
48
+ name: 'Save',
49
+ backendNodeId: 11,
50
+ nth: 1,
51
+ });
52
+ expect(replacement).toMatchObject({ ref: '2', backendNodeId: 42, role: 'button', name: 'Save', nth: 1 });
53
+ });
54
+ it('combines frame AX trees while keeping ref metadata frame-scoped', () => {
55
+ const result = buildAxSnapshotFromTrees([
56
+ {
57
+ tree: {
58
+ nodes: [
59
+ { nodeId: '1', role: { value: 'RootWebArea' }, childIds: ['2'] },
60
+ { nodeId: '2', role: { value: 'button' }, name: { value: 'Save' }, backendDOMNodeId: 10 },
61
+ ],
62
+ },
63
+ },
64
+ {
65
+ frame: { frameId: 'frame-1', url: 'https://app.example/embed' },
66
+ tree: {
67
+ nodes: [
68
+ { nodeId: '1', role: { value: 'RootWebArea' }, childIds: ['2'] },
69
+ { nodeId: '2', role: { value: 'button' }, name: { value: 'Save' }, backendDOMNodeId: 20 },
70
+ ],
71
+ },
72
+ },
73
+ ]);
74
+ expect(result.text).toContain('[1]button "Save"');
75
+ expect(result.text).toContain('frame "https://app.example/embed":');
76
+ expect(result.text).toContain(' [2]button "Save"');
77
+ expect(result.refs.get('1')).toEqual({
78
+ ref: '1',
79
+ backendNodeId: 10,
80
+ role: 'button',
81
+ name: 'Save',
82
+ });
83
+ expect(result.refs.get('2')).toEqual({
84
+ ref: '2',
85
+ backendNodeId: 20,
86
+ role: 'button',
87
+ name: 'Save',
88
+ frame: { frameId: 'frame-1', url: 'https://app.example/embed' },
89
+ });
90
+ });
91
+ });
@@ -27,11 +27,34 @@ export interface FillTextResult extends ResolveSuccess {
27
27
  length: number;
28
28
  mode?: 'input' | 'textarea' | 'contenteditable';
29
29
  }
30
+ export interface SetCheckedResult extends ResolveSuccess {
31
+ checked: boolean;
32
+ changed: boolean;
33
+ kind?: string;
34
+ }
35
+ export interface UploadFilesResult extends ResolveSuccess {
36
+ uploaded: boolean;
37
+ files: number;
38
+ file_names: string[];
39
+ target: string;
40
+ multiple?: boolean;
41
+ accept?: string;
42
+ }
43
+ export interface DragResult {
44
+ dragged: boolean;
45
+ source: string;
46
+ target: string;
47
+ source_matches_n: number;
48
+ target_matches_n: number;
49
+ source_match_level: TargetMatchLevel;
50
+ target_match_level: TargetMatchLevel;
51
+ }
30
52
  export declare abstract class BasePage implements IPage {
31
53
  protected _lastUrl: string | null;
32
54
  /** Cached previous snapshot hashes for incremental diff marking */
33
55
  private _prevSnapshotHashes;
34
56
  private _cdpTargetMarkerSeq;
57
+ private _axRefs;
35
58
  abstract goto(url: string, options?: {
36
59
  waitUntil?: 'load' | 'none';
37
60
  settleMs?: number;
@@ -53,15 +76,29 @@ export declare abstract class BasePage implements IPage {
53
76
  url?: string;
54
77
  }): Promise<BrowserCookie[]>;
55
78
  abstract screenshot(options?: ScreenshotOptions): Promise<string>;
79
+ annotatedScreenshot(options?: ScreenshotOptions): Promise<string>;
56
80
  abstract tabs(): Promise<unknown[]>;
57
81
  abstract selectTab(target: number | string): Promise<void>;
58
82
  click(ref: string, opts?: ResolveOptions): Promise<ResolveSuccess>;
59
83
  /** Uses native CDP click support when the concrete page exposes it. */
60
84
  protected tryNativeClick(x: number, y: number): Promise<boolean>;
85
+ protected tryNativeMouseMove(x: number, y: number): Promise<boolean>;
86
+ protected tryNativeDoubleClick(x: number, y: number): Promise<boolean>;
87
+ protected tryNativeDrag(from: {
88
+ x: number;
89
+ y: number;
90
+ }, to: {
91
+ x: number;
92
+ y: number;
93
+ }): Promise<boolean>;
94
+ protected tryClickAxRef(ref: string): Promise<ResolveSuccess | null>;
95
+ private resolveAxRefPoint;
96
+ private axBoxCenter;
61
97
  /** Uses native CDP text insertion when the concrete page exposes it. */
62
98
  protected tryNativeType(text: string): Promise<boolean>;
63
99
  /** Uses native CDP key events when the concrete page exposes them. */
64
100
  protected tryNativeKeyPress(key: string, modifiers: string[]): Promise<boolean>;
101
+ protected isResolvedFocused(): Promise<boolean>;
65
102
  /**
66
103
  * Run a DOM-domain CDP command against `window.__resolved`.
67
104
  *
@@ -71,6 +108,19 @@ export declare abstract class BasePage implements IPage {
71
108
  */
72
109
  protected tryCdpOnResolvedElement(method: 'DOM.focus' | 'DOM.scrollIntoViewIfNeeded'): Promise<boolean>;
73
110
  typeText(ref: string, text: string, opts?: ResolveOptions): Promise<ResolveSuccess>;
111
+ hover(ref: string, opts?: ResolveOptions): Promise<ResolveSuccess>;
112
+ focus(ref: string, opts?: ResolveOptions): Promise<ResolveSuccess & {
113
+ focused: boolean;
114
+ }>;
115
+ dblClick(ref: string, opts?: ResolveOptions): Promise<ResolveSuccess>;
116
+ private readCheckableState;
117
+ setChecked(ref: string, checked: boolean, opts?: ResolveOptions): Promise<SetCheckedResult>;
118
+ private setFileInputBySelector;
119
+ uploadFiles(ref: string, files: string[], opts?: ResolveOptions): Promise<UploadFilesResult>;
120
+ drag(source: string, target: string, opts?: {
121
+ from?: ResolveOptions;
122
+ to?: ResolveOptions;
123
+ }): Promise<DragResult>;
74
124
  fillText(ref: string, text: string, opts?: ResolveOptions): Promise<FillTextResult>;
75
125
  pressKey(key: string): Promise<void>;
76
126
  scrollTo(ref: string, opts?: ResolveOptions): Promise<unknown>;
@@ -84,6 +134,7 @@ export declare abstract class BasePage implements IPage {
84
134
  consoleMessages(_level?: string): Promise<unknown[]>;
85
135
  wait(options: number | WaitOptions): Promise<void>;
86
136
  snapshot(opts?: SnapshotOptions): Promise<unknown>;
137
+ private collectAxSnapshotTrees;
87
138
  getCurrentUrl(): Promise<string | null>;
88
139
  installInterceptor(pattern: string): Promise<void>;
89
140
  getInterceptedRequests(): Promise<unknown[]>;