yiyan-browser-agent 1.0.28 → 1.0.30
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/package.json +1 -1
- package/src/agent.js +15 -44
- package/src/index.js +55 -162
- package/src/browser-manager.js +0 -128
- package/src/page-agent.js +0 -157
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -1,68 +1,46 @@
|
|
|
1
|
-
// src/agent.js —
|
|
1
|
+
// src/agent.js — Simple agent, one browser per process
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const config
|
|
5
|
-
const logger
|
|
6
|
-
const
|
|
7
|
-
const PageAgent = require('./page-agent');
|
|
8
|
-
|
|
9
|
-
// ─────────────────────────────────────────────
|
|
10
|
-
// Agent class
|
|
11
|
-
// ─────────────────────────────────────────────
|
|
4
|
+
const config = require('./config');
|
|
5
|
+
const logger = require('./logger');
|
|
6
|
+
const YiyanBrowser = require('./browser');
|
|
12
7
|
|
|
13
8
|
class YiyanAgent {
|
|
14
9
|
constructor(options = {}) {
|
|
15
|
-
this.
|
|
16
|
-
this.options
|
|
17
|
-
this._running = false;
|
|
10
|
+
this.browser = new YiyanBrowser();
|
|
11
|
+
this.options = options;
|
|
18
12
|
}
|
|
19
13
|
|
|
20
|
-
// ── Public API ──────────────────────────────────────────────────────────────
|
|
21
|
-
|
|
22
|
-
/** Get browser and create page agent */
|
|
23
14
|
async init() {
|
|
24
|
-
|
|
25
|
-
this.pageAgent = new PageAgent(page);
|
|
15
|
+
await this.browser.launch();
|
|
26
16
|
}
|
|
27
17
|
|
|
28
|
-
/** Shutdown */
|
|
29
18
|
async shutdown() {
|
|
30
|
-
await
|
|
19
|
+
await this.browser.close();
|
|
31
20
|
}
|
|
32
21
|
|
|
33
|
-
/**
|
|
34
|
-
* Run a task
|
|
35
|
-
*/
|
|
36
22
|
async run(task) {
|
|
37
|
-
this._running = true;
|
|
38
23
|
const startTime = Date.now();
|
|
39
24
|
|
|
40
25
|
logger.info('Sending task to Yiyan...');
|
|
41
|
-
await this.
|
|
26
|
+
await this.browser.sendMessage(task);
|
|
42
27
|
|
|
43
28
|
logger.info('Waiting for response...');
|
|
44
|
-
const answer = await this.
|
|
29
|
+
const answer = await this.browser.waitForResponse();
|
|
45
30
|
|
|
46
31
|
const duration = Date.now() - startTime;
|
|
47
|
-
this._running = false;
|
|
48
32
|
|
|
49
33
|
return {
|
|
50
34
|
question: task,
|
|
51
|
-
answer: answer || 'No response
|
|
35
|
+
answer: answer || 'No response',
|
|
52
36
|
duration,
|
|
53
37
|
status: answer ? 'success' : 'error'
|
|
54
38
|
};
|
|
55
39
|
}
|
|
56
40
|
|
|
57
|
-
// ── Interactive Mode ────────────────────────────────────────────────────────
|
|
58
|
-
|
|
59
41
|
async runInteractive() {
|
|
60
42
|
const readline = require('readline');
|
|
61
|
-
const rl = readline.createInterface({
|
|
62
|
-
input: process.stdin,
|
|
63
|
-
output: process.stdout,
|
|
64
|
-
});
|
|
65
|
-
|
|
43
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
66
44
|
const ask = () => new Promise(resolve => rl.question('', resolve));
|
|
67
45
|
|
|
68
46
|
while (true) {
|
|
@@ -71,22 +49,15 @@ class YiyanAgent {
|
|
|
71
49
|
if (['exit', 'quit', 'q'].includes(task.toLowerCase())) break;
|
|
72
50
|
|
|
73
51
|
try {
|
|
74
|
-
|
|
75
|
-
await this.init();
|
|
52
|
+
await this.browser.newChat();
|
|
76
53
|
const result = await this.run(task);
|
|
77
54
|
console.log(JSON.stringify(result, null, 2));
|
|
78
55
|
} catch (err) {
|
|
79
|
-
console.log(JSON.stringify({
|
|
80
|
-
question: task,
|
|
81
|
-
answer: `Error: ${err.message}`,
|
|
82
|
-
duration: 0,
|
|
83
|
-
status: 'error'
|
|
84
|
-
}, null, 2));
|
|
56
|
+
console.log(JSON.stringify({ question: task, answer: `Error: ${err.message}`, duration: 0, status: 'error' }, null, 2));
|
|
85
57
|
}
|
|
86
58
|
}
|
|
87
|
-
|
|
88
59
|
rl.close();
|
|
89
60
|
}
|
|
90
61
|
}
|
|
91
62
|
|
|
92
|
-
module.exports = YiyanAgent;
|
|
63
|
+
module.exports = YiyanAgent;
|
package/src/index.js
CHANGED
|
@@ -1,223 +1,116 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// src/index.js — CLI entry point
|
|
2
|
+
// src/index.js — CLI entry point
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
|
-
const path
|
|
6
|
-
const fs
|
|
7
|
-
const config
|
|
8
|
-
const logger
|
|
9
|
-
const YiyanAgent
|
|
10
|
-
const BrowserManager = require('./browser-manager');
|
|
11
|
-
|
|
12
|
-
// ─────────────────────────────────────────────
|
|
13
|
-
// Parse CLI arguments
|
|
14
|
-
// ─────────────────────────────────────────────
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const config = require('./config');
|
|
8
|
+
const logger = require('./logger');
|
|
9
|
+
const YiyanAgent = require('./agent');
|
|
15
10
|
|
|
16
11
|
function parseArgs(argv) {
|
|
17
12
|
const args = argv.slice(2);
|
|
18
|
-
const opts = {
|
|
19
|
-
task : null,
|
|
20
|
-
interactive : false,
|
|
21
|
-
debug : false,
|
|
22
|
-
headless : false,
|
|
23
|
-
saveLog : false,
|
|
24
|
-
workingDir : null,
|
|
25
|
-
calibrate : false,
|
|
26
|
-
help : false,
|
|
27
|
-
};
|
|
13
|
+
const opts = { task: null, interactive: false, debug: false, headless: false, showBrowser: false, workingDir: null, calibrate: false, help: false };
|
|
28
14
|
|
|
29
|
-
let i = 0;
|
|
30
|
-
while (i < args.length) {
|
|
15
|
+
for (let i = 0; i < args.length; i++) {
|
|
31
16
|
const a = args[i];
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
case '-d':
|
|
43
|
-
case '--dir':
|
|
44
|
-
opts.workingDir = args[++i];
|
|
45
|
-
break;
|
|
46
|
-
|
|
47
|
-
case '-t':
|
|
48
|
-
case '--task':
|
|
49
|
-
opts.task = args[++i];
|
|
50
|
-
break;
|
|
51
|
-
|
|
52
|
-
default:
|
|
53
|
-
// If it doesn't start with '-', treat it as an inline task
|
|
54
|
-
if (!a.startsWith('-')) {
|
|
55
|
-
opts.task = args.slice(i).join(' ');
|
|
56
|
-
i = args.length; // consume the rest
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
i++;
|
|
17
|
+
if (a === '-i' || a === '--interactive') opts.interactive = true;
|
|
18
|
+
else if (a === '--debug') opts.debug = true;
|
|
19
|
+
else if (a === '--headless') opts.headless = true;
|
|
20
|
+
else if (a === '--show-browser') opts.showBrowser = true;
|
|
21
|
+
else if (a === '--calibrate') opts.calibrate = true;
|
|
22
|
+
else if (a === '-h' || a === '--help') opts.help = true;
|
|
23
|
+
else if (a === '-d' || a === '--dir') opts.workingDir = args[++i];
|
|
24
|
+
else if (a === '-t' || a === '--task') opts.task = args[++i];
|
|
25
|
+
else if (!a.startsWith('-')) { opts.task = args.slice(i).join(' '); break; }
|
|
60
26
|
}
|
|
61
|
-
|
|
62
27
|
return opts;
|
|
63
28
|
}
|
|
64
29
|
|
|
65
|
-
// ─────────────────────────────────────────────
|
|
66
|
-
// Help text
|
|
67
|
-
// ─────────────────────────────────────────────
|
|
68
|
-
|
|
69
30
|
function printHelp() {
|
|
70
31
|
console.log(`
|
|
71
|
-
\x1b[1mYIYAN AGENT
|
|
32
|
+
\x1b[1mYIYAN AGENT\x1b[0m — AI Coding Agent via Browser
|
|
72
33
|
|
|
73
34
|
\x1b[33mUSAGE\x1b[0m
|
|
74
|
-
|
|
35
|
+
yiyan-agent [TASK] # Headless mode (no browser window)
|
|
36
|
+
yiyan-agent -i # Interactive mode (shows browser for login)
|
|
75
37
|
|
|
76
38
|
\x1b[33mOPTIONS\x1b[0m
|
|
77
|
-
-
|
|
78
|
-
-
|
|
79
|
-
|
|
80
|
-
--
|
|
81
|
-
--
|
|
82
|
-
--save-log Save conversation log to ~/.yiyan-agent/logs/
|
|
83
|
-
--calibrate Open browser and print DOM info to help fix selectors
|
|
84
|
-
-h, --help Show this help
|
|
39
|
+
-i, --interactive Interactive mode (shows browser)
|
|
40
|
+
--show-browser Show browser window for single task
|
|
41
|
+
--debug Show debug info
|
|
42
|
+
--calibrate Debug DOM selectors
|
|
43
|
+
-h, --help Show help
|
|
85
44
|
|
|
86
45
|
\x1b[33mEXAMPLES\x1b[0m
|
|
87
|
-
#
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
# Interactive mode (recommended)
|
|
91
|
-
node src/index.js --interactive
|
|
92
|
-
|
|
93
|
-
# Run on a specific project directory
|
|
94
|
-
node src/index.js --dir ~/projects/myapp "Add TypeScript to this project"
|
|
95
|
-
|
|
96
|
-
# Debug mode (shows raw responses)
|
|
97
|
-
node src/index.js --debug "Write a binary search in Python"
|
|
98
|
-
|
|
99
|
-
# Headless (faster, requires prior login)
|
|
100
|
-
node src/index.js --headless "Refactor index.js to use async/await"
|
|
101
|
-
|
|
102
|
-
\x1b[33mFIRST-TIME SETUP\x1b[0m
|
|
103
|
-
1. npm run setup (installs deps + Playwright browser)
|
|
104
|
-
2. node src/index.js -i (opens browser, log in to Yiyan, then use normally)
|
|
105
|
-
Session is saved — you only log in once.
|
|
106
|
-
|
|
107
|
-
\x1b[33mCONFIG FILE\x1b[0m
|
|
108
|
-
Create \x1b[36myiyan-agent.config.json\x1b[0m in your working directory to override settings:
|
|
109
|
-
{
|
|
110
|
-
"HEADLESS": true,
|
|
111
|
-
"MAX_ITERATIONS": 50,
|
|
112
|
-
"STABLE_DELAY": 3000
|
|
113
|
-
}
|
|
46
|
+
yiyan-agent "济宁天气" # Runs headless (fast)
|
|
47
|
+
yiyan-agent -i # Login first time
|
|
48
|
+
yiyan-agent "写个脚本" --show-browser # Watch browser work
|
|
114
49
|
`);
|
|
115
50
|
}
|
|
116
51
|
|
|
117
|
-
// ─────────────────────────────────────────────
|
|
118
|
-
// Main
|
|
119
|
-
// ─────────────────────────────────────────────
|
|
120
|
-
|
|
121
52
|
async function main() {
|
|
122
53
|
const opts = parseArgs(process.argv);
|
|
123
54
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
process.exit(0);
|
|
128
|
-
}
|
|
55
|
+
if (opts.help) { printHelp(); process.exit(0); }
|
|
56
|
+
|
|
57
|
+
if (opts.debug) config.DEBUG = true;
|
|
129
58
|
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
if (opts.
|
|
59
|
+
// Headless logic: interactive mode shows browser, single task runs headless by default
|
|
60
|
+
// User can override with --show-browser to watch single task
|
|
61
|
+
if (opts.interactive || opts.calibrate || opts.showBrowser) {
|
|
62
|
+
config.HEADLESS = false; // Show browser
|
|
63
|
+
} else {
|
|
64
|
+
config.HEADLESS = true; // Single task: headless by default (faster)
|
|
65
|
+
}
|
|
133
66
|
if (opts.workingDir) {
|
|
134
67
|
const resolved = path.resolve(opts.workingDir);
|
|
135
|
-
if (!fs.existsSync(resolved)) {
|
|
136
|
-
logger.error(`Working directory not found: ${resolved}`);
|
|
137
|
-
process.exit(1);
|
|
138
|
-
}
|
|
68
|
+
if (!fs.existsSync(resolved)) { logger.error(`Dir not found: ${resolved}`); process.exit(1); }
|
|
139
69
|
config.WORKING_DIR = resolved;
|
|
140
70
|
}
|
|
141
71
|
|
|
142
|
-
// ── Banner ─────────────────────────────────────────────────────────────────
|
|
143
72
|
logger.banner();
|
|
144
|
-
logger.info(`Working
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const agent = new YiyanAgent({ saveLog: opts.saveLog });
|
|
152
|
-
|
|
153
|
-
// ── Graceful shutdown handler ──────────────────────────────────────────────
|
|
154
|
-
// Only delete endpoint file on Ctrl+C (explicit exit)
|
|
155
|
-
const shutdown = async (code = 0, keepBrowser = true) => {
|
|
156
|
-
if (!keepBrowser) {
|
|
157
|
-
try { BrowserManager.forceClose(); } catch {}
|
|
158
|
-
}
|
|
73
|
+
logger.info(`Working dir: ${config.WORKING_DIR}`);
|
|
74
|
+
|
|
75
|
+
const agent = new YiyanAgent();
|
|
76
|
+
|
|
77
|
+
const shutdown = async (code = 0) => {
|
|
78
|
+
logger.info('Shutting down...');
|
|
79
|
+
try { await agent.shutdown(); } catch {}
|
|
159
80
|
process.exit(code);
|
|
160
81
|
};
|
|
161
82
|
|
|
162
|
-
process.on('SIGINT',
|
|
163
|
-
process.on('SIGTERM', () => shutdown(0
|
|
164
|
-
|
|
165
|
-
logger.error(`Uncaught error: ${err.message}`);
|
|
166
|
-
if (config.DEBUG) console.error(err.stack);
|
|
167
|
-
await shutdown(1, false); // Error: close browser
|
|
168
|
-
});
|
|
169
|
-
process.on('unhandledRejection', async reason => {
|
|
170
|
-
logger.error(`Unhandled rejection: ${reason}`);
|
|
171
|
-
if (config.DEBUG) console.error(reason);
|
|
172
|
-
await shutdown(1, false); // Error: close browser
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// ── Calibrate mode ─────────────────────────────────────────────────────────
|
|
83
|
+
process.on('SIGINT', () => shutdown(0));
|
|
84
|
+
process.on('SIGTERM', () => shutdown(0));
|
|
85
|
+
|
|
176
86
|
if (opts.calibrate) {
|
|
177
|
-
logger.header('Calibration Mode — Reading DOM selectors');
|
|
178
87
|
await agent.init();
|
|
179
88
|
await agent.browser.dumpDebugInfo();
|
|
180
89
|
await agent.browser.screenshot();
|
|
181
|
-
logger.info('Done. Check the output above to update selectors in src/browser.js if needed.');
|
|
182
90
|
await shutdown(0);
|
|
183
91
|
}
|
|
184
92
|
|
|
185
|
-
|
|
186
|
-
if (!opts.interactive && !opts.task) {
|
|
187
|
-
logger.warn('No task provided. Switching to interactive mode...\n');
|
|
188
|
-
opts.interactive = true;
|
|
189
|
-
}
|
|
93
|
+
if (!opts.interactive && !opts.task) opts.interactive = true;
|
|
190
94
|
|
|
191
|
-
// ── Launch browser ─────────────────────────────────────────────────────────
|
|
192
95
|
try {
|
|
193
96
|
await agent.init();
|
|
194
97
|
} catch (err) {
|
|
195
|
-
logger.error(`Failed
|
|
196
|
-
if (config.DEBUG) console.error(err.stack);
|
|
98
|
+
logger.error(`Failed: ${err.message}`);
|
|
197
99
|
process.exit(1);
|
|
198
100
|
}
|
|
199
101
|
|
|
200
|
-
// ── Run ────────────────────────────────────────────────────────────────────
|
|
201
102
|
try {
|
|
202
103
|
if (opts.interactive) {
|
|
203
104
|
await agent.runInteractive();
|
|
204
|
-
await shutdown(0, true);
|
|
205
105
|
} else {
|
|
206
106
|
const result = await agent.run(opts.task);
|
|
207
107
|
console.log(JSON.stringify(result, null, 2));
|
|
208
|
-
// Success: don't close browser, next process can connect to it
|
|
209
|
-
// Error: close browser
|
|
210
|
-
process.exit(result.status === 'error' ? 1 : 0);
|
|
211
108
|
}
|
|
212
109
|
} catch (err) {
|
|
213
|
-
console.log(JSON.stringify({
|
|
214
|
-
question: opts.task || '',
|
|
215
|
-
answer: `Error: ${err.message}`,
|
|
216
|
-
duration: 0,
|
|
217
|
-
status: 'error'
|
|
218
|
-
}, null, 2));
|
|
219
|
-
await shutdown(1, true); // Error: close browser
|
|
110
|
+
console.log(JSON.stringify({ question: opts.task || '', answer: `Error: ${err.message}`, duration: 0, status: 'error' }, null, 2));
|
|
220
111
|
}
|
|
112
|
+
|
|
113
|
+
await shutdown(0);
|
|
221
114
|
}
|
|
222
115
|
|
|
223
|
-
main();
|
|
116
|
+
main();
|
package/src/browser-manager.js
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
// src/browser-manager.js — Cross-process browser reuse via CDP
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const { chromium } = require('playwright');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const os = require('os');
|
|
7
|
-
const fs = require('fs');
|
|
8
|
-
const logger = require('./logger');
|
|
9
|
-
const config = require('./config');
|
|
10
|
-
|
|
11
|
-
const CDP_PORT_FILE = path.join(os.homedir(), '.yiyan-agent', 'cdp-port.json');
|
|
12
|
-
const CDP_PORT = 9222;
|
|
13
|
-
const SESSION_DIR = path.join(os.homedir(), '.yiyan-agent', 'session');
|
|
14
|
-
|
|
15
|
-
class BrowserManager {
|
|
16
|
-
/**
|
|
17
|
-
* Get browser - connect existing or launch new
|
|
18
|
-
*/
|
|
19
|
-
static async getInstance() {
|
|
20
|
-
// Ensure directories exist
|
|
21
|
-
fs.mkdirSync(SESSION_DIR, { recursive: true });
|
|
22
|
-
|
|
23
|
-
// Try connect existing browser (with retries for concurrent launches)
|
|
24
|
-
for (let retry = 0; retry < 3; retry++) {
|
|
25
|
-
try {
|
|
26
|
-
if (fs.existsSync(CDP_PORT_FILE)) {
|
|
27
|
-
const endpointInfo = JSON.parse(fs.readFileSync(CDP_PORT_FILE, 'utf8'));
|
|
28
|
-
const wsEndpoint = endpointInfo.wsEndpoint;
|
|
29
|
-
|
|
30
|
-
if (!wsEndpoint) {
|
|
31
|
-
throw new Error('No wsEndpoint in file');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
logger.info('Connecting to existing browser...');
|
|
35
|
-
const browser = await chromium.connect({ wsEndpoint, timeout: 10000 });
|
|
36
|
-
|
|
37
|
-
// Show current browser info
|
|
38
|
-
const existingContexts = browser.contexts();
|
|
39
|
-
const existingPages = existingContexts.reduce((sum, ctx) => sum + ctx.pages().length, 0);
|
|
40
|
-
logger.dim(`Browser has ${existingContexts.length} contexts, ${existingPages} existing tabs`);
|
|
41
|
-
|
|
42
|
-
// Create NEW context and page (tab) for this task
|
|
43
|
-
const context = await browser.newContext({
|
|
44
|
-
viewport: { width: 1280, height: 900 },
|
|
45
|
-
userAgent: 'Mozilla/5.0 AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36',
|
|
46
|
-
});
|
|
47
|
-
const page = await context.newPage();
|
|
48
|
-
await page.goto(config.YIYAN_URL, { waitUntil: 'networkidle', timeout: 20000 });
|
|
49
|
-
await page.waitForTimeout(1500);
|
|
50
|
-
|
|
51
|
-
logger.success('Connected! New tab opened.');
|
|
52
|
-
return { browser, context, page, isNew: false };
|
|
53
|
-
}
|
|
54
|
-
} catch (err) {
|
|
55
|
-
// If connection failed, wait and retry (another process might be starting)
|
|
56
|
-
if (retry < 2) {
|
|
57
|
-
logger.warn(`Connection attempt ${retry + 1} failed, waiting 3 seconds...`);
|
|
58
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
59
|
-
} else {
|
|
60
|
-
logger.warn('Connection failed after retries, launching new browser...');
|
|
61
|
-
try { fs.unlinkSync(CDP_PORT_FILE); } catch {}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Launch new browser
|
|
67
|
-
return await this._launchNew();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Launch new browser with CDP port
|
|
72
|
-
*/
|
|
73
|
-
static async _launchNew() {
|
|
74
|
-
logger.info('Launching new browser server...');
|
|
75
|
-
|
|
76
|
-
// Use launchServer to create independent browser process
|
|
77
|
-
const browserServer = await chromium.launchServer({
|
|
78
|
-
headless: false,
|
|
79
|
-
args: [
|
|
80
|
-
'--disable-blink-features=AutomationControlled',
|
|
81
|
-
'--no-first-run',
|
|
82
|
-
'--no-sandbox',
|
|
83
|
-
],
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// Get the WebSocket endpoint
|
|
87
|
-
const wsEndpoint = browserServer.wsEndpoint();
|
|
88
|
-
logger.dim('WebSocket endpoint: ' + wsEndpoint);
|
|
89
|
-
|
|
90
|
-
// Save endpoint info IMMEDIATELY before any other operations
|
|
91
|
-
fs.writeFileSync(CDP_PORT_FILE, JSON.stringify({
|
|
92
|
-
wsEndpoint: wsEndpoint,
|
|
93
|
-
launchedAt: Date.now()
|
|
94
|
-
}));
|
|
95
|
-
logger.dim('Endpoint saved to: ' + CDP_PORT_FILE);
|
|
96
|
-
|
|
97
|
-
logger.success('Browser server started!');
|
|
98
|
-
|
|
99
|
-
// Connect to our own server
|
|
100
|
-
const browser = await chromium.connect({ wsEndpoint });
|
|
101
|
-
|
|
102
|
-
// Create context and page
|
|
103
|
-
const context = await browser.newContext({
|
|
104
|
-
viewport: { width: 1280, height: 900 },
|
|
105
|
-
userAgent: 'Mozilla/5.0 AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36',
|
|
106
|
-
});
|
|
107
|
-
const page = await context.newPage();
|
|
108
|
-
|
|
109
|
-
await page.goto(config.YIYAN_URL, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
110
|
-
await page.waitForTimeout(800);
|
|
111
|
-
|
|
112
|
-
logger.success('Tab ready.');
|
|
113
|
-
|
|
114
|
-
// Return without browserServer - let it run independently
|
|
115
|
-
// Do NOT close browserServer on process exit
|
|
116
|
-
return { browser, context, page, isNew: true };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Force close everything (for cleanup)
|
|
121
|
-
*/
|
|
122
|
-
static async forceClose() {
|
|
123
|
-
try { fs.unlinkSync(CDP_PORT_FILE); } catch {}
|
|
124
|
-
// Browser continues running independently
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
module.exports = BrowserManager;
|
package/src/page-agent.js
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
// src/page-agent.js — Agent for a single page/tab
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const logger = require('./logger');
|
|
5
|
-
const config = require('./config');
|
|
6
|
-
|
|
7
|
-
// Selectors for Yiyan
|
|
8
|
-
const SEL = {
|
|
9
|
-
chatInput: [
|
|
10
|
-
'.editable__T7WAW4uW',
|
|
11
|
-
'[role="textbox"]',
|
|
12
|
-
'[contenteditable="true"]',
|
|
13
|
-
'textarea',
|
|
14
|
-
],
|
|
15
|
-
sendButton: [
|
|
16
|
-
'button[aria-label*="发送"]',
|
|
17
|
-
'button[type="submit"]',
|
|
18
|
-
],
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
class PageAgent {
|
|
22
|
-
constructor(page) {
|
|
23
|
-
this.page = page;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Send message to Yiyan
|
|
28
|
-
*/
|
|
29
|
-
async sendMessage(text) {
|
|
30
|
-
// Wait for page to be fully loaded
|
|
31
|
-
await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
32
|
-
await this.page.waitForTimeout(1000);
|
|
33
|
-
|
|
34
|
-
// Find input
|
|
35
|
-
let inputEl = null;
|
|
36
|
-
for (const sel of SEL.chatInput) {
|
|
37
|
-
try {
|
|
38
|
-
inputEl = await this.page.waitForSelector(sel, { timeout: 8000, state: 'visible' });
|
|
39
|
-
if (inputEl) {
|
|
40
|
-
logger.dim('Found input: ' + sel);
|
|
41
|
-
break;
|
|
42
|
-
}
|
|
43
|
-
} catch {}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (!inputEl) {
|
|
47
|
-
throw new Error('Cannot find input box. Make sure Yiyan page is loaded.');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Focus and clear
|
|
51
|
-
await inputEl.click({ clickCount: 3, force: true });
|
|
52
|
-
await this.page.waitForTimeout(100);
|
|
53
|
-
await this.page.keyboard.press('Delete');
|
|
54
|
-
await this.page.waitForTimeout(50);
|
|
55
|
-
|
|
56
|
-
// Type message
|
|
57
|
-
await this.page.keyboard.type(text, { delay: 10 });
|
|
58
|
-
await this.page.keyboard.press('Enter');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Wait for response
|
|
63
|
-
*/
|
|
64
|
-
async waitForResponse() {
|
|
65
|
-
const timeout = config.RESPONSE_TIMEOUT;
|
|
66
|
-
const stableDelay = config.STABLE_DELAY;
|
|
67
|
-
const start = Date.now();
|
|
68
|
-
|
|
69
|
-
// Wait for new content
|
|
70
|
-
await this.page.waitForTimeout(1000);
|
|
71
|
-
|
|
72
|
-
// Poll for stable response
|
|
73
|
-
let lastText = '';
|
|
74
|
-
let stableStart = null;
|
|
75
|
-
|
|
76
|
-
while (Date.now() - start < timeout) {
|
|
77
|
-
const text = await this._extractAnswer();
|
|
78
|
-
|
|
79
|
-
if (text !== lastText && text.length > 0) {
|
|
80
|
-
lastText = text;
|
|
81
|
-
stableStart = Date.now();
|
|
82
|
-
} else if (stableStart && Date.now() - stableStart >= stableDelay) {
|
|
83
|
-
if (!await this._isGenerating()) {
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
stableStart = null;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
await this.page.waitForTimeout(200);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return this._cleanText(lastText);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Extract answer from page
|
|
97
|
-
*/
|
|
98
|
-
async _extractAnswer() {
|
|
99
|
-
return await this.page.evaluate(() => {
|
|
100
|
-
// Try specific selector first
|
|
101
|
-
const answerEl = document.querySelector('#answer_text_id');
|
|
102
|
-
if (answerEl) return answerEl.textContent || '';
|
|
103
|
-
|
|
104
|
-
// Fallback
|
|
105
|
-
const candidates = document.querySelectorAll('[class*="answer"], [class*="response"], [class*="markdown"]');
|
|
106
|
-
for (const el of candidates) {
|
|
107
|
-
const text = el.textContent || '';
|
|
108
|
-
if (text.length > 20) return text;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return '';
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Check if still generating
|
|
117
|
-
*/
|
|
118
|
-
async _isGenerating() {
|
|
119
|
-
return await this.page.evaluate(() => {
|
|
120
|
-
const stopBtn = document.querySelector('button[aria-label*="停止"]');
|
|
121
|
-
if (stopBtn && stopBtn.offsetParent !== null) return true;
|
|
122
|
-
|
|
123
|
-
const loading = document.querySelector('[class*="loading"], [class*="typing"]');
|
|
124
|
-
if (loading) return true;
|
|
125
|
-
|
|
126
|
-
return false;
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Clean response text
|
|
132
|
-
*/
|
|
133
|
-
_cleanText(text) {
|
|
134
|
-
if (!text) return '';
|
|
135
|
-
|
|
136
|
-
// Remove before "准备输出结果"
|
|
137
|
-
const marker = '准备输出结果';
|
|
138
|
-
const idx = text.indexOf(marker);
|
|
139
|
-
if (idx !== -1) {
|
|
140
|
-
text = text.slice(idx + marker.length).trim();
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Remove after markers
|
|
144
|
-
const cutMarkers = ['重新生成', '换个回答', '输出更详细的', '再多提供'];
|
|
145
|
-
for (const m of cutMarkers) {
|
|
146
|
-
const cutIdx = text.indexOf(m);
|
|
147
|
-
if (cutIdx !== -1) {
|
|
148
|
-
text = text.slice(0, cutIdx).trim();
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return text.trim();
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
module.exports = PageAgent;
|