@love-moon/conductor-cli 0.1.0
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/bin/conductor-chrome.js +376 -0
- package/bin/conductor-config.js +82 -0
- package/bin/conductor-daemon.js +67 -0
- package/bin/conductor-fire.js +903 -0
- package/package.json +34 -0
- package/src/daemon.js +376 -0
- package/src/fire/history.js +605 -0
- package/src/pageAutomation.js +131 -0
- package/src/providers/deepseek.js +405 -0
- package/src/providers/generic.js +6 -0
- package/src/providers/qwen.js +203 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import createDeepseekProvider from './providers/deepseek.js';
|
|
2
|
+
import createGenericProvider from './providers/generic.js';
|
|
3
|
+
import createQwenProvider from './providers/qwen.js';
|
|
4
|
+
|
|
5
|
+
const HIGHLIGHT_CLASS = 'conductor-highlight';
|
|
6
|
+
const providerFactories = {
|
|
7
|
+
deepseek: createDeepseekProvider,
|
|
8
|
+
qwen: createQwenProvider,
|
|
9
|
+
generic: createGenericProvider,
|
|
10
|
+
};
|
|
11
|
+
const providerMap = {
|
|
12
|
+
'chat.deepseek.com': 'deepseek',
|
|
13
|
+
'chat.qwen.ai': 'qwen',
|
|
14
|
+
};
|
|
15
|
+
const defaultProvider = 'generic';
|
|
16
|
+
const providerCache = new Map();
|
|
17
|
+
let styleInjected = false;
|
|
18
|
+
|
|
19
|
+
function getDocument() {
|
|
20
|
+
return typeof document !== 'undefined' ? document : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getWindow() {
|
|
24
|
+
return typeof window !== 'undefined' ? window : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function ensureHighlightStyle() {
|
|
28
|
+
const doc = getDocument();
|
|
29
|
+
if (!doc || styleInjected) return;
|
|
30
|
+
const styleId = 'conductor-highlight-style';
|
|
31
|
+
if (doc.getElementById(styleId)) {
|
|
32
|
+
styleInjected = true;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const style = doc.createElement('style');
|
|
36
|
+
style.id = styleId;
|
|
37
|
+
style.textContent = `
|
|
38
|
+
.${HIGHLIGHT_CLASS} {
|
|
39
|
+
outline: 2px solid #ff9800 !important;
|
|
40
|
+
outline-offset: 2px !important;
|
|
41
|
+
border-radius: 4px !important;
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
doc.head.appendChild(style);
|
|
45
|
+
styleInjected = true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function clearHighlights() {
|
|
49
|
+
const doc = getDocument();
|
|
50
|
+
if (!doc) return;
|
|
51
|
+
doc.querySelectorAll(`.${HIGHLIGHT_CLASS}`).forEach(node => node.classList.remove(HIGHLIGHT_CLASS));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function addHighlight(el, scroll = false) {
|
|
55
|
+
if (!el) return;
|
|
56
|
+
const doc = getDocument();
|
|
57
|
+
if (!doc) return;
|
|
58
|
+
ensureHighlightStyle();
|
|
59
|
+
el.classList.add(HIGHLIGHT_CLASS);
|
|
60
|
+
if (scroll) {
|
|
61
|
+
try {
|
|
62
|
+
el.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
63
|
+
} catch {
|
|
64
|
+
// ignore
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function highlight(el, scroll = true) {
|
|
70
|
+
clearHighlights();
|
|
71
|
+
addHighlight(el, scroll);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function detectProvider() {
|
|
75
|
+
const win = getWindow();
|
|
76
|
+
const host = win?.location?.host;
|
|
77
|
+
return (host && providerMap[host]) || defaultProvider;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resolveFactory(name) {
|
|
81
|
+
return providerFactories[name] || providerFactories[defaultProvider];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function loadProvider(name) {
|
|
85
|
+
const providerName = name || detectProvider();
|
|
86
|
+
const cached = providerCache.get(providerName);
|
|
87
|
+
if (cached) {
|
|
88
|
+
return cached;
|
|
89
|
+
}
|
|
90
|
+
const factory = resolveFactory(providerName);
|
|
91
|
+
if (!factory) {
|
|
92
|
+
throw new Error(`Unknown provider: ${providerName}`);
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const provider = factory({ highlight, clearHighlights, addHighlight });
|
|
96
|
+
providerCache.set(providerName, provider);
|
|
97
|
+
return provider;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (providerName !== defaultProvider) {
|
|
100
|
+
return loadProvider(defaultProvider);
|
|
101
|
+
}
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function ensureProvider(name) {
|
|
107
|
+
return loadProvider(name);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function create_task() {
|
|
111
|
+
const provider = ensureProvider();
|
|
112
|
+
return provider.create_task();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function send_message(text) {
|
|
116
|
+
const provider = ensureProvider();
|
|
117
|
+
return provider.send_message(text || '');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function receive_message() {
|
|
121
|
+
const provider = ensureProvider();
|
|
122
|
+
return provider.receive_message();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function highlightDetectedElements() {
|
|
126
|
+
const provider = ensureProvider();
|
|
127
|
+
if (typeof provider.highlightDetectedElements !== 'function') {
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
return provider.highlightDetectedElements();
|
|
131
|
+
}
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
const BUTTON_TEXTS = ['开启新对话', '新建对话', 'new chat', 'new conversation', '开启新對話', '新建對話', 'New Chat'];
|
|
2
|
+
const SEND_KEYWORDS = ['发送', 'send', 'submit', 'enter', 'send message', '发出', 'arrow', '↑', 'paper plane', 'plane'];
|
|
3
|
+
const AI_KEYWORDS = ['assistant', 'ai', 'bot', '系统', '回复'];
|
|
4
|
+
|
|
5
|
+
export default function createDeepseekProvider({ highlight, clearHighlights, addHighlight }) {
|
|
6
|
+
function isVisible(el) {
|
|
7
|
+
if (!el || !(el instanceof HTMLElement)) return false;
|
|
8
|
+
const rect = el.getBoundingClientRect();
|
|
9
|
+
if (rect.width === 0 || rect.height === 0) return false;
|
|
10
|
+
const style = window.getComputedStyle(el);
|
|
11
|
+
if (style.visibility === 'hidden' || style.display === 'none' || style.opacity === '0') return false;
|
|
12
|
+
if (rect.bottom < 0 || rect.top > window.innerHeight + 200) return false;
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function describe(el) {
|
|
17
|
+
const text = (el.innerText || el.value || '').trim();
|
|
18
|
+
const trimmed = text.length > 40 ? `${text.slice(0, 37)}...` : text;
|
|
19
|
+
return `${el.tagName.toLowerCase()}${trimmed ? ` "${trimmed}"` : ''}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getText(el) {
|
|
23
|
+
return ((el.innerText || el.textContent || '').replace(/\s+/g, ' ') || '').trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isClickable(el) {
|
|
27
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
28
|
+
const tag = el.tagName.toLowerCase();
|
|
29
|
+
const role = (el.getAttribute('role') || '').toLowerCase();
|
|
30
|
+
const tabindex = el.getAttribute('tabindex');
|
|
31
|
+
const style = window.getComputedStyle(el);
|
|
32
|
+
const pointer = style.cursor === 'pointer';
|
|
33
|
+
const dataClick = el.getAttribute('data-action') || el.getAttribute('data-testid') || '';
|
|
34
|
+
return (
|
|
35
|
+
tag === 'button' ||
|
|
36
|
+
tag === 'a' ||
|
|
37
|
+
tag === 'summary' ||
|
|
38
|
+
(['input', 'div', 'span'].includes(tag) && el.getAttribute('onclick') != null) ||
|
|
39
|
+
role === 'button' ||
|
|
40
|
+
role === 'link' ||
|
|
41
|
+
(tabindex && Number(tabindex) >= 0) ||
|
|
42
|
+
pointer ||
|
|
43
|
+
/button|click|toggle|new|plus/.test(dataClick.toLowerCase())
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function findButtonByText(texts) {
|
|
48
|
+
const candidates = [];
|
|
49
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
|
|
50
|
+
while (walker.nextNode()) {
|
|
51
|
+
const el = walker.currentNode;
|
|
52
|
+
if (!isClickable(el)) continue;
|
|
53
|
+
if (!isVisible(el)) continue;
|
|
54
|
+
const text = getText(el).toLowerCase();
|
|
55
|
+
const aria = (
|
|
56
|
+
`${el.getAttribute('aria-label') || ''} ${el.getAttribute('title') || ''} ${el.getAttribute('data-tooltip') || ''}`
|
|
57
|
+
)
|
|
58
|
+
.trim()
|
|
59
|
+
.toLowerCase();
|
|
60
|
+
const combined = `${text} ${aria}`;
|
|
61
|
+
if (!combined) continue;
|
|
62
|
+
if (texts.some(target => combined.includes(target.toLowerCase()))) {
|
|
63
|
+
candidates.push({ el, score: combined.length });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (candidates.length === 0) return null;
|
|
67
|
+
candidates.sort((a, b) => a.score - b.score);
|
|
68
|
+
return candidates[0].el;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function findClosestClickable(el) {
|
|
72
|
+
let current = el;
|
|
73
|
+
while (current && current !== document.body) {
|
|
74
|
+
if (isClickable(current) && isVisible(current)) return current;
|
|
75
|
+
current = current.parentElement;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function findNewChatButton() {
|
|
81
|
+
const button = findButtonByText(BUTTON_TEXTS);
|
|
82
|
+
if (button) return button;
|
|
83
|
+
const maybePlus = Array.from(document.querySelectorAll('[aria-label],[title],[data-testid]')).find(el => {
|
|
84
|
+
if (!isVisible(el)) return false;
|
|
85
|
+
if (!isClickable(el)) return false;
|
|
86
|
+
const label = `${el.getAttribute('aria-label') || ''} ${el.getAttribute('title') || ''} ${el.getAttribute('data-testid') || ''}`
|
|
87
|
+
.toLowerCase();
|
|
88
|
+
return BUTTON_TEXTS.some(t => label.includes(t.toLowerCase()));
|
|
89
|
+
});
|
|
90
|
+
if (maybePlus) return maybePlus;
|
|
91
|
+
|
|
92
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
93
|
+
while (walker.nextNode()) {
|
|
94
|
+
const textNode = walker.currentNode;
|
|
95
|
+
const text = (textNode.textContent || '').toLowerCase().trim();
|
|
96
|
+
if (!text) continue;
|
|
97
|
+
if (!BUTTON_TEXTS.some(t => text.includes(t.toLowerCase()))) continue;
|
|
98
|
+
const clickable = findClosestClickable(textNode.parentElement);
|
|
99
|
+
if (clickable) return clickable;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function scoreInput(el) {
|
|
105
|
+
let score = 0;
|
|
106
|
+
const placeholder = (el.getAttribute('placeholder') || '').toLowerCase();
|
|
107
|
+
const aria = (el.getAttribute('aria-label') || '').toLowerCase();
|
|
108
|
+
const classes = (el.className || '').toLowerCase();
|
|
109
|
+
if (el.tagName === 'TEXTAREA') score += 2;
|
|
110
|
+
if (el.getAttribute('contenteditable') === 'true') score += 3;
|
|
111
|
+
if (placeholder.includes('聊天') || placeholder.includes('chat') || placeholder.includes('message')) score += 3;
|
|
112
|
+
if (aria.includes('聊天') || aria.includes('chat') || aria.includes('message')) score += 3;
|
|
113
|
+
if (placeholder.includes('输入') || aria.includes('输入') || classes.includes('chat')) score += 1;
|
|
114
|
+
if (el.closest('[role="textbox"]')) score += 1;
|
|
115
|
+
return score;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function findChatInput() {
|
|
119
|
+
const selectors = ['textarea', 'input[type="text"]', 'input[type="search"]', '[contenteditable="true"]', '[role="textbox"]'];
|
|
120
|
+
const candidates = Array.from(document.querySelectorAll(selectors.join(','))).filter(isVisible);
|
|
121
|
+
if (candidates.length === 0) return null;
|
|
122
|
+
candidates.sort((a, b) => scoreInput(b) - scoreInput(a));
|
|
123
|
+
return candidates[0] || null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function findInputContainer(input) {
|
|
127
|
+
if (!input) return null;
|
|
128
|
+
return (
|
|
129
|
+
input.closest(
|
|
130
|
+
[
|
|
131
|
+
'[data-testid*="composer"]',
|
|
132
|
+
'[data-testid*="chat-input"]',
|
|
133
|
+
'[data-testid*="message-input"]',
|
|
134
|
+
'[role="textbox"]',
|
|
135
|
+
'[aria-label*="输入"]',
|
|
136
|
+
'[class*="composer"]',
|
|
137
|
+
'[class*="input"]',
|
|
138
|
+
'[class*="message"]',
|
|
139
|
+
'[class*="editor"]',
|
|
140
|
+
'form',
|
|
141
|
+
].join(','),
|
|
142
|
+
) || input.parentElement
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function calculateDistance(a, b) {
|
|
147
|
+
try {
|
|
148
|
+
const rectA = a.getBoundingClientRect();
|
|
149
|
+
const rectB = b.getBoundingClientRect();
|
|
150
|
+
const dx = rectA.left + rectA.width / 2 - (rectB.left + rectB.width / 2);
|
|
151
|
+
const dy = rectA.top + rectA.height / 2 - (rectB.top + rectB.height / 2);
|
|
152
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
return Number.POSITIVE_INFINITY;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function findSendButtonNear(input) {
|
|
159
|
+
if (!input) return null;
|
|
160
|
+
const container = findInputContainer(input) || input.parentElement;
|
|
161
|
+
const roots = [container];
|
|
162
|
+
const inputRect = (() => {
|
|
163
|
+
try {
|
|
164
|
+
return input.getBoundingClientRect();
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
})();
|
|
169
|
+
const candidates = [];
|
|
170
|
+
let fallbackNearest = null;
|
|
171
|
+
let fallbackNearestDistance = Number.POSITIVE_INFINITY;
|
|
172
|
+
let fallbackRightmost = null;
|
|
173
|
+
|
|
174
|
+
for (const root of roots) {
|
|
175
|
+
if (!root) continue;
|
|
176
|
+
const buttons = Array.from(
|
|
177
|
+
root.querySelectorAll('button, [role="button"], input[type="submit"], input[type="button"], a'),
|
|
178
|
+
);
|
|
179
|
+
for (const btn of buttons) {
|
|
180
|
+
if (!isVisible(btn)) continue;
|
|
181
|
+
const label = `${btn.innerText || ''} ${btn.value || ''} ${btn.getAttribute('aria-label') || ''} ${btn.getAttribute('title') || ''}`
|
|
182
|
+
.toLowerCase()
|
|
183
|
+
.trim();
|
|
184
|
+
const type = (btn.getAttribute('type') || '').toLowerCase();
|
|
185
|
+
const distance = calculateDistance(input, btn);
|
|
186
|
+
const hasSvg = !!btn.querySelector('svg');
|
|
187
|
+
const textHasArrow = label.includes('arrow') || (btn.textContent || '').includes('↑');
|
|
188
|
+
const circleLike = (() => {
|
|
189
|
+
try {
|
|
190
|
+
const rect = btn.getBoundingClientRect();
|
|
191
|
+
const style = window.getComputedStyle(btn);
|
|
192
|
+
const radius = parseFloat(style.borderRadius || '0');
|
|
193
|
+
return Math.abs(rect.width - rect.height) < 8 && radius >= rect.width / 3;
|
|
194
|
+
} catch {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
})();
|
|
198
|
+
|
|
199
|
+
const rect = (() => {
|
|
200
|
+
try {
|
|
201
|
+
return btn.getBoundingClientRect();
|
|
202
|
+
} catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
})();
|
|
206
|
+
|
|
207
|
+
let score = 0;
|
|
208
|
+
if (SEND_KEYWORDS.some(k => label.includes(k))) score += 5;
|
|
209
|
+
if (type === 'submit') score += 3;
|
|
210
|
+
if (btn.getAttribute('data-send') === 'true') score += 3;
|
|
211
|
+
const testid = btn.getAttribute('data-testid') || '';
|
|
212
|
+
if (testid.toLowerCase().includes('send')) score += 3;
|
|
213
|
+
if (hasSvg) score += 1;
|
|
214
|
+
if (textHasArrow) score += 2;
|
|
215
|
+
if (circleLike) score += 1;
|
|
216
|
+
if (rect && inputRect) {
|
|
217
|
+
const belowInput = rect.top >= inputRect.bottom - 20;
|
|
218
|
+
const nearRight = rect.right >= inputRect.right - 20;
|
|
219
|
+
const rightOfInput = rect.left >= inputRect.left;
|
|
220
|
+
if (belowInput) score += 2;
|
|
221
|
+
if (nearRight) score += 2;
|
|
222
|
+
if (rightOfInput) score += 1;
|
|
223
|
+
score += Math.max(0, rect.right - inputRect.right) / 150;
|
|
224
|
+
}
|
|
225
|
+
const distanceScore = distance < 500 ? (500 - distance) / 100 : 0;
|
|
226
|
+
score += distanceScore;
|
|
227
|
+
|
|
228
|
+
if (score > 0) {
|
|
229
|
+
candidates.push({ btn, score, distance });
|
|
230
|
+
} else if (distance < fallbackNearestDistance) {
|
|
231
|
+
fallbackNearest = btn;
|
|
232
|
+
fallbackNearestDistance = distance;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (rect && inputRect) {
|
|
236
|
+
const isBelowRow = rect.top >= inputRect.bottom - 60;
|
|
237
|
+
if (isBelowRow) {
|
|
238
|
+
if (
|
|
239
|
+
!fallbackRightmost ||
|
|
240
|
+
rect.right > fallbackRightmost.rect.right + 4 ||
|
|
241
|
+
(rect.right > fallbackRightmost.rect.right - 4 && rect.top > fallbackRightmost.rect.top)
|
|
242
|
+
) {
|
|
243
|
+
fallbackRightmost = { btn, rect };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (candidates.length === 0) {
|
|
250
|
+
if (fallbackRightmost) return fallbackRightmost.btn;
|
|
251
|
+
return fallbackNearestDistance < 300 ? fallbackNearest : null;
|
|
252
|
+
}
|
|
253
|
+
candidates.sort((a, b) => b.score - a.score || a.distance - b.distance);
|
|
254
|
+
return candidates[0].btn;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function setInputValue(el, text) {
|
|
258
|
+
if (el.getAttribute('contenteditable') === 'true') {
|
|
259
|
+
el.focus();
|
|
260
|
+
el.textContent = text;
|
|
261
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
262
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if ('value' in el) {
|
|
266
|
+
el.focus();
|
|
267
|
+
el.value = text;
|
|
268
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
269
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function dispatchEnter(el) {
|
|
274
|
+
if (!el) return;
|
|
275
|
+
el.focus();
|
|
276
|
+
['keydown', 'keypress', 'keyup'].forEach(type => {
|
|
277
|
+
const event = new KeyboardEvent(type, {
|
|
278
|
+
key: 'Enter',
|
|
279
|
+
code: 'Enter',
|
|
280
|
+
keyCode: 13,
|
|
281
|
+
which: 13,
|
|
282
|
+
bubbles: true,
|
|
283
|
+
cancelable: true,
|
|
284
|
+
composed: true,
|
|
285
|
+
});
|
|
286
|
+
el.dispatchEvent(event);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function findAiMessages() {
|
|
291
|
+
const deepseekBlocks = Array.from(document.querySelectorAll('.ds-message .ds-markdown'));
|
|
292
|
+
const deepseekMessages = deepseekBlocks
|
|
293
|
+
.map(el => {
|
|
294
|
+
const text = (el.innerText || '').trim();
|
|
295
|
+
return text ? { element: el, text } : null;
|
|
296
|
+
})
|
|
297
|
+
.filter(Boolean);
|
|
298
|
+
if (deepseekMessages.length > 0) return deepseekMessages;
|
|
299
|
+
|
|
300
|
+
const candidates = Array.from(document.querySelectorAll('article, div, li, section, p, span'));
|
|
301
|
+
const messages = [];
|
|
302
|
+
const seen = new Set();
|
|
303
|
+
|
|
304
|
+
for (const el of candidates) {
|
|
305
|
+
if (!isVisible(el)) continue;
|
|
306
|
+
const text = (el.innerText || '').trim();
|
|
307
|
+
if (!text) continue;
|
|
308
|
+
|
|
309
|
+
const descriptor = [
|
|
310
|
+
el.getAttribute('data-role'),
|
|
311
|
+
el.getAttribute('data-message-author'),
|
|
312
|
+
el.getAttribute('data-author'),
|
|
313
|
+
el.getAttribute('data-testid'),
|
|
314
|
+
el.getAttribute('aria-label'),
|
|
315
|
+
el.className,
|
|
316
|
+
]
|
|
317
|
+
.filter(Boolean)
|
|
318
|
+
.join(' ')
|
|
319
|
+
.toLowerCase();
|
|
320
|
+
|
|
321
|
+
const looksLikeAi =
|
|
322
|
+
descriptor.includes('assistant') ||
|
|
323
|
+
descriptor.includes('ai') ||
|
|
324
|
+
descriptor.includes('bot') ||
|
|
325
|
+
descriptor.includes('reply') ||
|
|
326
|
+
AI_KEYWORDS.some(keyword => descriptor.includes(keyword));
|
|
327
|
+
if (!looksLikeAi) continue;
|
|
328
|
+
|
|
329
|
+
const key = `${text.slice(0, 50)}|${descriptor}`;
|
|
330
|
+
if (seen.has(key)) continue;
|
|
331
|
+
seen.add(key);
|
|
332
|
+
messages.push({ element: el, text });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return messages;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function create_task() {
|
|
339
|
+
const button = findNewChatButton();
|
|
340
|
+
if (!button) {
|
|
341
|
+
return { ok: false, message: '未找到“开启新对话/新建对话”按钮' };
|
|
342
|
+
}
|
|
343
|
+
highlight(button);
|
|
344
|
+
setTimeout(() => button.click(), 1000);
|
|
345
|
+
return { ok: true, message: `找到按钮: ${describe(button)}` };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function send_message(text) {
|
|
349
|
+
const input = findChatInput();
|
|
350
|
+
if (!input) return { ok: false, message: '未找到聊天输入框' };
|
|
351
|
+
setInputValue(input, text || '');
|
|
352
|
+
const sendBtn = findSendButtonNear(input);
|
|
353
|
+
if (sendBtn) {
|
|
354
|
+
highlight(sendBtn);
|
|
355
|
+
sendBtn.click();
|
|
356
|
+
} else {
|
|
357
|
+
highlight(input);
|
|
358
|
+
setTimeout(() => dispatchEnter(input), 1000);
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
ok: true,
|
|
362
|
+
message: `填充消息${sendBtn ? '并尝试点击发送' : ''}`,
|
|
363
|
+
sendButton: sendBtn ? describe(sendBtn) : null,
|
|
364
|
+
input: describe(input),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function receive_message() {
|
|
369
|
+
const messages = findAiMessages();
|
|
370
|
+
if (messages.length === 0) return { ok: false, message: '未找到 AI 聊天内容' };
|
|
371
|
+
const latest = messages[messages.length - 1];
|
|
372
|
+
highlight(latest.element);
|
|
373
|
+
return {
|
|
374
|
+
ok: true,
|
|
375
|
+
message: '获取到 AI 回复',
|
|
376
|
+
latest: latest.text,
|
|
377
|
+
count: messages.length,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function highlightDetectedElements() {
|
|
382
|
+
const found = [];
|
|
383
|
+
const btn = findNewChatButton();
|
|
384
|
+
if (btn) found.push(btn);
|
|
385
|
+
const input = findChatInput();
|
|
386
|
+
if (input) {
|
|
387
|
+
const container = findInputContainer(input);
|
|
388
|
+
found.push(container || input);
|
|
389
|
+
const sendBtn = findSendButtonNear(input);
|
|
390
|
+
if (sendBtn) found.push(sendBtn);
|
|
391
|
+
}
|
|
392
|
+
if (found.length > 0) {
|
|
393
|
+
clearHighlights();
|
|
394
|
+
found.forEach((el, idx) => addHighlight(el, idx === 0));
|
|
395
|
+
}
|
|
396
|
+
return found.length;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
create_task,
|
|
401
|
+
send_message,
|
|
402
|
+
receive_message,
|
|
403
|
+
highlightDetectedElements,
|
|
404
|
+
};
|
|
405
|
+
}
|