Files
browser-gateway/pages.js
T

350 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// HTML home page and JSON API docs for the browser-mcp gateway.
// All examples are built from the visitor's Host header so IP/port always match.
export function renderHome(baseUrl, token) {
// B is a placeholder element filled by client-side JS with the effective base
// URL (visitor's host by default, overridable in the settings bar).
const B = '<b class="bp"></b>';
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>browser-mcp gateway</title>
<style>
body { font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; width: 96%; max-width: 1400px; margin: 1.5em auto; padding: 0 1em; color: #24292f; line-height: 1.6; box-sizing: border-box; }
h1 { border-bottom: 2px solid #d0d7de; padding-bottom: .3em; }
h2 { margin-top: 1.8em; color: #0969da; }
pre { background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 12px; overflow-x: auto; font-size: 13px; }
code { background: #f6f8fa; padding: 2px 5px; border-radius: 4px; font-size: 13px; }
pre code { padding: 0; background: none; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #d0d7de; padding: 6px 10px; text-align: left; font-size: 14px; }
th { background: #f6f8fa; }
.warn { background: #fff8c5; border: 1px solid #d4a72c; border-radius: 6px; padding: 10px 14px; }
.bp { font-weight: inherit; }
.settings { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; background: #ddf4e4; border: 1px solid #1a7f37; border-radius: 8px; padding: 12px 16px; margin: 1em 0; }
.settings label { font-weight: 600; }
.settings input { flex: 1; min-width: 240px; padding: 6px 10px; border: 1px solid #d0d7de; border-radius: 6px; font-family: ui-monospace, Consolas, monospace; font-size: 13px; }
.settings input:focus { outline: 2px solid #0969da; outline-offset: -1px; }
.settings button { padding: 6px 16px; border-radius: 6px; border: 1px solid #1a7f37; background: #1a7f37; color: #fff; cursor: pointer; font-size: 13px; }
.settings button:hover { background: #15652e; }
.settings button.ghost { background: #fff; color: #24292f; border-color: #d0d7de; }
.settings button.ghost:hover { background: #f6f8fa; }
.settings .msg { color: #1a7f37; font-size: 13px; min-height: 1em; }
</style>
</head>
<body>
<h1>browser-mcp gateway</h1>
<div class="settings">
<label for="hostInput">示例地址</label>
<input id="hostInput" type="text" spellcheck="false" placeholder="host:port,例如 10.1.1.6:9109">
<button id="applyBtn">应用</button>
<button id="resetBtn" class="ghost">重置</button>
<span class="msg" id="barMsg"></span>
</div>
<p>本网关把本机 Chrome 浏览器(通过 browser-mcp 扩展)暴露到局域网。页面所有示例基于 <code>${B}</code> 生成(默认为你当前的访问地址,可在上方设置栏修改)。</p>
<h2>1. 认证</h2>
<p>所有接口都需要认证,二选一:</p>
<ul>
<li>HTTP 头:<code>Authorization: Bearer ${token}</code></li>
<li>URL 参数:<code>?token=${token}</code>(方便浏览器直接打开,例如 <a href="/docs?token=${token}">API 文档</a>)</li>
</ul>
<h2>2. 查看 API 文档</h2>
<p>HTML 文档:<a href="/docs?token=${token}"><code>${B}/docs</code></a> | JSON 版本:<a href="/docs.json?token=${token}"><code>${B}/docs.json</code></a></p>
<h2>3. REST API 使用(/api)</h2>
<table>
<tr><th>方法</th><th>路径</th><th>说明</th></tr>
<tr><td>GET</td><td>/api/health</td><td>存活与上游状态</td></tr>
<tr><td>GET</td><td>/api/tools</td><td>列出全部浏览器工具(40 个)</td></tr>
<tr><td>POST</td><td>/api/call</td><td>调用工具,body 为 <code>{"name": "工具名", "arguments": {...}}</code></td></tr>
</table>
<p>示例:打开网页并读取内容</p>
<pre><code>curl -s -X POST ${B}/api/call \\
-H "Authorization: Bearer ${token}" \\
-H "Content-Type: application/json" \\
-d '{"name":"browser_navigate","arguments":{"url":"https://example.com"}}'
curl -s -X POST ${B}/api/call \\
-H "Authorization: Bearer ${token}" \\
-H "Content-Type: application/json" \\
-d '{"name":"browser_get_page_content","arguments":{"format":"text"}}'</code></pre>
<p>返回格式:MCP 标准结果 <code>{"content":[{"type":"text","text":"..."}]}</code>,错误时 HTTP 状态码非 200 且 body 为 <code>{"error":"..."}</code>。</p>
<h2>4. 添加 MCP(/mcp 端点)</h2>
<p>MCP 端点地址:<code>${B}/mcp</code>(Streamable HTTP 传输,无状态模式)。</p>
<h3>Kimi Code</h3>
<p>编辑 <code>~/.kimi-code/mcp.json</code>,在 <code>mcpServers</code> 中加入:</p>
<pre><code>"browser-remote": {
"url": "${B}/mcp",
"headers": { "Authorization": "Bearer ${token}" }
}</code></pre>
<p>保存后<strong>新建会话</strong>生效(已打开的会话不会加载新增的 server)。也可以在 TUI 里用 <code>/mcp-config</code> 交互式添加。</p>
<h3>Claude Code</h3>
<pre><code>claude mcp add --transport http browser-remote ${B}/mcp \\
--header "Authorization: Bearer ${token}"</code></pre>
<h2>5. 一句话部署</h2>
<p>Linux / macOS(写入 Kimi Code 用户级配置):</p>
<pre><code>node -e "const f=process.env.HOME+'/.kimi-code/mcp.json',j=require(f);j.mcpServers['browser-remote']={url:'${B}/mcp',headers:{Authorization:'Bearer ${token}'}};require('fs').writeFileSync(f,JSON.stringify(j,null,2))"</code></pre>
<p>Windows PowerShell:</p>
<pre><code>$f="$env:USERPROFILE\\.kimi-code\\mcp.json"; $j=Get-Content $f -Raw | ConvertFrom-Json; $j.mcpServers | Add-Member -NotePropertyName browser-remote -NotePropertyValue @{url='${B}/mcp';headers=@{Authorization='Bearer ${token}'}} -Force; $j | ConvertTo-Json -Depth 10 | Set-Content $f -Encoding UTF8</code></pre>
<div class="warn">
<strong>注意:</strong>远程调用驱动的是网关所在机器的 Chrome,请保持该机器的 browser-mcp 扩展处于连接状态。Token 即全部权限,请勿泄露到不可信网络。
</div>
<script>
(function () {
var KEY = 'gw-custom-host';
var input = document.getElementById('hostInput');
var msg = document.getElementById('barMsg');
function curHost() { return localStorage.getItem(KEY) || location.host; }
function render() {
var base = 'http://' + curHost();
var els = document.querySelectorAll('.bp');
for (var i = 0; i < els.length; i++) els[i].textContent = base;
}
function flash(text) {
msg.textContent = text;
setTimeout(function () { msg.textContent = ''; }, 3000);
}
document.getElementById('applyBtn').onclick = function () {
var v = input.value.trim().replace(/^https?:\\/\\//, '').replace(/\\/+$/, '');
if (!v) { flash('请输入 host:port'); return; }
localStorage.setItem(KEY, v);
input.value = v;
render();
flash('已应用:' + v);
};
document.getElementById('resetBtn').onclick = function () {
localStorage.removeItem(KEY);
input.value = location.host;
render();
flash('已重置为当前访问地址');
};
input.value = curHost();
render();
})();
</script>
</body>
</html>`;
}
export function renderDocs(baseUrl, token) {
const auth = { type: 'bearer', header: 'Authorization: Bearer <token>', alt: 'query param ?token=<token>' };
return {
name: 'browser-mcp-gateway',
version: '1.0.0',
description: 'LAN gateway exposing the local browser-mcp (Agent360) browser over MCP Streamable HTTP and a simple REST API.',
baseUrl,
auth,
endpoints: [
{
method: 'GET', path: '/',
description: 'HTML 使用说明页(认证、REST API 用法、MCP 接入、一句话部署)。',
response: 'text/html',
},
{
method: 'GET', path: '/docs',
description: 'HTML 格式的 API 文档(本页)。',
response: 'text/html',
},
{
method: 'GET', path: '/docs.json',
description: 'JSON 格式的 API 文档(机器可读)。',
response: 'application/json',
},
{
method: 'GET', path: '/api/health',
description: 'Liveness probe; reports whether the upstream browser-mcp stdio server is connected.',
response: { ok: true, upstream: true },
example: `curl -s ${baseUrl}/api/health -H "Authorization: Bearer ${token}"`,
},
{
method: 'GET', path: '/api/tools',
description: 'List all available browser tools (MCP tools/list).',
response: { tools: [{ name: 'browser_navigate', description: '...', inputSchema: {} }] },
example: `curl -s ${baseUrl}/api/tools -H "Authorization: Bearer ${token}"`,
},
{
method: 'POST', path: '/api/call',
description: 'Call a browser tool (MCP tools/call). Body: {"name": string, "arguments"?: object}.',
request: { name: 'browser_navigate', arguments: { url: 'https://example.com' } },
response: { content: [{ type: 'text', text: '...' }] },
example: `curl -s -X POST ${baseUrl}/api/call -H "Authorization: Bearer ${token}" -H "Content-Type: application/json" -d '{"name":"browser_list_tabs","arguments":{}}'`,
},
{
method: 'POST', path: '/mcp',
description: 'MCP Streamable HTTP endpoint (stateless; POST only). Point any MCP client here.',
notes: 'Accept header must include application/json and text/event-stream per the MCP spec.',
},
],
mcpClients: {
'kimi-code': {
file: '~/.kimi-code/mcp.json',
config: { 'browser-remote': { url: `${baseUrl}/mcp`, headers: { Authorization: `Bearer ${token}` } } },
note: 'Takes effect in new sessions; or use /mcp-config in the TUI.',
},
'claude-code': {
oneLiner: `claude mcp add --transport http browser-remote ${baseUrl}/mcp --header "Authorization: Bearer ${token}"`,
},
},
oneLinerDeploy: {
posix: `node -e "const f=process.env.HOME+'/.kimi-code/mcp.json',j=require(f);j.mcpServers['browser-remote']={url:'${baseUrl}/mcp',headers:{Authorization:'Bearer ${token}'}};require('fs').writeFileSync(f,JSON.stringify(j,null,2))"`,
powershell: `$f="$env:USERPROFILE\\.kimi-code\\mcp.json"; $j=Get-Content $f -Raw | ConvertFrom-Json; $j.mcpServers | Add-Member -NotePropertyName browser-remote -NotePropertyValue @{url='${baseUrl}/mcp';headers=@{Authorization='Bearer ${token}'}} -Force; $j | ConvertTo-Json -Depth 10 | Set-Content $f -Encoding UTF8`,
},
errors: {
401: 'missing or wrong token',
400: 'bad request body',
404: 'unknown route',
503: 'upstream browser-mcp not connected (is the Chrome extension connected?)',
},
};
}
function esc(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
export function renderDocsHtml(baseUrl, token) {
const d = renderDocs(baseUrl, token);
const pre = (o) => `<pre><code>${esc(typeof o === 'string' ? o : JSON.stringify(o, null, 2))}</code></pre>`;
const endpointRows = d.endpoints.map((e) =>
`<tr><td><span class="m m-${e.method.toLowerCase()}">${e.method}</span></td><td><code>${e.path}</code></td><td>${esc(e.description)}</td></tr>`,
).join('\n');
const endpointDetails = d.endpoints.map((e) => `
<section class="card">
<h3><span class="m m-${e.method.toLowerCase()}">${e.method}</span> <code>${e.path}</code></h3>
<p>${esc(e.description)}</p>
${e.notes ? `<p class="note">${esc(e.notes)}</p>` : ''}
${e.request ? `<h4>请求 body</h4>${pre(e.request)}` : ''}
${e.response ? `<h4>响应${typeof e.response === 'string' ? '(' + esc(e.response) + ')' : ' 示例'}</h4>${pre(e.response)}` : ''}
${e.example ? `<h4>示例</h4>${pre(e.example)}` : ''}
</section>`).join('\n');
const errorRows = Object.entries(d.errors).map(([code, msg]) =>
`<tr><td><code>${code}</code></td><td>${esc(msg)}</td></tr>`,
).join('\n');
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>API 文档 - browser-mcp gateway</title>
<style>
body { font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; width: 96%; max-width: 1400px; margin: 1.5em auto; padding: 0 1em; color: #24292f; line-height: 1.6; box-sizing: border-box; }
h1 { border-bottom: 2px solid #d0d7de; padding-bottom: .3em; }
h2 { margin-top: 1.8em; color: #0969da; }
h3 { margin-bottom: .3em; }
h4 { margin: .8em 0 .3em; color: #57606a; font-size: 14px; }
pre { background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 12px; overflow-x: auto; font-size: 13px; }
code { background: #f6f8fa; padding: 2px 5px; border-radius: 4px; font-size: 13px; }
pre code { padding: 0; background: none; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #d0d7de; padding: 6px 10px; text-align: left; font-size: 14px; vertical-align: top; }
th { background: #f6f8fa; }
.card { border: 1px solid #d0d7de; border-radius: 8px; padding: 4px 18px 14px; margin: 14px 0; }
.m { display: inline-block; min-width: 3em; text-align: center; font-weight: 700; font-size: 12px; border-radius: 4px; padding: 2px 8px; color: #fff; }
.m-get { background: #1a7f37; }
.m-post { background: #0969da; }
.note { color: #57606a; font-size: 13px; }
.meta { color: #57606a; }
</style>
</head>
<body>
<h1>${esc(d.name)} <span class="meta">v${esc(d.version)}</span></h1>
<p>${esc(d.description)}</p>
<p>Base URL:<code>${esc(d.baseUrl)}</code> | <a href="${esc(d.baseUrl)}/?token=${esc(token)}">返回使用说明</a> | <a href="${esc(d.baseUrl)}/docs.json?token=${esc(token)}">JSON 版本</a></p>
<h2>认证</h2>
<p>HTTP 头:<code>${esc(d.auth.header)}</code>,或 URL 参数:<code>${esc(d.auth.alt)}</code></p>
<h2>端点总览</h2>
<table>
<tr><th>方法</th><th>路径</th><th>说明</th></tr>
${endpointRows}
</table>
<h2>端点详情</h2>
${endpointDetails}
<h2>MCP 客户端接入</h2>
<h3>Kimi Code(${esc(d.mcpClients['kimi-code'].file)})</h3>
${pre(JSON.stringify({ mcpServers: d.mcpClients['kimi-code'].config }, null, 2))}
<p class="note">${esc(d.mcpClients['kimi-code'].note)}</p>
<h3>Claude Code(一句话)</h3>
${pre(d.mcpClients['claude-code'].oneLiner)}
<h2>一句话部署(写入 Kimi Code 配置)</h2>
<h3>Linux / macOS</h3>
${pre(d.oneLinerDeploy.posix)}
<h3>Windows PowerShell</h3>
${pre(d.oneLinerDeploy.powershell)}
<h2>错误码</h2>
<table>
<tr><th>状态码</th><th>含义</th></tr>
${errorRows}
</table>
</body>
</html>`;
}
// Server-side JSON syntax highlighting for the /docs.json browser view.
function highlightJson(obj) {
const json = JSON.stringify(obj, null, 2);
const re = /("(?:\\.|[^"\\])*")(\s*:)?|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\b|(true|false|null)\b/g;
let out = '';
let last = 0;
let m;
while ((m = re.exec(json))) {
out += esc(json.slice(last, m.index));
if (m[1] !== undefined) {
if (m[2]) out += `<span class="k">${esc(m[1])}</span>${esc(m[2])}`;
else out += `<span class="s">${esc(m[1])}</span>`;
} else if (m[3] !== undefined) {
out += `<span class="n">${esc(m[3])}</span>`;
} else {
out += `<span class="b">${esc(m[4])}</span>`;
}
last = m.index + m[0].length;
}
return out + esc(json.slice(last));
}
export function renderJsonViewer(baseUrl, token, doc) {
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>docs.json - browser-mcp gateway</title>
<style>
body { font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; width: 96%; max-width: 1400px; margin: 1.5em auto; padding: 0 1em; color: #24292f; line-height: 1.6; box-sizing: border-box; }
h1 { border-bottom: 2px solid #d0d7de; padding-bottom: .3em; font-size: 22px; }
.meta { color: #57606a; font-size: 14px; }
pre { background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; overflow-x: auto; font-size: 13px; font-family: ui-monospace, Consolas, monospace; line-height: 1.5; }
.k { color: #0550ae; }
.s { color: #0a3069; }
.n { color: #0550ae; }
.b { color: #cf222e; }
</style>
</head>
<body>
<h1>docs.json <span class="meta">browser-mcp gateway API 文档(JSON)</span></h1>
<p class="meta">浏览器访问自动格式化高亮显示;用 curl 或 <code>Accept: application/json</code> 请求可获取原始 JSON。
| <a href="${esc(baseUrl)}/docs?token=${esc(token)}">返回 HTML 文档</a>
| <a href="${esc(baseUrl)}/?token=${esc(token)}">使用说明</a></p>
<pre>${highlightJson(doc)}</pre>
</body>
</html>`;
}