yiyan-browser-agent 1.0.1 β 1.0.2
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/LICENSE +1 -1
- package/README.md +265 -94
- package/package.json +28 -30
- package/src/agent.js +257 -0
- package/src/browser.js +624 -0
- package/src/calibrate.js +178 -0
- package/src/config.js +68 -0
- package/src/index.js +218 -0
- package/src/logger.js +118 -0
- package/src/parser.js +272 -0
- package/src/postinstall.js +47 -0
- package/src/prompt.js +188 -0
- package/src/tools.js +547 -0
- package/dist/cli.d.ts +0 -26
- package/dist/cli.js +0 -1048
- package/dist/cli.js.map +0 -1
- package/dist/index.d.ts +0 -37
- package/dist/index.js +0 -874
- package/dist/index.js.map +0 -1
- package/dist/types-BhQ78DYf.d.ts +0 -39
package/src/calibrate.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// src/calibrate.js β Auto-detect Yiyan UI selectors and update browser.js
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const { chromium } = require('playwright');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const config = require('./config');
|
|
9
|
+
|
|
10
|
+
console.log('\nπ¬ Yiyan Agent β Selector Calibration Tool\n');
|
|
11
|
+
console.log('This tool opens Yiyan (ζεΏδΈθ¨), inspects the DOM, and prints out');
|
|
12
|
+
console.log('the selectors that your browser.js should use.\n');
|
|
13
|
+
|
|
14
|
+
async function calibrate() {
|
|
15
|
+
const context = await chromium.launchPersistentContext(config.SESSION_DIR, {
|
|
16
|
+
headless : false,
|
|
17
|
+
viewport : { width: 1280, height: 900 },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const pages = context.pages();
|
|
21
|
+
const page = pages.length > 0 ? pages[0] : await context.newPage();
|
|
22
|
+
|
|
23
|
+
console.log('β Navigating to', config.YIYAN_URL, '...');
|
|
24
|
+
await page.goto(config.YIYAN_URL, { waitUntil: 'domcontentloaded', timeout: 30_000 });
|
|
25
|
+
await page.waitForTimeout(3_000);
|
|
26
|
+
|
|
27
|
+
console.log('β Inspecting DOM...\n');
|
|
28
|
+
|
|
29
|
+
const report = await page.evaluate(() => {
|
|
30
|
+
function isVisible(el) {
|
|
31
|
+
const s = window.getComputedStyle(el);
|
|
32
|
+
return s.display !== 'none' && s.visibility !== 'hidden' && s.opacity !== '0' && el.offsetParent !== null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function classify(el) {
|
|
36
|
+
return {
|
|
37
|
+
tag : el.tagName.toLowerCase(),
|
|
38
|
+
id : el.id || null,
|
|
39
|
+
classes : el.className?.slice?.(0, 120) || null,
|
|
40
|
+
placeholder : el.placeholder || null,
|
|
41
|
+
ariaLabel : el.getAttribute('aria-label') || null,
|
|
42
|
+
dataTestId : el.dataset?.testid || null,
|
|
43
|
+
role : el.getAttribute('role') || null,
|
|
44
|
+
visible : isVisible(el),
|
|
45
|
+
text : (el.innerText || '').slice(0, 40).replace(/\n/g, ' ') || null,
|
|
46
|
+
type : el.getAttribute('type') || null,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ββ Inputs βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
51
|
+
const inputs = Array.from(document.querySelectorAll('textarea, [contenteditable="true"]'))
|
|
52
|
+
.map(classify);
|
|
53
|
+
|
|
54
|
+
// ββ Buttons ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
55
|
+
const buttons = Array.from(document.querySelectorAll('button, [role="button"]'))
|
|
56
|
+
.filter(isVisible)
|
|
57
|
+
.map(classify)
|
|
58
|
+
.slice(0, 30);
|
|
59
|
+
|
|
60
|
+
// ββ All named classes βββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
61
|
+
const classFreq = {};
|
|
62
|
+
document.querySelectorAll('*').forEach(el => {
|
|
63
|
+
(el.getAttribute('class') || '').split(/\s+/).forEach(c => {
|
|
64
|
+
if (c.length > 2 && c.length < 50) {
|
|
65
|
+
classFreq[c] = (classFreq[c] || 0) + 1;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
const topClasses = Object.entries(classFreq)
|
|
70
|
+
.sort((a, b) => b[1] - a[1])
|
|
71
|
+
.slice(0, 80)
|
|
72
|
+
.map(([cls, n]) => ({ cls, n }));
|
|
73
|
+
|
|
74
|
+
// ββ Suggested selectors βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
75
|
+
const suggestedInput = (
|
|
76
|
+
inputs.find(i => i.placeholder?.toLowerCase().includes('message'))?.id ||
|
|
77
|
+
inputs.find(i => i.placeholder?.toLowerCase().includes('ask'))?.id ||
|
|
78
|
+
inputs.find(i => i.visible)?.id ||
|
|
79
|
+
null
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const sendBtn = buttons.find(b =>
|
|
83
|
+
/send/i.test(b.ariaLabel || '') ||
|
|
84
|
+
/send/i.test(b.text || '') ||
|
|
85
|
+
/send/i.test(b.classes || '')
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const stopBtn = buttons.find(b =>
|
|
89
|
+
/stop/i.test(b.ariaLabel || '') ||
|
|
90
|
+
/stop/i.test(b.text || '') ||
|
|
91
|
+
/stop/i.test(b.classes || '')
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const newChatBtn = buttons.find(b =>
|
|
95
|
+
/new chat/i.test(b.ariaLabel || '') ||
|
|
96
|
+
/new chat/i.test(b.text || '') ||
|
|
97
|
+
/new.?chat/i.test(b.classes || '')
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
url : window.location.href,
|
|
102
|
+
title : document.title,
|
|
103
|
+
inputs,
|
|
104
|
+
buttons,
|
|
105
|
+
topClasses,
|
|
106
|
+
suggestions: { suggestedInput, sendBtn, stopBtn, newChatBtn },
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ββ Print report ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
111
|
+
const sep = 'β'.repeat(60);
|
|
112
|
+
|
|
113
|
+
console.log(sep);
|
|
114
|
+
console.log('URL :', report.url);
|
|
115
|
+
console.log('Title :', report.title);
|
|
116
|
+
console.log(sep);
|
|
117
|
+
|
|
118
|
+
console.log('\nπ₯ INPUT ELEMENTS:');
|
|
119
|
+
if (report.inputs.length === 0) {
|
|
120
|
+
console.log(' (none found β are you logged in?)');
|
|
121
|
+
}
|
|
122
|
+
report.inputs.forEach((el, i) => {
|
|
123
|
+
console.log(` [${i}] ${JSON.stringify(el)}`);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
console.log('\nπ BUTTONS (visible, first 30):');
|
|
127
|
+
report.buttons.forEach((el, i) => {
|
|
128
|
+
console.log(` [${i}] ${JSON.stringify(el)}`);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
console.log('\nπ·οΈ TOP CSS CLASSES:');
|
|
132
|
+
report.topClasses.slice(0, 40).forEach(({ cls, n }) => {
|
|
133
|
+
console.log(` ${String(n).padStart(4)}x .${cls}`);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
console.log('\n' + sep);
|
|
137
|
+
console.log('π― SUGGESTED SELECTORS (update browser.js SEL object):');
|
|
138
|
+
console.log(sep);
|
|
139
|
+
|
|
140
|
+
const s = report.suggestions;
|
|
141
|
+
|
|
142
|
+
if (s.suggestedInput) {
|
|
143
|
+
console.log(` chatInput : '#${s.suggestedInput}' (or use the id above)`);
|
|
144
|
+
}
|
|
145
|
+
if (s.sendBtn) {
|
|
146
|
+
const sel = s.sendBtn.ariaLabel
|
|
147
|
+
? `button[aria-label="${s.sendBtn.ariaLabel}"]`
|
|
148
|
+
: s.sendBtn.id ? `#${s.sendBtn.id}` : `.${s.sendBtn.classes?.split(' ')[0]}`;
|
|
149
|
+
console.log(` sendButton : '${sel}'`);
|
|
150
|
+
}
|
|
151
|
+
if (s.stopBtn) {
|
|
152
|
+
const sel = s.stopBtn.ariaLabel
|
|
153
|
+
? `button[aria-label="${s.stopBtn.ariaLabel}"]`
|
|
154
|
+
: `.${s.stopBtn.classes?.split(' ')[0]}`;
|
|
155
|
+
console.log(` stopButton : '${sel}'`);
|
|
156
|
+
}
|
|
157
|
+
if (s.newChatBtn) {
|
|
158
|
+
const sel = s.newChatBtn.ariaLabel
|
|
159
|
+
? `button[aria-label="${s.newChatBtn.ariaLabel}"]`
|
|
160
|
+
: `.${s.newChatBtn.classes?.split(' ')[0]}`;
|
|
161
|
+
console.log(` newChat : '${sel}'`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
console.log(sep);
|
|
165
|
+
console.log('\nπΈ Taking screenshot β /tmp/yiyan-calibrate.png');
|
|
166
|
+
await page.screenshot({ path: '/tmp/yiyan-calibrate.png', fullPage: false });
|
|
167
|
+
|
|
168
|
+
console.log('\nβ
Calibration complete! Update src/browser.js SEL object with the selectors above.');
|
|
169
|
+
console.log(' Press Ctrl+C to exit.\n');
|
|
170
|
+
|
|
171
|
+
// Keep browser open so user can inspect manually
|
|
172
|
+
await new Promise(() => {}); // wait forever
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
calibrate().catch(err => {
|
|
176
|
+
console.error('Calibration error:', err.message);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
});
|
package/src/config.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// src/config.js β Central configuration for Yiyan Agent
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
|
|
6
|
+
// βββββββββββββββββββββββββββββββββββββββββββββ
|
|
7
|
+
// Default configuration
|
|
8
|
+
// βββββββββββββββββββββββββββββββββββββββββββββ
|
|
9
|
+
const defaults = {
|
|
10
|
+
// Browser
|
|
11
|
+
YIYAN_URL : 'https://yiyan.baidu.com/',
|
|
12
|
+
SESSION_DIR : path.join(os.homedir(), '.yiyan-agent', 'session'),
|
|
13
|
+
HEADLESS : false,
|
|
14
|
+
|
|
15
|
+
// Timing
|
|
16
|
+
RESPONSE_TIMEOUT : 180_000,
|
|
17
|
+
STABLE_DELAY : 2_500,
|
|
18
|
+
SEND_DELAY : 400,
|
|
19
|
+
|
|
20
|
+
// Agent
|
|
21
|
+
MAX_ITERATIONS : 40,
|
|
22
|
+
WORKING_DIR : process.cwd(),
|
|
23
|
+
|
|
24
|
+
// Output
|
|
25
|
+
MAX_OUTPUT_LENGTH : 8_000,
|
|
26
|
+
DEBUG : false,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// βββββββββββββββββββββββββββββββββββββββββββββ
|
|
30
|
+
// Config loading priority (highest wins):
|
|
31
|
+
//
|
|
32
|
+
// 1. ~/.yiyan-agent/config.json β global user config
|
|
33
|
+
// 2. ./yiyan-agent.config.json β per-project config
|
|
34
|
+
// βββββββββββββββββββββββββββββββββββββββββββββ
|
|
35
|
+
|
|
36
|
+
function loadJson(filePath) {
|
|
37
|
+
try {
|
|
38
|
+
if (fs.existsSync(filePath)) {
|
|
39
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
console.warn('[yiyan-agent] Could not parse config file: ' + filePath);
|
|
43
|
+
}
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const globalConfigPath = path.join(os.homedir(), '.yiyan-agent', 'config.json');
|
|
48
|
+
const projectConfigPath = path.join(process.cwd(), 'yiyan-agent.config.json');
|
|
49
|
+
|
|
50
|
+
const config = {
|
|
51
|
+
...defaults,
|
|
52
|
+
...loadJson(globalConfigPath), // global overrides defaults
|
|
53
|
+
...loadJson(projectConfigPath), // project overrides global
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Remove comment keys from JSON files
|
|
57
|
+
delete config._comment;
|
|
58
|
+
|
|
59
|
+
// Resolve session dir to absolute path
|
|
60
|
+
if (!path.isAbsolute(config.SESSION_DIR)) {
|
|
61
|
+
config.SESSION_DIR = path.resolve(process.cwd(), config.SESSION_DIR);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Ensure required directories exist
|
|
65
|
+
fs.mkdirSync(config.SESSION_DIR, { recursive: true });
|
|
66
|
+
fs.mkdirSync(path.join(os.homedir(), '.yiyan-agent', 'logs'), { recursive: true });
|
|
67
|
+
|
|
68
|
+
module.exports = config;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// src/index.js β CLI entry point for Yiyan Agent
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
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');
|
|
10
|
+
|
|
11
|
+
// βββββββββββββββββββββββββββββββββββββββββββββ
|
|
12
|
+
// Parse CLI arguments
|
|
13
|
+
// βββββββββββββββββββββββββββββββββββββββββββββ
|
|
14
|
+
|
|
15
|
+
function parseArgs(argv) {
|
|
16
|
+
const args = argv.slice(2);
|
|
17
|
+
const opts = {
|
|
18
|
+
task : null,
|
|
19
|
+
interactive : false,
|
|
20
|
+
debug : false,
|
|
21
|
+
headless : false,
|
|
22
|
+
saveLog : false,
|
|
23
|
+
workingDir : null,
|
|
24
|
+
calibrate : false,
|
|
25
|
+
help : false,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let i = 0;
|
|
29
|
+
while (i < args.length) {
|
|
30
|
+
const a = args[i];
|
|
31
|
+
switch (a) {
|
|
32
|
+
case '-i':
|
|
33
|
+
case '--interactive': opts.interactive = true; break;
|
|
34
|
+
case '--debug': opts.debug = true; break;
|
|
35
|
+
case '--headless': opts.headless = true; break;
|
|
36
|
+
case '--save-log': opts.saveLog = true; break;
|
|
37
|
+
case '--calibrate': opts.calibrate = true; break;
|
|
38
|
+
case '-h':
|
|
39
|
+
case '--help': opts.help = true; break;
|
|
40
|
+
|
|
41
|
+
case '-d':
|
|
42
|
+
case '--dir':
|
|
43
|
+
opts.workingDir = args[++i];
|
|
44
|
+
break;
|
|
45
|
+
|
|
46
|
+
case '-t':
|
|
47
|
+
case '--task':
|
|
48
|
+
opts.task = args[++i];
|
|
49
|
+
break;
|
|
50
|
+
|
|
51
|
+
default:
|
|
52
|
+
// If it doesn't start with '-', treat it as an inline task
|
|
53
|
+
if (!a.startsWith('-')) {
|
|
54
|
+
opts.task = args.slice(i).join(' ');
|
|
55
|
+
i = args.length; // consume the rest
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
i++;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return opts;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// βββββββββββββββββββββββββββββββββββββββββββββ
|
|
65
|
+
// Help text
|
|
66
|
+
// βββββββββββββββββββββββββββββββββββββββββββββ
|
|
67
|
+
|
|
68
|
+
function printHelp() {
|
|
69
|
+
console.log(`
|
|
70
|
+
\x1b[1mYIYAN AGENT (ζεΏδΈθ¨)\x1b[0m β AI Coding Agent via Browser Automation
|
|
71
|
+
|
|
72
|
+
\x1b[33mUSAGE\x1b[0m
|
|
73
|
+
node src/index.js [OPTIONS] [TASK]
|
|
74
|
+
|
|
75
|
+
\x1b[33mOPTIONS\x1b[0m
|
|
76
|
+
-t, --task <task> Task to run (can also be the last argument without a flag)
|
|
77
|
+
-i, --interactive Interactive REPL mode β keep browser open, run multiple tasks
|
|
78
|
+
-d, --dir <path> Set working directory (default: current directory)
|
|
79
|
+
--debug Verbose debug output
|
|
80
|
+
--headless Run browser in headless mode (must be logged in already)
|
|
81
|
+
--save-log Save conversation log to ~/.yiyan-agent/logs/
|
|
82
|
+
--calibrate Open browser and print DOM info to help fix selectors
|
|
83
|
+
-h, --help Show this help
|
|
84
|
+
|
|
85
|
+
\x1b[33mEXAMPLES\x1b[0m
|
|
86
|
+
# Run a single task
|
|
87
|
+
node src/index.js "Create a REST API in Express with CRUD for users"
|
|
88
|
+
|
|
89
|
+
# Interactive mode (recommended)
|
|
90
|
+
node src/index.js --interactive
|
|
91
|
+
|
|
92
|
+
# Run on a specific project directory
|
|
93
|
+
node src/index.js --dir ~/projects/myapp "Add TypeScript to this project"
|
|
94
|
+
|
|
95
|
+
# Debug mode (shows raw responses)
|
|
96
|
+
node src/index.js --debug "Write a binary search in Python"
|
|
97
|
+
|
|
98
|
+
# Headless (faster, requires prior login)
|
|
99
|
+
node src/index.js --headless "Refactor index.js to use async/await"
|
|
100
|
+
|
|
101
|
+
\x1b[33mFIRST-TIME SETUP\x1b[0m
|
|
102
|
+
1. npm run setup (installs deps + Playwright browser)
|
|
103
|
+
2. node src/index.js -i (opens browser, log in to Yiyan, then use normally)
|
|
104
|
+
Session is saved β you only log in once.
|
|
105
|
+
|
|
106
|
+
\x1b[33mCONFIG FILE\x1b[0m
|
|
107
|
+
Create \x1b[36myiyan-agent.config.json\x1b[0m in your working directory to override settings:
|
|
108
|
+
{
|
|
109
|
+
"HEADLESS": true,
|
|
110
|
+
"MAX_ITERATIONS": 50,
|
|
111
|
+
"STABLE_DELAY": 3000
|
|
112
|
+
}
|
|
113
|
+
`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// βββββββββββββββββββββββββββββββββββββββββββββ
|
|
117
|
+
// Main
|
|
118
|
+
// βββββββββββββββββββββββββββββββββββββββββββββ
|
|
119
|
+
|
|
120
|
+
async function main() {
|
|
121
|
+
const opts = parseArgs(process.argv);
|
|
122
|
+
|
|
123
|
+
// ββ Help βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
124
|
+
if (opts.help) {
|
|
125
|
+
printHelp();
|
|
126
|
+
process.exit(0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ββ Apply options to config ββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
130
|
+
if (opts.debug) config.DEBUG = true;
|
|
131
|
+
if (opts.headless) config.HEADLESS = true;
|
|
132
|
+
if (opts.workingDir) {
|
|
133
|
+
const resolved = path.resolve(opts.workingDir);
|
|
134
|
+
if (!fs.existsSync(resolved)) {
|
|
135
|
+
logger.error(`Working directory not found: ${resolved}`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
config.WORKING_DIR = resolved;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ββ Banner βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
142
|
+
logger.banner();
|
|
143
|
+
logger.info(`Working directory : \x1b[36m${config.WORKING_DIR}\x1b[0m`);
|
|
144
|
+
logger.info(`Session directory : \x1b[36m${config.SESSION_DIR}\x1b[0m`);
|
|
145
|
+
logger.info(`Headless mode : \x1b[36m${config.HEADLESS}\x1b[0m`);
|
|
146
|
+
logger.info(`Debug mode : \x1b[36m${config.DEBUG}\x1b[0m`);
|
|
147
|
+
console.log('');
|
|
148
|
+
|
|
149
|
+
// ββ Create agent βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
150
|
+
const agent = new YiyanAgent({ saveLog: opts.saveLog });
|
|
151
|
+
|
|
152
|
+
// ββ Graceful shutdown handler ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
153
|
+
const shutdown = async (code = 0) => {
|
|
154
|
+
logger.info('\nShutting down...');
|
|
155
|
+
try { await agent.shutdown(); } catch {}
|
|
156
|
+
process.exit(code);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
process.on('SIGINT', () => shutdown(0));
|
|
160
|
+
process.on('SIGTERM', () => shutdown(0));
|
|
161
|
+
process.on('uncaughtException', async err => {
|
|
162
|
+
logger.error(`Uncaught error: ${err.message}`);
|
|
163
|
+
if (config.DEBUG) console.error(err.stack);
|
|
164
|
+
await shutdown(1);
|
|
165
|
+
});
|
|
166
|
+
process.on('unhandledRejection', async reason => {
|
|
167
|
+
logger.error(`Unhandled rejection: ${reason}`);
|
|
168
|
+
if (config.DEBUG) console.error(reason);
|
|
169
|
+
await shutdown(1);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ββ Calibrate mode βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
173
|
+
if (opts.calibrate) {
|
|
174
|
+
logger.header('Calibration Mode β Reading DOM selectors');
|
|
175
|
+
await agent.init();
|
|
176
|
+
await agent.browser.dumpDebugInfo();
|
|
177
|
+
await agent.browser.screenshot();
|
|
178
|
+
logger.info('Done. Check the output above to update selectors in src/browser.js if needed.');
|
|
179
|
+
await shutdown(0);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ββ Validate we have a task or interactive mode ββββββββββββββββββββββββββββ
|
|
183
|
+
if (!opts.interactive && !opts.task) {
|
|
184
|
+
logger.warn('No task provided. Switching to interactive mode...\n');
|
|
185
|
+
opts.interactive = true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ββ Launch browser βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
189
|
+
try {
|
|
190
|
+
await agent.init();
|
|
191
|
+
} catch (err) {
|
|
192
|
+
logger.error(`Failed to launch browser: ${err.message}`);
|
|
193
|
+
if (config.DEBUG) console.error(err.stack);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ββ Run ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
198
|
+
try {
|
|
199
|
+
if (opts.interactive) {
|
|
200
|
+
await agent.runInteractive();
|
|
201
|
+
} else {
|
|
202
|
+
const result = await agent.run(opts.task);
|
|
203
|
+
console.log(JSON.stringify(result, null, 2));
|
|
204
|
+
}
|
|
205
|
+
} catch (err) {
|
|
206
|
+
console.log(JSON.stringify({
|
|
207
|
+
question: opts.task || '',
|
|
208
|
+
answer: `Error: ${err.message}`,
|
|
209
|
+
duration: 0,
|
|
210
|
+
status: 'error'
|
|
211
|
+
}, null, 2));
|
|
212
|
+
await shutdown(1);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
await shutdown(0);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
main();
|
package/src/logger.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// src/logger.js β ANSI-colored terminal output (no dependencies)
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const A = {
|
|
5
|
+
reset : '\x1b[0m',
|
|
6
|
+
bold : '\x1b[1m',
|
|
7
|
+
dim : '\x1b[2m',
|
|
8
|
+
red : '\x1b[31m',
|
|
9
|
+
green : '\x1b[32m',
|
|
10
|
+
yellow : '\x1b[33m',
|
|
11
|
+
blue : '\x1b[34m',
|
|
12
|
+
magenta : '\x1b[35m',
|
|
13
|
+
cyan : '\x1b[36m',
|
|
14
|
+
white : '\x1b[37m',
|
|
15
|
+
gray : '\x1b[90m',
|
|
16
|
+
lred : '\x1b[91m',
|
|
17
|
+
lgreen : '\x1b[92m',
|
|
18
|
+
lyellow : '\x1b[93m',
|
|
19
|
+
lblue : '\x1b[94m',
|
|
20
|
+
lmagenta: '\x1b[95m',
|
|
21
|
+
lcyan : '\x1b[96m',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const c = (code, text) => `${A[code]}${text}${A.reset}`;
|
|
25
|
+
const cb = (code, text) => `${A.bold}${A[code]}${text}${A.reset}`;
|
|
26
|
+
|
|
27
|
+
// ββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
28
|
+
function truncDisplay(str, max = 400) {
|
|
29
|
+
if (!str) return '';
|
|
30
|
+
const s = String(str);
|
|
31
|
+
if (s.length <= max) return s;
|
|
32
|
+
return s.slice(0, max) + c('gray', `β¦ (+${s.length - max} chars)`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function jsonPreview(obj, max = 350) {
|
|
36
|
+
const s = JSON.stringify(obj, null, 2);
|
|
37
|
+
return truncDisplay(s, max);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ββ Public logger API βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
41
|
+
const logger = {
|
|
42
|
+
banner() {
|
|
43
|
+
console.log(`
|
|
44
|
+
${c('cyan','ββββββββββββββββββββββββββββββββββββββββββββββββββββ')}
|
|
45
|
+
${c('cyan','β')} ${cb('lcyan','π€ Yiyan Browser Agent (ζεΏδΈθ¨)')} ${c('cyan','β')}
|
|
46
|
+
${c('cyan','β')} ${c('gray','AI Coding Agent via Browser Automation')} ${c('cyan','β')}
|
|
47
|
+
${c('cyan','β')} ${c('gray','No API key needed β uses yiyan.baidu.com')} ${c('cyan','β')}
|
|
48
|
+
${c('cyan','ββββββββββββββββββββββββββββββββββββββββββββββββββββ')}
|
|
49
|
+
`);
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
header(msg) {
|
|
53
|
+
const line = 'β'.repeat(50);
|
|
54
|
+
console.log(`\n${c('blue', line)}`);
|
|
55
|
+
console.log(`${c('bold','π ')}${cb('white', msg)}`);
|
|
56
|
+
console.log(`${c('blue', line)}\n`);
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
info(msg) { console.log(`${c('lblue',' βΉ ')} ${msg}`); },
|
|
60
|
+
success(msg) { console.log(`${c('lgreen',' β ')} ${c('lgreen', msg)}`); },
|
|
61
|
+
warn(msg) { console.log(`${c('lyellow',' β ')} ${c('lyellow', msg)}`); },
|
|
62
|
+
error(msg) { console.log(`${c('lred',' β ')} ${c('lred', msg)}`); },
|
|
63
|
+
dim(msg) { console.log(`${A.dim} ${msg}${A.reset}`); },
|
|
64
|
+
|
|
65
|
+
/** Spinner-style line (overwrites itself with \r) */
|
|
66
|
+
thinking(msg) {
|
|
67
|
+
process.stdout.write(` ${c('cyan','β³')} ${c('gray', msg)}\r`);
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
/** Clear the current line */
|
|
71
|
+
clearLine() {
|
|
72
|
+
process.stdout.write(`\r${' '.repeat(80)}\r`);
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
// ββ Tool call display βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
76
|
+
toolCall(name, args) {
|
|
77
|
+
console.log(`\n ${cb('magenta','β‘ TOOL CALL')} ${c('cyan', `β ${name}`)}`);
|
|
78
|
+
const preview = jsonPreview(args);
|
|
79
|
+
if (preview.trim()) {
|
|
80
|
+
preview.split('\n').forEach(l => console.log(` ${c('gray', l)}`));
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
toolResult(result, isError = false) {
|
|
85
|
+
const icon = isError ? c('lred',' β Result:') : c('lgreen',' β Result:');
|
|
86
|
+
const text = truncDisplay(String(result), 300);
|
|
87
|
+
const color = isError ? 'lred' : 'gray';
|
|
88
|
+
console.log(`${icon}`);
|
|
89
|
+
text.split('\n').slice(0, 12).forEach(l => console.log(` ${c(color, l)}`));
|
|
90
|
+
if (String(result).split('\n').length > 12) {
|
|
91
|
+
console.log(` ${c('gray',' β¦ (truncated for display)')}`);
|
|
92
|
+
}
|
|
93
|
+
console.log('');
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
// ββ Final agent output ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
97
|
+
finalOutput(msg) {
|
|
98
|
+
const line = 'β'.repeat(50);
|
|
99
|
+
console.log(`\n${c('lgreen', line)}`);
|
|
100
|
+
console.log(`${cb('lgreen','β
TASK COMPLETE')}`);
|
|
101
|
+
console.log(`${c('lgreen', line)}\n`);
|
|
102
|
+
console.log(msg);
|
|
103
|
+
console.log('');
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
// ββ Section separator βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
107
|
+
separator(label = '') {
|
|
108
|
+
const pad = label ? ` ${label} ` : '';
|
|
109
|
+
console.log(`\n${c('gray', 'Β·'.repeat(20) + pad + 'Β·'.repeat(20))}\n`);
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
// ββ Iteration marker ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
113
|
+
iteration(n, max) {
|
|
114
|
+
console.log(`\n${c('gray',' β')} ${c('dim',`Step ${n}/${max}`)} ${c('gray','β')}`);
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
module.exports = logger;
|