局域网与服务
局域网与服务
这一页讲的是 JSXHook 里几块“对外服务能力”的关系:
- 局域网文件页
- 局域网剪贴板同步
- 脚本侧
httpServer - 脚本侧
mcpServer
它们看起来都和“网络”“服务”有关,但职责并不一样。
如果你以后要排查下面这类问题,这页会很有用:
- 为什么局域网文件页默认是
1314端口 - 为什么 LAN 页面能访问文件,但路径不能越出
/sdcard - 为什么远端手机连上了,但剪贴板没同步
httpServer和局域网页面是不是同一套引擎mcpServer的默认地址、默认端口、默认 endpoint 到底是什么
目录
- 先记住这 12 条
- 关键源码
- 这一层到底分成哪三块
- LanTransferManager:局域网文件页与剪贴板同步
- LAN 页面有哪些路由
- 路径边界与存储权限
- 剪贴板同步与远端手机连接
- StandaloneHttpServer:脚本 HTTP 服务底座
- McpServerBridge:把 HTTP 升级成 MCP
- 什么时候看哪一层
- 排错顺序
先记住这 12 条
- 局域网文件页和脚本里的
httpServer不是同一个对象,但它们都属于“JSXHook 的对外服务能力”。 LanTransferManager默认端口是1314。StandaloneHttpServer默认 host/port 是:127.0.0.118765
mcpServer默认 host/port/endpoint 是:127.0.0.15698/mcp
mcpServer.start({ allowLan: true })时,如果没手填 host,会自动把 host 变成0.0.0.0。- LAN 文件浏览根目录不是任意路径,源码会把浏览范围收敛在外部存储根附近,并做越界校验。
- 空路径或
/在 LAN 文件页里通常会落到浏览根目录。 - LAN 上传会自动创建父目录,但删除浏览根目录本身会被显式禁止。
- LAN 剪贴板同步不是单向广播,它还做了回声抑制,避免本机和远端反复互相写回。
StandaloneHttpServer的 token 校验来源是:- 请求头
x-module-token - 或 query 参数
token
- 请求头
StandaloneHttpServer没有“动态路径参数路由”这一层,路由匹配是精确的method + path,外加一个ANY兜底。mcpServer在 CORS 上默认信任本地 origin,但远端浏览器 origin 需要落在allowedOrigins里。
关键源码
core/lan/LanTransferManager.ktcore/lan/LanTransferServer.ktcore/lan/LanTransferWebPage.ktcore/server/StandaloneHttpServer.kthook/javascript/McpServerBridge.kt
这一层到底分成哪三块
可以先用一句话分工:
| 组件 | 主要职责 |
|---|---|
LanTransferManager | JSXHook 自带的局域网文件页、文件浏览、上传下载、剪贴板同步、远端手机互联 |
StandaloneHttpServer | 轻量 HTTP 服务底座,给脚本 httpServer 和 MCP 桥接层复用 |
McpServerBridge | 在 StandaloneHttpServer 之上实现 MCP 协议、会话、工具/资源/提示词注册 |
也就是说:
- 你想做“局域网文件页”时,主要看
LanTransferManager - 你想做“脚本自定义 HTTP 接口”时,主要看
httpServer/StandaloneHttpServer - 你想做“让 Agent / MCP Client 能调你的工具”时,主要看
mcpServer/McpServerBridge
LanTransferManager:局域网文件页与剪贴板同步
LanTransferManager 是 JSXHook 自带 LAN 功能的总控层。
它持久化了哪些设置
源码里这几个设置会存进 SharedPreferences("lan_transfer"):
| 键 | 含义 | 默认值 |
|---|---|---|
enabled | 是否开启 LAN 服务 | false |
port | 服务端口 | 1314 |
clipboard_sync | 是否启用剪贴板同步 | true |
remote_peer_target | 远端手机目标地址 | "" |
端口和开关的真实规则
applySettings(...) 会先把新配置做一层规范化:
port强制夹到1..65535remotePeerTarget会先trim()
如果最终 enabled = false,它会做这些事:
- 停掉服务器
- 停掉剪贴板监听
- 断开远端手机连接
- 清空错误态
如果 enabled = true,会发生什么
它会尝试:
- 启动 LAN HTTP/WS 服务器
- 如果开了剪贴板同步,就开始监听剪贴板
- 同步远端手机连接状态
- 更新状态快照
如果中间任何一步异常:
- 会记录错误信息
- 停服
- 关闭剪贴板监听
- 断开远端连接
openPageUrl(context) 的返回逻辑
这个方法会优先返回:
lanUrl- 如果拿不到,再回退
localUrl
所以你在 UI 上看到的“打开局域网页面”,本质上就是优先选能给别的设备访问的局域网地址。
LAN 页面有哪些路由
LanTransferManager.handleHttpRequest(...) 里实际上已经把 LAN 页面能做的事都列清了。
主要路由总表
| 路径 | 方法 | 作用 |
|---|---|---|
/ | GET | 返回局域网页面 HTML |
/api/state | GET | 返回当前 LAN 状态 |
/api/list | GET | 列当前目录 |
/api/search | GET | 递归搜索文件 |
/api/raw | GET | 在线预览原始文件 |
/api/download | GET | 下载文件 |
/api/upload | POST | 上传文件 |
/api/file | DELETE | 删除文件或目录 |
/api/open | GET | 返回某个路径的基础信息 |
/preview | GET | 返回预览页 HTML |
/favicon.ico | 任意 | 直接 204 |
一些很实用的细节
1. /api/list 和 /api/search 的区别
| 接口 | 搜索方式 |
|---|---|
/api/list | 当前目录,不递归 |
/api/search | 递归搜索子目录 |
2. /api/raw 和 /api/download 的区别
| 接口 | Content-Disposition |
|---|---|
/api/raw | inline,更偏在线预览 |
/api/download | attachment,更偏下载 |
3. 上传会自动补目录
/api/upload 最终会:
- 解析目标目录
- 根据
relativePath或文件名决定最终路径 - 自动
mkdirs()
4. 删除不是无限制的
/api/file 的 DELETE 有一个非常关键的保护:
- 如果目标路径刚好是浏览根目录本身,直接拒绝删除
路径边界与存储权限
这块值得单独讲,因为它直接决定了 LAN 文件页是不是“会乱跑目录”的实现。
浏览根目录从哪来
源码会按顺序尝试这些候选:
/sdcardEnvironment.getExternalStorageDirectory()/storage/emulated/0
找到第一个存在的路径后,会把它当成浏览根。
空路径和 / 怎么处理
在 LAN 文件页里:
- 空字符串
- 只含空白
/
通常都会被解析成浏览根目录。
路径越界保护怎么做
最终路径会过一层 ensureWithinBrowseRoot(...):
- 如果目标路径就是浏览根,允许
- 如果目标路径在浏览根下面,允许
- 只要试图跳出根目录,直接抛异常
所以这种路径会被挡住:
../../- 指向别的根目录的绝对路径
- 任何 canonical path 不在浏览根下的路径
为什么这层保护重要
因为 LAN 页面本身就支持:
- 列目录
- 下载
- 上传
- 删除
如果不做路径边界保护,这块会非常危险。
存储权限检查
requestStorageAccessGranted() 底层判断的是:
- Android 11 以下:默认认为可用
- Android 11 及以上:看
Environment.isExternalStorageManager()
所以如果你遇到“局域网页面开了,但看不到完整文件树”之类的问题,要把“是否有全部文件访问权限”列进排查项。
剪贴板同步与远端手机连接
LAN 这块不只有文件浏览,还有一个做得比较完整的剪贴板同步链。
本机剪贴板同步怎么触发
当开启 clipboardSync 时:
LanTransferManager会注册ClipboardManager.OnPrimaryClipChangedListener- 剪贴板变化后调用
onClipboardChanged()
它为什么不会无限回声
源码用了两个关键变量:
latestClipboardTextpendingClipboardEcho
逻辑上是:
- 收到远端文本时,先写
pendingClipboardEcho - 本机真正收到这次剪贴板变更时,发现和
pendingClipboardEcho相同 - 这次变化只确认落地,不再反向广播
这就是“回声抑制”。
远端手机连接什么时候会建立
只有这些条件同时满足,才会继续连远端:
enabled = trueremotePeerTarget非空clipboardSync = true
只要任何一项不满足,就会断开远端连接。
远端地址是怎么被规范化的
resolveRemotePeerWebSocketUrl(...) 的真实规则:
| 输入 | 结果 |
|---|---|
纯 192.168.1.8 | 自动补成 ws://192.168.1.8:<port> |
http://... / https://... | 自动换算成 ws:// / wss:// |
| 不带端口 | 用当前设置里的端口 |
| host 非法或为空 | 抛格式错误 |
一个容易忽略的点
局域网远端同步默认走的是 WebSocket,不是普通轮询 HTTP。
StandaloneHttpServer:脚本 HTTP 服务底座
StandaloneHttpServer 是一个通用轻量 HTTP 引擎。
脚本里的 httpServer 和 MCP 桥接层本质上都是在复用它。
默认配置
| 字段 | 默认值 |
|---|---|
host | 127.0.0.1 |
port | 18765 |
token | "" |
start() 的行为
传入配置后会先规范化:
host为空时回退127.0.0.1port强制夹到1..65535
如果当前服务已经活着,并且配置完全一样:
- 不会重复重启
- 会返回一个带
reused = true的状态
路由匹配规则
路由键是:
methodpath
优先顺序是:
- 精确匹配
method + path - 找不到再匹配
ANY + path
method 会怎么规范化
| 输入 method | 最终 method |
|---|---|
GET | GET |
post | POST |
* | ANY |
ALL | ANY |
path 会怎么规范化
| 输入 path | 最终 path |
|---|---|
"" | / |
/ | / |
users/list | /users/list |
//users//list// | /users/list |
token 校验规则
如果 config.token 是空字符串:
- 不做 token 校验
如果 token 非空:
- 请求头
x-module-token - 或 query 参数
token
只要有一个匹配即可通过。
请求体解析规则
StandaloneHttpServer 会把请求拆成:
querybodyparamsrawBody
其中 params 是:
- query 参数
- 加上 body 里能解析出的键值
body 解析规则
Content-Type | body 的解析方式 |
|---|---|
application/json | 解析成 Kotlin Map / List / 基本类型 |
application/x-www-form-urlencoded | 解析成参数表 |
| 其他 | 保留原始字符串 |
失败返回长什么样
如果路由不存在、token 不对、处理器抛异常,底层会统一返回 JSON 错误结构,例如:
{
"code": -1,
"message": "route not found",
"data": {
"method": "GET",
"path": "/demo"
}
}
一个很重要的边界
它没有 Express 那种“/user/:id 自动提取路径参数”的机制。
你要么:
- 用 query/body 传参
- 要么自己在 handler 里解析 path
McpServerBridge:把 HTTP 升级成 MCP
mcpServer 不是重新造了个新网络引擎,而是在 StandaloneHttpServer 之上实现 MCP 协议语义。
默认配置
| 字段 | 默认值 |
|---|---|
host | 127.0.0.1 |
port | 5698 |
token | "" |
endpoint | /mcp |
name | jsxhook-mcp |
version | 1.0.0 |
instructions | "" |
allowedOrigins | 空集合 |
allowLan 对 host 的影响
如果你在配置对象里写了:
{ allowLan: true }
而没有手动写 host,那么 host 会自动变成:
0.0.0.0
如果没开 allowLan,默认还是:
127.0.0.1
它安装了哪些固定路由
在当前 endpoint 上,它会安装:
| 方法 | 行为 |
|---|---|
OPTIONS | 处理 CORS 预检 |
GET | 返回 405,不允许普通 GET 调用 |
DELETE | 重置会话生命周期状态 |
POST | 真正处理 MCP / JSON-RPC 请求 |
默认 URL 长什么样
对外展示 URL 时,如果 host 是 0.0.0.0,它会把可展示地址换成:
http://127.0.0.1:<port><endpoint>
也就是更适合本机客户端复制使用的形式。
协议版本支持
源码里目前支持这些协议版本:
2025-11-252025-06-182025-03-262024-11-05
协商时会按:
- initialize 参数里的
protocolVersion - 请求头
mcp-protocol-version - 默认最新版本
这个顺序来选。
origin 规则
默认会直接允许这些本地 origin:
http://localhost...https://localhost...http://127.0.0.1...https://127.0.0.1...http://[::1]...https://[::1]...
如果是远端 origin:
- 必须命中
allowedOrigins
token 校验还在不在
还在。
因为它底层依然是 StandaloneHttpServer,所以 token 校验依然走:
x-module-token- 或 query
token
生命周期状态会记录什么
mcpServer.state() 这类状态里,核心会看到:
startedhostportendpointnameversionsessionIdsessionEstablishedinitializedtoolCountresourceCountresourceTemplateCountpromptCountprotocolVersionurl
什么时候看哪一层
这个选择很重要。
你要做的是“局域网文件管理 / 传文件 / 远端手机同步”
先看:
LanTransferManager
你要做的是“脚本自定义一两个 HTTP 接口”
先看:
httpServerStandaloneHttpServer
你要做的是“把工具暴露给 Agent / MCP Client”
先看:
mcpServerMcpServerBridge
你要排查“网络服务为什么起不来”
顺序建议是:
- 端口是否合法 / 是否被占用
- host 是否符合预期
- token 是否配置错
- origin 是否被拦
- 上层业务逻辑是否抛异常
排错顺序
1. LAN 页面打不开
先查:
enabled有没有开- 端口是不是
1314或你自定义的端口 - 服务启动时有没有异常
2. LAN 页面能打开,但看不到想要的文件
先查:
- 当前有没有全部文件访问权限
- 目标路径是不是已经越出浏览根
- 你传的是文件路径还是目录路径
3. 上传可以,删除失败
先查:
- 目标是不是浏览根本身
- 目标文件是否存在
- 目录是否正在被占用或系统拒绝删除
4. 剪贴板不同步
先查:
clipboardSync是否开启- 远端地址是否为空
- 远端连接是否真的建立
- 是不是被回声抑制逻辑拦住了重复写回
5. httpServer 路由总是 404
先查:
- method 是否一致
- path 是否被 normalize 后和你想的不一样
- 是否应该注册成
ANY
6. mcpServer 客户端连不上
先查:
- host / port / endpoint 是否一致
- token 是否匹配
- origin 是否被允许
- 客户端发的协议版本是否在支持列表里
最后压成一句话
LanTransferManager负责 JSXHook 自带的局域网文件页和剪贴板同步。StandaloneHttpServer负责给脚本 HTTP 服务提供一个通用底座。McpServerBridge负责把这个 HTTP 底座升级成 MCP 能理解的协议层。- 真正排错时,先分清你碰到的是“LAN 功能”“通用 HTTP 服务”还是“MCP 协议层”,再往下查会清楚很多。
