feat(settings): 设置页右边常驻服务器浏览器的实时画面
用途只有一个:**过验证码**。抖音/小红书弹滑块时采集会静默失败(网关回 200 + 空 body,看起来像「这个博主没作品」),人在这块画面里点一下就好,不用跑到机器跟前。 走 noVNC(宿主机上 websockify 起的静态站),iframe 嵌进来而不是 window.open —— 要的就是「抬眼就能看见」。主机名取自当前页面,换 IP/域名访问都能用。 noVNC 不发 X-Frame-Options,所以嵌得进来(curl -I 确认过)。 密码**没有写进代码**:noVNC 地址里的 password 参数留空,等宿主机上那个密码去掉 之后就是点开即连。写死在这里会进 git 历史、还会打进前端产物。
This commit is contained in:
@@ -0,0 +1,97 @@
|
|||||||
|
import { useMemo, useState } from 'react'
|
||||||
|
import { ExternalLink, MonitorPlay, RotateCw } from 'lucide-react'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器上那个浏览器的**实时画面** —— 就是定时任务接管的那一个。
|
||||||
|
*
|
||||||
|
* 用途只有一个:**过验证码**。抖音/小红书弹滑块或短信验证时,采集会静默失败
|
||||||
|
* (网关回 200 + 空 body,看起来像「这个博主没作品」),人在这个画面里点一下就好了,
|
||||||
|
* 不用跑到机器跟前。
|
||||||
|
*
|
||||||
|
* 走 noVNC(websockify 在宿主机上起的静态站)。
|
||||||
|
*
|
||||||
|
* **别把它当成「看采集结果」的地方** —— 结果是作品栏和报表,这里是浏览器本身。
|
||||||
|
*/
|
||||||
|
|
||||||
|
// websockify 监听的端口。和 18051 这个应用不是同一个,所以**必须**是绝对地址。
|
||||||
|
const VNC_PORT = 6080
|
||||||
|
|
||||||
|
// 特意**留空**:服务端那边密码去掉之后,地址里不传 password 就不会弹框。
|
||||||
|
//
|
||||||
|
// 千万不要把密码写死在这里 —— 它会进 git 历史,还会打进前端产物,而任何能打开
|
||||||
|
// 这个页面的人都能看到。要密码就打进 noVNC 自己的框里,或者去服务端把密码去掉。
|
||||||
|
const VNC_PASSWORD = ''
|
||||||
|
|
||||||
|
/** 拼 noVNC 的地址。主机名取自当前页面,所以换域名/IP 访问都能用。 */
|
||||||
|
function viewerUrl(host: string, password: string): string {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
autoconnect: '1',
|
||||||
|
// 让远程画面缩放到容器大小,而不是撑出滚动条。
|
||||||
|
resize: 'scale',
|
||||||
|
// 掉线自己重连,省得每次断线都要手动点。
|
||||||
|
reconnect: '1',
|
||||||
|
reconnect_delay: '2000',
|
||||||
|
})
|
||||||
|
if (password) params.set('password', password)
|
||||||
|
return `http://${host}:${VNC_PORT}/vnc.html?${params.toString()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RemoteBrowserPanel() {
|
||||||
|
const [nonce, setNonce] = useState(0)
|
||||||
|
|
||||||
|
const url = useMemo(
|
||||||
|
() => viewerUrl(window.location.hostname, VNC_PASSWORD),
|
||||||
|
// nonce 变化 = 用户点了「重连」,靠换 key 强制 iframe 重新加载。
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[nonce],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full min-h-0 rounded-lg border border-cyber-border-subtle bg-cyber-bg-secondary/40 overflow-hidden">
|
||||||
|
<div className="flex items-center gap-2 px-3 py-2 border-b border-cyber-border-subtle bg-cyber-bg-tertiary/60 flex-shrink-0">
|
||||||
|
<MonitorPlay className="w-3.5 h-3.5 text-cyber-neon-cyan flex-shrink-0" />
|
||||||
|
<span className="text-[11px] font-mono text-cyber-text-primary">远程浏览器</span>
|
||||||
|
<span className="text-[10px] font-mono text-cyber-text-muted truncate">
|
||||||
|
采集用的那个 · 弹验证码时在这里点
|
||||||
|
</span>
|
||||||
|
<div className="ml-auto flex items-center gap-1 flex-shrink-0">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 px-1.5"
|
||||||
|
title="重新连接"
|
||||||
|
onClick={() => setNonce((value) => value + 1)}
|
||||||
|
>
|
||||||
|
<RotateCw className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 px-1.5"
|
||||||
|
title="在新窗口打开"
|
||||||
|
onClick={() => window.open(url, '_blank')}
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* iframe 而不是 window.open:要的就是「抬眼就能看见」,而不是再开一个标签页。
|
||||||
|
noVNC 那边不发 X-Frame-Options,所以嵌得进来。 */}
|
||||||
|
<iframe
|
||||||
|
key={nonce}
|
||||||
|
src={url}
|
||||||
|
title="远程浏览器"
|
||||||
|
className="flex-1 min-h-0 w-full bg-black"
|
||||||
|
allow="clipboard-read; clipboard-write"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p className="px-3 py-1.5 text-[10px] font-mono text-cyber-text-muted border-t border-cyber-border-subtle flex-shrink-0">
|
||||||
|
连不上就说明宿主机上的 websockify 没在跑(`{window.location.hostname}:{VNC_PORT}`)。
|
||||||
|
这个画面里就是服务器的浏览器本人,能看到你所有登录态 —— 只在局域网里用。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { KeyRound, QrCode, Save } from 'lucide-react'
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { CookiePanel } from '@/components/monitor/CookiePanel'
|
import { CookiePanel } from '@/components/monitor/CookiePanel'
|
||||||
import { QrLoginPanel } from '@/components/monitor/QrLoginPanel'
|
import { QrLoginPanel } from '@/components/monitor/QrLoginPanel'
|
||||||
|
import { RemoteBrowserPanel } from '@/components/settings/RemoteBrowserPanel'
|
||||||
import { Section, SettingField } from '@/components/settings/SettingFields'
|
import { Section, SettingField } from '@/components/settings/SettingFields'
|
||||||
import { useSettings, useUpdateSettings } from '@/hooks/useMonitor'
|
import { useSettings, useUpdateSettings } from '@/hooks/useMonitor'
|
||||||
import { useCurrentPlatform } from '@/hooks/usePlatform'
|
import { useCurrentPlatform } from '@/hooks/usePlatform'
|
||||||
@@ -55,8 +56,9 @@ export function SettingsView({ onNavigate }: { onNavigate?: (view: AppView) => v
|
|||||||
const label = capability?.label ?? platform
|
const label = capability?.label ?? platform
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 overflow-y-auto terminal-scroll pr-1">
|
<div className="flex-1 flex gap-4 min-h-0">
|
||||||
<div className="max-w-3xl space-y-3 pb-6">
|
{/* max-w-3xl 是为了让设置项保持好读的行宽;右边那块画面把剩下的宽度拿走。 */}
|
||||||
|
<div className="flex-1 max-w-3xl overflow-y-auto terminal-scroll pr-1 min-w-0 space-y-3 pb-6">
|
||||||
<div className="rounded-lg border border-cyber-neon-cyan/30 bg-cyber-neon-cyan/5 px-4 py-2">
|
<div className="rounded-lg border border-cyber-neon-cyan/30 bg-cyber-neon-cyan/5 px-4 py-2">
|
||||||
<p className="text-[11px] font-mono text-cyber-text-secondary">
|
<p className="text-[11px] font-mono text-cyber-text-secondary">
|
||||||
当前平台:
|
当前平台:
|
||||||
@@ -139,6 +141,12 @@ export function SettingsView({ onNavigate }: { onNavigate?: (view: AppView) => v
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 右边常驻一块服务器浏览器的实时画面 —— 弹验证码时要能抬眼就看见、伸手就能点,
|
||||||
|
而不是「切页面 → 找入口 → 等加载」。宽度固定,左边那列自己让位。 */}
|
||||||
|
<aside className="w-[380px] xl:w-[460px] 2xl:w-[560px] flex-shrink-0 min-h-0">
|
||||||
|
<RemoteBrowserPanel />
|
||||||
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user