diff --git a/napcat.webui/package.json b/napcat.webui/package.json index 5b38b017..a5efece0 100644 --- a/napcat.webui/package.json +++ b/napcat.webui/package.json @@ -46,9 +46,9 @@ "@react-aria/visually-hidden": "^3.8.19", "@reduxjs/toolkit": "^2.5.1", "@uidotdev/usehooks": "^2.4.1", + "@xterm/addon-canvas": "^0.7.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-web-links": "^0.11.0", - "@xterm/addon-webgl": "^0.18.0", "@xterm/xterm": "^5.5.0", "ahooks": "^3.8.4", "axios": "^1.7.9", diff --git a/napcat.webui/public/fonts/AaCute.woff b/napcat.webui/public/fonts/AaCute.woff new file mode 100644 index 00000000..19fcf9b8 Binary files /dev/null and b/napcat.webui/public/fonts/AaCute.woff differ diff --git a/napcat.webui/public/fonts/JetBrainsMono-Italic.ttf b/napcat.webui/public/fonts/JetBrainsMono-Italic.ttf new file mode 100644 index 00000000..54148355 Binary files /dev/null and b/napcat.webui/public/fonts/JetBrainsMono-Italic.ttf differ diff --git a/napcat.webui/src/fonts/JetBrainsMono.ttf b/napcat.webui/public/fonts/JetBrainsMono.ttf similarity index 100% rename from napcat.webui/src/fonts/JetBrainsMono.ttf rename to napcat.webui/public/fonts/JetBrainsMono.ttf diff --git a/napcat.webui/src/components/display_network_item.tsx b/napcat.webui/src/components/display_network_item.tsx index 87473951..fc8e550f 100644 --- a/napcat.webui/src/components/display_network_item.tsx +++ b/napcat.webui/src/components/display_network_item.tsx @@ -27,7 +27,7 @@ const NetworkItemDisplay: React.FC = ({
一言加载失败:{error.message}
) : ( <> -
{data?.hitokoto}
+
{data?.hitokoto}
—— {data?.from}{' '} {data?.from_who} diff --git a/napcat.webui/src/components/hover_titled_card.tsx b/napcat.webui/src/components/hover_titled_card.tsx index a6c4a5ef..c336fc99 100644 --- a/napcat.webui/src/components/hover_titled_card.tsx +++ b/napcat.webui/src/components/hover_titled_card.tsx @@ -34,7 +34,7 @@ export default function HoverTiltedCard({ rotateAmplitude = 14, showTooltip = false, overlayContent = ( -
+
NapCat
), diff --git a/napcat.webui/src/components/onebot/api/debug.tsx b/napcat.webui/src/components/onebot/api/debug.tsx index 19d4516f..150b1fcd 100644 --- a/napcat.webui/src/components/onebot/api/debug.tsx +++ b/napcat.webui/src/components/onebot/api/debug.tsx @@ -138,7 +138,7 @@ const OneBotApiDebug: React.FC = (props) => { shadow="sm" className="my-4 bg-opacity-50 backdrop-blur-md overflow-visible z-20" > - + 请求体
-
{data?.nick}
-
- {data?.uin} -
+
{data?.nick}
+
{data?.uin}
)} diff --git a/napcat.webui/src/components/sidebar/index.tsx b/napcat.webui/src/components/sidebar/index.tsx index 4124a815..137b928d 100644 --- a/napcat.webui/src/components/sidebar/index.tsx +++ b/napcat.webui/src/components/sidebar/index.tsx @@ -47,11 +47,11 @@ const SideBar: React.FC = (props) => { style={{ overflow: 'hidden' }} > -
+
diff --git a/napcat.webui/src/components/terminal/terminal-instance.tsx b/napcat.webui/src/components/terminal/terminal-instance.tsx index 1b21e382..2c35bacc 100644 --- a/napcat.webui/src/components/terminal/terminal-instance.tsx +++ b/napcat.webui/src/components/terminal/terminal-instance.tsx @@ -10,23 +10,24 @@ interface TerminalInstanceProps { export function TerminalInstance({ id }: TerminalInstanceProps) { const termRef = useRef(null) + const connected = useRef(false) + + const handleData = (data: string) => { + try { + const parsed = JSON.parse(data) + if (parsed.data) { + termRef.current?.write(parsed.data) + } + } catch (e) { + termRef.current?.write(data) + } + } useEffect(() => { - const handleData = (data: string) => { - try { - const parsed = JSON.parse(data) - if (parsed.data) { - termRef.current?.write(parsed.data) - } - } catch (e) { - termRef.current?.write(data) - } - } - - TerminalManager.connectTerminal(id, handleData) - return () => { - TerminalManager.disconnectTerminal(id, handleData) + if (connected.current) { + TerminalManager.disconnectTerminal(id, handleData) + } } }, [id]) @@ -34,5 +35,22 @@ export function TerminalInstance({ id }: TerminalInstanceProps) { TerminalManager.sendInput(id, data) } - return + const handleResize = (cols: number, rows: number) => { + if (!connected.current) { + connected.current = true + console.log('instance', rows, cols) + TerminalManager.connectTerminal(id, handleData, { rows, cols }) + } else { + TerminalManager.sendResize(id, cols, rows) + } + } + + return ( + + ) } diff --git a/napcat.webui/src/components/xterm.tsx b/napcat.webui/src/components/xterm.tsx index c4d2887d..47e4ef37 100644 --- a/napcat.webui/src/components/xterm.tsx +++ b/napcat.webui/src/components/xterm.tsx @@ -1,5 +1,7 @@ +import { CanvasAddon } from '@xterm/addon-canvas' import { FitAddon } from '@xterm/addon-fit' import { WebLinksAddon } from '@xterm/addon-web-links' +// import { WebglAddon } from '@xterm/addon-webgl' import { Terminal } from '@xterm/xterm' import '@xterm/xterm/css/xterm.css' import clsx from 'clsx' @@ -7,8 +9,6 @@ import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react' import { useTheme } from '@/hooks/use-theme' -import { gradientText } from '@/utils/terminal' - export type XTermRef = { write: ( ...args: Parameters @@ -19,26 +19,26 @@ export type XTermRef = { ) => ReturnType writelnAsync: (data: Parameters[0]) => Promise clear: () => void + terminalRef: React.RefObject } export interface XTermProps - extends Omit, 'onInput'> { + extends Omit, 'onInput' | 'onResize'> { onInput?: (data: string) => void onKey?: (key: string, event: KeyboardEvent) => void + onResize?: (cols: number, rows: number) => void // 新增属性 } const XTerm = forwardRef((props, ref) => { const domRef = useRef(null) const terminalRef = useRef(null) - const { className, onInput, onKey, ...rest } = props + const { className, onInput, onKey, onResize, ...rest } = props const { theme } = useTheme() useEffect(() => { - if (!domRef.current) { - return - } const terminal = new Terminal({ allowTransparency: true, - fontFamily: '"JetBrains Mono", "Aa偷吃可爱长大的", "Noto Serif SC", monospace', + fontFamily: + '"JetBrains Mono", "Aa偷吃可爱长大的", "Noto Serif SC", monospace', cursorInactiveStyle: 'outline', drawBoldTextInBrightColors: false, fontSize: 14, @@ -48,25 +48,15 @@ const XTerm = forwardRef((props, ref) => { const fitAddon = new FitAddon() terminal.loadAddon( new WebLinksAddon((event, uri) => { - if (event.ctrlKey) { + if (event.ctrlKey || event.metaKey) { window.open(uri, '_blank') } }) ) terminal.loadAddon(fitAddon) - terminal.open(domRef.current) - - terminal.writeln( - gradientText( - 'Welcome to NapCat WebUI', - [255, 0, 0], - [0, 255, 0], - true, - true, - true - ) - ) + terminal.open(domRef.current!) + terminal.loadAddon(new CanvasAddon()) terminal.onData((data) => { if (onInput) { onInput(data) @@ -81,6 +71,12 @@ const XTerm = forwardRef((props, ref) => { const resizeObserver = new ResizeObserver(() => { fitAddon.fit() + // 获取当前终端尺寸 + const cols = terminal.cols + const rows = terminal.rows + if (onResize) { + onResize(cols, rows) + } }) // 字体加载完成后重新调整终端大小 @@ -100,21 +96,49 @@ const XTerm = forwardRef((props, ref) => { useEffect(() => { if (terminalRef.current) { - terminalRef.current.options.theme = { - background: theme === 'dark' ? '#00000000' : '#ffffff00', - foreground: theme === 'dark' ? '#fff' : '#000', - selectionBackground: - theme === 'dark' - ? 'rgba(179, 0, 0, 0.3)' - : 'rgba(255, 167, 167, 0.3)', - cursor: theme === 'dark' ? '#fff' : '#000', - cursorAccent: theme === 'dark' ? '#000' : '#fff', - black: theme === 'dark' ? '#fff' : '#000' + if (theme === 'dark') { + terminalRef.current.options.theme = { + background: '#00000000', + black: '#000000', + red: '#cd3131', + green: '#0dbc79', + yellow: '#e5e510', + blue: '#2472c8', + cyan: '#11a8cd', + white: '#e5e5e5', + brightBlack: '#666666', + brightRed: '#f14c4c', + brightGreen: '#23d18b', + brightYellow: '#f5f543', + brightBlue: '#3b8eea', + brightCyan: '#29b8db', + brightWhite: '#e5e5e5', + foreground: '#cccccc', + selectionBackground: '#3a3d41', + cursor: '#ffffff' + } + } else { + terminalRef.current.options.theme = { + background: '#ffffff00', + black: '#000000', + red: '#aa3731', + green: '#448c27', + yellow: '#cb9000', + blue: '#325cc0', + cyan: '#0083b2', + white: '#7f7f7f', + brightBlack: '#777777', + brightRed: '#f05050', + brightGreen: '#60cb00', + brightYellow: '#ffbc5d', + brightBlue: '#007acc', + brightCyan: '#00aacb', + brightWhite: '#b0b0b0', + foreground: '#000000', + selectionBackground: '#bfdbfe', + cursor: '#007acc' + } } - terminalRef.current.options.fontWeight = - theme === 'dark' ? 'normal' : '600' - terminalRef.current.options.fontWeightBold = - theme === 'dark' ? 'bold' : '900' } }, [theme]) @@ -139,7 +163,8 @@ const XTerm = forwardRef((props, ref) => { }, clear: () => { terminalRef.current?.clear() - } + }, + terminalRef: terminalRef }), [] ) diff --git a/napcat.webui/src/config/site.tsx b/napcat.webui/src/config/site.tsx index 9dfa7ee6..79bdc43f 100644 --- a/napcat.webui/src/config/site.tsx +++ b/napcat.webui/src/config/site.tsx @@ -51,7 +51,7 @@ export const siteConfig = { href: '/config' }, { - label: 'NapCat日志', + label: '猫猫日志', icon: (
diff --git a/napcat.webui/src/controllers/terminal_manager.ts b/napcat.webui/src/controllers/terminal_manager.ts index a66b5224..2a0c375e 100644 --- a/napcat.webui/src/controllers/terminal_manager.ts +++ b/napcat.webui/src/controllers/terminal_manager.ts @@ -41,9 +41,16 @@ class TerminalManager { return data.data } - connectTerminal(id: string, callback: TerminalCallback): WebSocket { + connectTerminal( + id: string, + callback: TerminalCallback, + config?: { + cols?: number + rows?: number + } + ): WebSocket { let conn = this.connections.get(id) - + const { cols = 80, rows = 24 } = config || {} if (!conn) { const url = new URL(window.location.href) url.protocol = url.protocol.replace('http', 'ws') @@ -74,6 +81,7 @@ class TerminalManager { ws.onopen = () => { if (conn) conn.isConnected = true + this.sendResize(id, cols, rows) } ws.onclose = () => { @@ -111,6 +119,13 @@ class TerminalManager { conn.ws.send(JSON.stringify({ type: 'input', data })) } } + + sendResize(id: string, cols: number, rows: number) { + const conn = this.connections.get(id) + if (conn?.ws.readyState === WebSocket.OPEN) { + conn.ws.send(JSON.stringify({ type: 'resize', cols, rows })) + } + } } const terminalManager = new TerminalManager() diff --git a/napcat.webui/src/fonts/AaCute.ttf b/napcat.webui/src/fonts/AaCute.ttf deleted file mode 100644 index e3ee1ee4..00000000 Binary files a/napcat.webui/src/fonts/AaCute.ttf and /dev/null differ diff --git a/napcat.webui/src/layouts/default.tsx b/napcat.webui/src/layouts/default.tsx index b4146d0f..1f575f75 100644 --- a/napcat.webui/src/layouts/default.tsx +++ b/napcat.webui/src/layouts/default.tsx @@ -98,7 +98,7 @@ const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => { >
+
{ - const filename = req.query.id as string; + const filename = req.query['id']; + if (!filename || typeof filename !== 'string') { + return sendError(res, 'ID不能为空'); + } + if (filename.includes('..')) { return sendError(res, 'ID不合法'); } @@ -40,7 +44,8 @@ export const LogRealTimeHandler: RequestHandler = async (req, res) => { // 终端相关处理器 export const CreateTerminalHandler: RequestHandler = async (req, res) => { try { - const { id } = terminalManager.createTerminal(); + const { cols, rows } = req.body; + const { id } = terminalManager.createTerminal(cols, rows); return sendSuccess(res, { id }); } catch (error) { console.error('Failed to create terminal:', error); @@ -54,7 +59,10 @@ export const GetTerminalListHandler: RequestHandler = (_, res) => { }; export const CloseTerminalHandler: RequestHandler = (req, res) => { - const id = req.params.id; + const id = req.params['id']; + if (!id) { + return sendError(res, 'ID不能为空'); + } terminalManager.closeTerminal(id); return sendSuccess(res, {}); }; diff --git a/src/webui/src/terminal/terminal_manager.ts b/src/webui/src/terminal/terminal_manager.ts index 2d9c6e01..077510dd 100644 --- a/src/webui/src/terminal/terminal_manager.ts +++ b/src/webui/src/terminal/terminal_manager.ts @@ -13,6 +13,8 @@ interface TerminalInstance { sockets: Set; // 新增标识,用于防止重复关闭 isClosing: boolean; + // 新增:存储终端历史输出 + buffer: string; } class TerminalManager { @@ -67,21 +69,24 @@ class TerminalManager { return; } - const dataHandler = (data: string) => { - if (ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ type: 'output', data })); - } - }; - instance.sockets.add(ws); instance.lastAccess = Date.now(); + // 新增:发送当前终端内容给新连接 + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: 'output', data: instance.buffer })); + } + ws.on('message', (data) => { if (instance) { const result = JSON.parse(data.toString()); if (result.type === 'input') { instance.pty.write(result.data); } + // 新增:处理 resize 消息 + if (result.type === 'resize') { + instance.pty.resize(result.cols, result.rows); + } } }); @@ -103,18 +108,17 @@ class TerminalManager { }); } - // 修改:移除参数 id,使用 crypto.randomUUID 生成终端 id - createTerminal() { + // 修改:新增 cols 和 rows 参数,同步 xterm 尺寸,防止错位 + createTerminal(cols: number, rows: number) { const id = randomUUID(); const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash'; const pty = ptySpawn(shell, [], { name: 'xterm-256color', - cols: 80, - rows: 24, + cols, // 使用客户端传入的 cols + rows, // 使用客户端传入的 rows cwd: process.cwd(), env: { ...process.env, - // 统一编码设置 LANG: os.platform() === 'win32' ? 'chcp 65001' : 'zh_CN.UTF-8', TERM: 'xterm-256color', }, @@ -125,9 +129,13 @@ class TerminalManager { lastAccess: Date.now(), sockets: new Set(), isClosing: false, + buffer: '', // 初始化终端内容缓存 }; pty.onData((data: any) => { + // 追加数据到 buffer + instance.buffer += data; + // 发送数据给已连接的 websocket instance.sockets.forEach((ws) => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'output', data }));