ps-claw 1.0.9 → 1.1.1

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/web-ui/server.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * PS Claw — Interface Web
5
- * Servidor leve que serve a UI e faz proxy para o gateway do PS Claw
6
- * Suporta multiplos gateways, API de modelos e configuracoes
4
+ * PS Claw — Servidor Web
5
+ * Proxy reverso para APIs de IA (Anthropic, OpenAI, Google, etc.)
6
+ * Evita CORS bloqueando chamadas diretas do navegador
7
7
  */
8
8
 
9
9
  import http from "node:http";
@@ -13,180 +13,83 @@ import path from "node:path";
13
13
  import { fileURLToPath } from "node:url";
14
14
 
15
15
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
-
17
16
  const WEB_PORT = process.env.PS_CLAW_WEB_PORT || 3000;
18
- const GATEWAY_PORT = process.env.PS_CLAW_GATEWAY_PORT || 18789;
19
- const GATEWAY_TOKEN = process.env.OPENCLAW_GATEWAY_TOKEN || "";
20
17
 
21
- function serveFile(res, filePath, contentType) {
22
- try {
23
- const content = fs.readFileSync(filePath);
24
- res.writeHead(200, { "Content-Type": contentType });
25
- res.end(content);
26
- } catch {
27
- res.writeHead(404);
28
- res.end("Not found");
29
- }
30
- }
18
+ // Proxy genérico para APIs externas
19
+ function proxyRequest(targetUrl, method, headers, body, res) {
20
+ const url = new URL(targetUrl);
21
+ const isHttps = url.protocol === "https:";
22
+ const lib = isHttps ? https : http;
31
23
 
32
- function proxyToGateway(req, res, bodyData, targetHost, targetPort, targetPath, authToken) {
33
24
  const options = {
34
- hostname: targetHost,
35
- port: targetPort,
36
- path: targetPath,
37
- method: req.method,
38
- headers: {
39
- ...req.headers,
40
- host: `${targetHost}:${targetPort}`,
41
- ...(authToken && { authorization: `Bearer ${authToken}` }),
42
- },
25
+ hostname: url.hostname,
26
+ port: url.port || (isHttps ? 443 : 80),
27
+ path: url.pathname + url.search,
28
+ method,
29
+ headers: { ...headers, host: url.hostname },
43
30
  };
44
31
 
45
- const proxyReq = http.request(options, (proxyRes) => {
46
- res.writeHead(proxyRes.statusCode, proxyRes.headers);
32
+ const req = lib.request(options, (proxyRes) => {
33
+ res.writeHead(proxyRes.statusCode, {
34
+ ...proxyRes.headers,
35
+ "access-control-allow-origin": "*",
36
+ });
47
37
  proxyRes.pipe(res);
48
38
  });
49
39
 
50
- proxyReq.on("error", () => {
40
+ req.on("error", (e) => {
51
41
  res.writeHead(502, { "Content-Type": "application/json" });
52
- res.end(JSON.stringify({ error: "Gateway do PS Claw nao esta rodando. Execute: ps-claw gateway run" }));
42
+ res.end(JSON.stringify({ error: e.message }));
53
43
  });
54
44
 
55
- if (bodyData) proxyReq.write(bodyData);
56
- proxyReq.end();
57
- }
58
-
59
- function parseUrl(urlStr) {
60
- try {
61
- const parsed = new URL(urlStr);
62
- return {
63
- protocol: parsed.protocol,
64
- hostname: parsed.hostname,
65
- port: parsed.port || (parsed.protocol === "https:" ? "443" : "80"),
66
- path: parsed.pathname + parsed.search,
67
- };
68
- } catch {
69
- return null;
70
- }
45
+ if (body) req.write(body);
46
+ req.end();
71
47
  }
72
48
 
73
49
  const server = http.createServer((req, res) => {
74
- // CORS
75
50
  res.setHeader("Access-Control-Allow-Origin", "*");
76
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
77
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
78
- if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; }
79
-
80
- // Proxy para o gateway local (padrao)
81
- if (req.url.startsWith("/gateway")) {
82
- let body = "";
83
- req.on("data", chunk => body += chunk);
84
- req.on("end", () => proxyToGateway(
85
- req, res, body || null,
86
- "127.0.0.1", GATEWAY_PORT,
87
- req.url.replace("/gateway", ""),
88
- GATEWAY_TOKEN
89
- ));
90
- return;
91
- }
51
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
52
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, x-api-key, anthropic-version");
92
53
 
93
- // API: Health check do gateway local
94
- if (req.url === "/api/health" && req.method === "GET") {
95
- const healthReq = http.request({
96
- hostname: "127.0.0.1",
97
- port: GATEWAY_PORT,
98
- path: "/health",
99
- method: "GET",
100
- ...(GATEWAY_TOKEN && { headers: { authorization: `Bearer ${GATEWAY_TOKEN}` } }),
101
- }, (healthRes) => {
102
- let data = "";
103
- healthRes.on("data", chunk => data += chunk);
104
- healthRes.on("end", () => {
105
- res.writeHead(healthRes.statusCode, { "Content-Type": "application/json" });
106
- res.end(data);
107
- });
108
- });
109
- healthReq.on("error", () => {
110
- res.writeHead(502, { "Content-Type": "application/json" });
111
- res.end(JSON.stringify({ status: "offline", error: "Gateway offline" }));
112
- });
113
- healthReq.end();
114
- return;
115
- }
54
+ if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; }
116
55
 
117
- // API: Listar modelos disponiveis
118
- if (req.url === "/api/models" && req.method === "GET") {
119
- const models = [
120
- { id: "gpt-4o", name: "GPT-4o", provider: "openai" },
121
- { id: "gpt-4o-mini", name: "GPT-4o Mini", provider: "openai" },
122
- { id: "gpt-4-turbo", name: "GPT-4 Turbo", provider: "openai" },
123
- { id: "claude-opus-4-5", name: "Claude Opus 4.5", provider: "anthropic" },
124
- { id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5", provider: "anthropic" },
125
- { id: "claude-3-5-sonnet", name: "Claude 3.5 Sonnet", provider: "anthropic" },
126
- { id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", provider: "google" },
127
- { id: "gemini-2.0-flash", name: "Gemini 2.0 Flash", provider: "google" },
128
- { id: "deepseek-chat", name: "DeepSeek Chat", provider: "deepseek" },
129
- ];
130
- res.writeHead(200, { "Content-Type": "application/json" });
131
- res.end(JSON.stringify({ models }));
132
- return;
133
- }
56
+ // Proxy para APIs externas: /proxy?url=https://api.anthropic.com/...
57
+ if (req.url.startsWith("/proxy")) {
58
+ const params = new URL(req.url, `http://localhost`).searchParams;
59
+ const target = params.get("url");
60
+ if (!target) { res.writeHead(400); res.end("Missing url param"); return; }
134
61
 
135
- // API: Proxy para gateway remoto
136
- if (req.url.startsWith("/api/proxy") && req.method === "POST") {
137
62
  let body = "";
138
- req.on("data", chunk => body += chunk);
139
- req.on("end", () => {
140
- try {
141
- const { url: targetUrl, path: targetPath, token, method, body: reqBody } = JSON.parse(body);
142
- const parsed = parseUrl(targetUrl);
143
- if (!parsed) {
144
- res.writeHead(400, { "Content-Type": "application/json" });
145
- res.end(JSON.stringify({ error: "URL invalida" }));
146
- return;
147
- }
148
- proxyToGateway(
149
- { method: method || "GET", headers: {} },
150
- res, reqBody ? JSON.stringify(reqBody) : null,
151
- parsed.hostname, parsed.port,
152
- targetPath || parsed.path,
153
- token
154
- );
155
- } catch {
156
- res.writeHead(400, { "Content-Type": "application/json" });
157
- res.end(JSON.stringify({ error: "Requisicao invalida" }));
158
- }
159
- });
63
+ req.on("data", c => body += c);
64
+ req.on("end", () => proxyRequest(target, req.method, req.headers, body, res));
160
65
  return;
161
66
  }
162
67
 
163
- // Servir arquivos estaticos
164
- const url = req.url === "/" ? "/index.html" : req.url;
165
- const filePath = path.join(__dirname, "public", url);
166
- const ext = path.extname(filePath);
167
- const mimeTypes = {
68
+ // Servir arquivos estáticos
69
+ const filePath = req.url === "/" ? "/index.html" : req.url;
70
+ const fullPath = path.join(__dirname, "public", filePath);
71
+ const ext = path.extname(fullPath);
72
+ const mime = {
168
73
  ".html": "text/html; charset=utf-8",
169
74
  ".js": "application/javascript",
170
- ".mjs": "application/javascript",
171
75
  ".css": "text/css",
172
- ".png": "image/png",
173
- ".svg": "image/svg+xml",
174
- ".ico": "image/x-icon",
175
76
  ".json": "application/json",
176
- ".woff": "font/woff",
177
- ".woff2": "font/woff2",
178
77
  };
179
78
 
180
- serveFile(res, filePath, mimeTypes[ext] || "text/plain");
79
+ try {
80
+ const content = fs.readFileSync(fullPath);
81
+ res.writeHead(200, { "Content-Type": mime[ext] || "text/plain" });
82
+ res.end(content);
83
+ } catch {
84
+ res.writeHead(404); res.end("Not found");
85
+ }
181
86
  });
182
87
 
183
88
  server.listen(WEB_PORT, () => {
184
89
  console.log("");
185
90
  console.log(" 🦞 PS Claw — Interface Web");
186
- console.log(" ─────────────────────────────");
91
+ console.log(" ────────────────────────────────");
187
92
  console.log(` ✅ Rodando em: http://localhost:${WEB_PORT}`);
188
- console.log(` 🔗 Gateway: http://localhost:${GATEWAY_PORT}`);
189
- console.log("");
190
- console.log(" Abra o navegador em: http://localhost:" + WEB_PORT);
93
+ console.log(" 📖 Abra o navegador e configure sua API key");
191
94
  console.log("");
192
95
  });