@omnivoice/page-agent 0.0.0-stage → 0.2.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/LICENSE +22 -0
- package/README.md +249 -2
- package/dist/esm/PageAgent.d.ts +160 -0
- package/dist/esm/page-agent.js +426 -0
- package/dist/esm/page-agent.js.map +1 -0
- package/dist/iife/page-agent.demo.js +402 -0
- package/package.json +63 -5
- package/server/agent-gateway.mjs +215 -0
package/package.json
CHANGED
|
@@ -1,6 +1,64 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@omnivoice/page-agent",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/esm/page-agent.js",
|
|
7
|
+
"types": "./dist/esm/PageAgent.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/esm/PageAgent.d.ts",
|
|
11
|
+
"import": "./dist/esm/page-agent.js",
|
|
12
|
+
"default": "./dist/esm/page-agent.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist/",
|
|
17
|
+
"server/"
|
|
18
|
+
],
|
|
19
|
+
"description": "GUI agent for web applications - add intelligent automation to any webpage with a single script",
|
|
20
|
+
"keywords": [
|
|
21
|
+
"ai",
|
|
22
|
+
"automation",
|
|
23
|
+
"ui-agent",
|
|
24
|
+
"GUI-agent",
|
|
25
|
+
"browser-automation",
|
|
26
|
+
"web-agent",
|
|
27
|
+
"llm",
|
|
28
|
+
"dom-interaction",
|
|
29
|
+
"web-automation",
|
|
30
|
+
"GUI-simulation"
|
|
31
|
+
],
|
|
32
|
+
"author": "Simon<gaomeng1900>",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"homepage": "https://www.npmjs.com/package/@omnivoice/page-agent",
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "vite build && npm run build:demo",
|
|
37
|
+
"build:demo": "vite build --config vite.iife.config.js",
|
|
38
|
+
"dev:demo": "concurrently \"vite build --config vite.iife.config.js --watch\" \"npx serve dist/iife -p 5174\"",
|
|
39
|
+
"prepublishOnly": "node ../../scripts/pre-publish.js",
|
|
40
|
+
"postpublish": "node ../../scripts/post-publish.js"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@omnivoice/core": "0.2.0",
|
|
44
|
+
"@omnivoice/llms": "0.2.0",
|
|
45
|
+
"@omnivoice/page-controller": "0.2.0",
|
|
46
|
+
"@omnivoice/ui": "0.2.0",
|
|
47
|
+
"chalk": "^6.0.0"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"zod": "^3.25.0 || ^4.0.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"zod": "^4.5.1"
|
|
54
|
+
},
|
|
55
|
+
"repository": {
|
|
56
|
+
"type": "git",
|
|
57
|
+
"url": "git+https://github.com/SorPuti/page-agent-fork.git",
|
|
58
|
+
"directory": "packages/page-agent"
|
|
59
|
+
},
|
|
60
|
+
"bin": {
|
|
61
|
+
"omnivoice-agent-gateway": "server/agent-gateway.mjs"
|
|
62
|
+
},
|
|
63
|
+
"module": "./dist/esm/page-agent.js"
|
|
64
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* FORK: gateway do agente — a casa da chave de API. O front-end NUNCA vê a chave: ele fala com
|
|
4
|
+
* este gateway (mesma origem do site do projeto) e o gateway injeta a chave ao chamar o modelo.
|
|
5
|
+
*
|
|
6
|
+
* Por que: colocar `apiKey` no HTML/JS expõe a chave a qualquer um que abrir o devtools. O
|
|
7
|
+
* padrão correto é: segredo só no servidor. Cada projeto roda o SEU gateway com a SUA config
|
|
8
|
+
* (modelo, URL do provedor, chave, voz), então o controle fica com o projeto, não com a página.
|
|
9
|
+
*
|
|
10
|
+
* Config (prioridade: env > arquivo JSON):
|
|
11
|
+
* AGENT_BASE_URL Endpoint OpenAI-compatible upstream. ex.: https://api.openai.com/v1
|
|
12
|
+
* AGENT_API_KEY Chave do provedor (SÓ aqui).
|
|
13
|
+
* AGENT_MODEL Modelo a usar. ex.: gpt-4o-mini
|
|
14
|
+
* AGENT_LANGUAGE pt-BR | en-US | zh-CN (default pt-BR)
|
|
15
|
+
* AGENT_MAX_STEPS Máx. de passos por tarefa (default 30)
|
|
16
|
+
* AGENT_VOICE_INPUT true/false — permite ditado no widget (default true)
|
|
17
|
+
* AGENT_VOICE_OUTPUT true/false — agente fala o resultado (default false)
|
|
18
|
+
* AGENT_VOICE_LANG Idioma da voz (default = AGENT_LANGUAGE)
|
|
19
|
+
* AGENT_PORT Porta do gateway (default 8790)
|
|
20
|
+
* AGENT_HOST Bind (default 127.0.0.1 — local; use 0.0.0.0 só atrás de proxy seguro)
|
|
21
|
+
* AGENT_CONFIG Caminho p/ um agent.config.json com os mesmos campos (case-insensitive).
|
|
22
|
+
*
|
|
23
|
+
* Endpoints:
|
|
24
|
+
* GET /config → config PÚBLICA (sem chave) p/ o widget montar.
|
|
25
|
+
* POST /v1/chat/completions → proxy p/ ${AGENT_BASE_URL}/chat/completions, injetando a chave.
|
|
26
|
+
* GET /health → { ok, configured, model }
|
|
27
|
+
*
|
|
28
|
+
* Uso:
|
|
29
|
+
* AGENT_API_KEY=sk-… AGENT_BASE_URL=https://api.openai.com/v1 AGENT_MODEL=gpt-4o-mini \
|
|
30
|
+
* node agent-gateway.mjs
|
|
31
|
+
* Ou: npm i @omnivoice/page-agent && npx omnivoice-agent-gateway (bin do pacote)
|
|
32
|
+
*
|
|
33
|
+
* Zero dependências (usa fetch global do Node 18+).
|
|
34
|
+
*/
|
|
35
|
+
import { createServer } from 'node:http'
|
|
36
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
37
|
+
import { resolve } from 'node:path'
|
|
38
|
+
|
|
39
|
+
// ---- Config: env > arquivo JSON > default ----
|
|
40
|
+
function loadFileConfig() {
|
|
41
|
+
const p = process.env.AGENT_CONFIG
|
|
42
|
+
if (!p) return {}
|
|
43
|
+
const abs = resolve(process.cwd(), p)
|
|
44
|
+
if (!existsSync(abs)) {
|
|
45
|
+
console.warn(`[gateway] AGENT_CONFIG aponta p/ arquivo inexistente: ${abs}`)
|
|
46
|
+
return {}
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(readFileSync(abs, 'utf8'))
|
|
50
|
+
} catch (e) {
|
|
51
|
+
console.warn(`[gateway] AGENT_CONFIG inválido (${abs}): ${e.message}`)
|
|
52
|
+
return {}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function envBool(v, def) {
|
|
57
|
+
if (v === undefined || v === '') return def
|
|
58
|
+
return ['1', 'true', 'yes', 'on'].includes(String(v).toLowerCase())
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const fileCfg = loadFileConfig()
|
|
62
|
+
const pick = (envKey, fileKey, def) => {
|
|
63
|
+
const fromEnv = process.env[envKey]
|
|
64
|
+
if (fromEnv !== undefined && fromEnv !== '') return fromEnv
|
|
65
|
+
const fromFile = fileCfg[fileKey]
|
|
66
|
+
if (fromFile !== undefined && fromFile !== null && fromFile !== '') return fromFile
|
|
67
|
+
return def
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const config = {
|
|
71
|
+
baseURL: String(pick('AGENT_BASE_URL', 'baseURL', '')).replace(/\/+$/, ''),
|
|
72
|
+
apiKey: String(pick('AGENT_API_KEY', 'apiKey', '')),
|
|
73
|
+
model: String(pick('AGENT_MODEL', 'model', 'gpt-4o-mini')),
|
|
74
|
+
language: String(pick('AGENT_LANGUAGE', 'language', 'pt-BR')),
|
|
75
|
+
maxSteps: Number(pick('AGENT_MAX_STEPS', 'maxSteps', 30)) || 30,
|
|
76
|
+
voice: {
|
|
77
|
+
input: envBool(pick('AGENT_VOICE_INPUT', 'voiceInput', 'true'), true),
|
|
78
|
+
output: envBool(pick('AGENT_VOICE_OUTPUT', 'voiceOutput', 'false'), false),
|
|
79
|
+
lang: String(pick('AGENT_VOICE_LANG', 'voiceLang', '')) || undefined,
|
|
80
|
+
},
|
|
81
|
+
promptForNextTask: envBool(pick('AGENT_PROMPT_NEXT', 'promptForNextTask', 'true'), true),
|
|
82
|
+
port: Number(pick('AGENT_PORT', 'port', 8790)) || 8790,
|
|
83
|
+
host: String(pick('AGENT_HOST', 'host', '127.0.0.1')),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---- Helpers ----
|
|
87
|
+
function sendJson(res, status, obj) {
|
|
88
|
+
const body = JSON.stringify(obj)
|
|
89
|
+
res.writeHead(status, {
|
|
90
|
+
'content-type': 'application/json; charset=utf-8',
|
|
91
|
+
'content-length': Buffer.byteLength(body),
|
|
92
|
+
// Sem cache da config: o projeto pode trocar modelo/voz e querer efeito imediato.
|
|
93
|
+
'cache-control': 'no-store',
|
|
94
|
+
})
|
|
95
|
+
res.end(body)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Config pública (o que pode ir pro front). A chave NUNCA sai daqui.
|
|
99
|
+
function publicConfig() {
|
|
100
|
+
return {
|
|
101
|
+
model: config.model,
|
|
102
|
+
language: config.language,
|
|
103
|
+
maxSteps: config.maxSteps,
|
|
104
|
+
promptForNextTask: config.promptForNextTask,
|
|
105
|
+
voice: {
|
|
106
|
+
input: config.voice.input,
|
|
107
|
+
output: config.voice.output,
|
|
108
|
+
...(config.voice.lang && { lang: config.voice.lang }),
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---- Proxy de chat/completions ----
|
|
114
|
+
async function proxyCompletions(req, res) {
|
|
115
|
+
if (!config.baseURL) {
|
|
116
|
+
return sendJson(res, 503, { error: 'AGENT_BASE_URL não configurado no gateway' })
|
|
117
|
+
}
|
|
118
|
+
if (!config.apiKey) {
|
|
119
|
+
return sendJson(res, 503, { error: 'AGENT_API_KEY não configurada no gateway' })
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let raw
|
|
123
|
+
try {
|
|
124
|
+
raw = await readBody(req)
|
|
125
|
+
} catch (e) {
|
|
126
|
+
return sendJson(res, 400, { error: `Corpo da request inválido: ${e.message}` })
|
|
127
|
+
}
|
|
128
|
+
let body
|
|
129
|
+
try {
|
|
130
|
+
body = raw ? JSON.parse(raw) : {}
|
|
131
|
+
} catch {
|
|
132
|
+
return sendJson(res, 400, { error: 'Corpo não é JSON válido' })
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// O gateway é a autoridade do modelo: força o modelo configurado (o front não escolhe).
|
|
136
|
+
body.model = config.model
|
|
137
|
+
|
|
138
|
+
// Cabeçalhos: injeta a chave do servidor e DESCARTA qualquer Authorization do client,
|
|
139
|
+
// para que uma página comprometida não consiga usar outra chave/conta.
|
|
140
|
+
const headers = { 'content-type': 'application/json' }
|
|
141
|
+
if (config.apiKey) headers.authorization = `Bearer ${config.apiKey}`
|
|
142
|
+
|
|
143
|
+
let upstream
|
|
144
|
+
try {
|
|
145
|
+
upstream = await fetch(`${config.baseURL}/chat/completions`, {
|
|
146
|
+
method: 'POST',
|
|
147
|
+
headers,
|
|
148
|
+
body: JSON.stringify(body),
|
|
149
|
+
})
|
|
150
|
+
} catch (e) {
|
|
151
|
+
console.error('[gateway] falha ao chamar upstream:', e.message)
|
|
152
|
+
return sendJson(res, 502, { error: `Falha ao contatar o provedor: ${e.message}` })
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Encaminha o corpo (stream ou JSON) e o status, sem vazar headers do provedor.
|
|
156
|
+
const text = await upstream.text()
|
|
157
|
+
res.writeHead(upstream.status, {
|
|
158
|
+
'content-type': upstream.headers.get('content-type') ?? 'application/json; charset=utf-8',
|
|
159
|
+
'cache-control': 'no-store',
|
|
160
|
+
})
|
|
161
|
+
res.end(text)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Lê o corpo da request com limite (evita estouro de memória com payload gigante).
|
|
165
|
+
const MAX_BODY = 2 * 1024 * 1024
|
|
166
|
+
function readBody(req) {
|
|
167
|
+
return new Promise((resolveP, rejectP) => {
|
|
168
|
+
let size = 0
|
|
169
|
+
const chunks = []
|
|
170
|
+
req.on('data', c => {
|
|
171
|
+
size += c.length
|
|
172
|
+
if (size > MAX_BODY) {
|
|
173
|
+
rejectP(new Error('payload muito grande'))
|
|
174
|
+
req.destroy()
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
chunks.push(c)
|
|
178
|
+
})
|
|
179
|
+
req.on('end', () => resolveP(Buffer.concat(chunks).toString('utf8')))
|
|
180
|
+
req.on('error', rejectP)
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---- Servidor ----
|
|
185
|
+
const server = createServer(async (req, res) => {
|
|
186
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`)
|
|
187
|
+
const path = url.pathname
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
if (req.method === 'GET' && path === '/config') {
|
|
191
|
+
return sendJson(res, 200, publicConfig())
|
|
192
|
+
}
|
|
193
|
+
if (req.method === 'GET' && path === '/health') {
|
|
194
|
+
return sendJson(res, 200, {
|
|
195
|
+
ok: true,
|
|
196
|
+
configured: Boolean(config.apiKey && config.baseURL),
|
|
197
|
+
model: config.model,
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
if (req.method === 'POST' && path === '/v1/chat/completions') {
|
|
201
|
+
return await proxyCompletions(req, res)
|
|
202
|
+
}
|
|
203
|
+
return sendJson(res, 404, { error: `Rota não encontrada: ${req.method} ${path}` })
|
|
204
|
+
} catch (e) {
|
|
205
|
+
console.error('[gateway] erro não tratado:', e)
|
|
206
|
+
if (!res.headersSent) sendJson(res, 500, { error: 'Erro interno do gateway' })
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
server.listen(config.port, config.host, () => {
|
|
211
|
+
console.log(`[gateway] agente ouvindo em http://${config.host}:${config.port}`)
|
|
212
|
+
console.log(`[gateway] model=${config.model} language=${config.language} voice=${JSON.stringify(config.voice)}`)
|
|
213
|
+
console.log(`[gateway] chave configurada: ${config.apiKey ? 'sim (mantida no servidor)' : 'NÃO — defina AGENT_API_KEY'}`)
|
|
214
|
+
console.log(`[gateway] upstream: ${config.baseURL || '(não definido)'}`)
|
|
215
|
+
})
|