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/package.json +5 -2
- package/ps-claw.cmd +2 -0
- package/ps-claw.sh +2 -0
- package/web-ui/public/index.html +935 -1072
- package/web-ui/server.mjs +46 -143
package/web-ui/server.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* PS Claw —
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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:
|
|
35
|
-
port:
|
|
36
|
-
path:
|
|
37
|
-
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
|
|
46
|
-
res.writeHead(proxyRes.statusCode,
|
|
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
|
-
|
|
40
|
+
req.on("error", (e) => {
|
|
51
41
|
res.writeHead(502, { "Content-Type": "application/json" });
|
|
52
|
-
res.end(JSON.stringify({ error:
|
|
42
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
53
43
|
});
|
|
54
44
|
|
|
55
|
-
if (
|
|
56
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
//
|
|
118
|
-
if (req.url
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
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",
|
|
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
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
const ext = path.extname(
|
|
167
|
-
const
|
|
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
|
-
|
|
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(
|
|
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
|
});
|