mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
feat: 系统终端
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
"version": "0.0.6",
|
"version": "0.0.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite --host=0.0.0.0",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"lint": "eslint -c eslint.config.mjs ./src/**/**/*.{ts,tsx} --fix",
|
"lint": "eslint -c eslint.config.mjs ./src/**/**/*.{ts,tsx} --fix",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
|
@@ -1,74 +0,0 @@
|
|||||||
import { useSortable } from '@dnd-kit/sortable'
|
|
||||||
import { CSS } from '@dnd-kit/utilities'
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import { useRef } from 'react'
|
|
||||||
|
|
||||||
import { Tab } from './tabs'
|
|
||||||
|
|
||||||
interface SortableTabProps {
|
|
||||||
id: string
|
|
||||||
value: string
|
|
||||||
children: React.ReactNode
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SortableTab({
|
|
||||||
id,
|
|
||||||
value,
|
|
||||||
children,
|
|
||||||
className
|
|
||||||
}: SortableTabProps) {
|
|
||||||
const {
|
|
||||||
attributes,
|
|
||||||
listeners,
|
|
||||||
setNodeRef,
|
|
||||||
transform,
|
|
||||||
transition,
|
|
||||||
isDragging
|
|
||||||
} = useSortable({ id })
|
|
||||||
|
|
||||||
const mouseDownTime = useRef<number>(0)
|
|
||||||
const mouseDownPos = useRef<{ x: number; y: number }>({ x: 0, y: 0 })
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
transform: CSS.Transform.toString(transform),
|
|
||||||
transition,
|
|
||||||
zIndex: isDragging ? 1 : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMouseDown = (e: React.MouseEvent) => {
|
|
||||||
mouseDownTime.current = Date.now()
|
|
||||||
mouseDownPos.current = { x: e.clientX, y: e.clientY }
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMouseUp = (e: React.MouseEvent) => {
|
|
||||||
const timeDiff = Date.now() - mouseDownTime.current
|
|
||||||
const distanceX = Math.abs(e.clientX - mouseDownPos.current.x)
|
|
||||||
const distanceY = Math.abs(e.clientY - mouseDownPos.current.y)
|
|
||||||
|
|
||||||
// 如果时间小于200ms且移动距离小于5px,认为是点击而不是拖拽
|
|
||||||
if (timeDiff < 200 && distanceX < 5 && distanceY < 5) {
|
|
||||||
listeners?.onClick?.(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tab
|
|
||||||
ref={setNodeRef}
|
|
||||||
style={style}
|
|
||||||
value={value}
|
|
||||||
{...attributes}
|
|
||||||
onMouseDown={handleMouseDown}
|
|
||||||
onMouseUp={handleMouseUp}
|
|
||||||
className={clsx(
|
|
||||||
'cursor-move select-none border-b-2 transition-colors',
|
|
||||||
isDragging
|
|
||||||
? 'bg-default-100 border-primary'
|
|
||||||
: 'hover:bg-default-100 border-transparent',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Tab>
|
|
||||||
)
|
|
||||||
}
|
|
@@ -1,7 +1,7 @@
|
|||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { type ReactNode, createContext, forwardRef, useContext } from 'react'
|
import { type ReactNode, createContext, forwardRef, useContext } from 'react'
|
||||||
|
|
||||||
interface TabsContextValue {
|
export interface TabsContextValue {
|
||||||
activeKey: string
|
activeKey: string
|
||||||
onChange: (key: string) => void
|
onChange: (key: string) => void
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@ const TabsContext = createContext<TabsContextValue>({
|
|||||||
onChange: () => {}
|
onChange: () => {}
|
||||||
})
|
})
|
||||||
|
|
||||||
interface TabsProps {
|
export interface TabsProps {
|
||||||
activeKey: string
|
activeKey: string
|
||||||
onChange: (key: string) => void
|
onChange: (key: string) => void
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
@@ -26,7 +26,7 @@ export function Tabs({ activeKey, onChange, children, className }: TabsProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TabListProps {
|
export interface TabListProps {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
@@ -37,38 +37,44 @@ export function TabList({ children, className }: TabListProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TabProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
export interface TabProps extends React.ButtonHTMLAttributes<HTMLDivElement> {
|
||||||
value: string
|
value: string
|
||||||
className?: string
|
className?: string
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
|
isSelected?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Tab = forwardRef<HTMLButtonElement, TabProps>(
|
export const Tab = forwardRef<HTMLDivElement, TabProps>(
|
||||||
({ value, className, children, ...props }, ref) => {
|
({ className, isSelected, value, ...props }, ref) => {
|
||||||
const { activeKey, onChange } = useContext(TabsContext)
|
const { onChange } = useContext(TabsContext)
|
||||||
|
|
||||||
|
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
onChange(value)
|
||||||
|
props.onClick?.(e)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onClick={() => onChange(value)}
|
role="tab"
|
||||||
|
aria-selected={isSelected}
|
||||||
|
onClick={handleClick}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'px-4 py-2 rounded-t transition-colors',
|
'px-2 py-1 flex items-center gap-1 text-sm font-medium border-b-2 transition-colors',
|
||||||
activeKey === value
|
isSelected
|
||||||
? 'bg-primary text-white'
|
? 'border-danger text-danger'
|
||||||
: 'hover:bg-default-100',
|
: 'border-transparent hover:border-default',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
/>
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
Tab.displayName = 'Tab'
|
Tab.displayName = 'Tab'
|
||||||
|
|
||||||
interface TabPanelProps {
|
export interface TabPanelProps {
|
||||||
value: string
|
value: string
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
className?: string
|
className?: string
|
||||||
|
38
napcat.webui/src/components/tabs/sortable_tab.tsx
Normal file
38
napcat.webui/src/components/tabs/sortable_tab.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { useSortable } from '@dnd-kit/sortable'
|
||||||
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
|
|
||||||
|
import { Tab } from '@/components/tabs'
|
||||||
|
import type { TabProps } from '@/components/tabs'
|
||||||
|
|
||||||
|
interface SortableTabProps extends TabProps {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SortableTab({ id, ...props }: SortableTabProps) {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging
|
||||||
|
} = useSortable({ id })
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
zIndex: isDragging ? 1 : 0,
|
||||||
|
position: 'relative' as const,
|
||||||
|
touchAction: 'none'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tab
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
import WebUIManager from '@/controllers/webui_manager'
|
import TerminalManager from '@/controllers/terminal_manager'
|
||||||
|
|
||||||
import XTerm, { XTermRef } from '../xterm'
|
import XTerm, { XTermRef } from '../xterm'
|
||||||
|
|
||||||
@@ -10,48 +10,29 @@ interface TerminalInstanceProps {
|
|||||||
|
|
||||||
export function TerminalInstance({ id }: TerminalInstanceProps) {
|
export function TerminalInstance({ id }: TerminalInstanceProps) {
|
||||||
const termRef = useRef<XTermRef>(null)
|
const termRef = useRef<XTermRef>(null)
|
||||||
const wsRef = useRef<WebSocket>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const ws = WebUIManager.connectTerminal(id, (data) => {
|
const handleData = (data: string) => {
|
||||||
termRef.current?.write(data)
|
try {
|
||||||
})
|
const parsed = JSON.parse(data)
|
||||||
wsRef.current = ws
|
if (parsed.data) {
|
||||||
|
termRef.current?.write(parsed.data)
|
||||||
// 添加连接状态监听
|
}
|
||||||
ws.onopen = () => {
|
} catch (e) {
|
||||||
console.log('Terminal connected:', id)
|
termRef.current?.write(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
TerminalManager.connectTerminal(id, handleData)
|
||||||
console.error('Terminal connection error:', error)
|
|
||||||
termRef.current?.write(
|
|
||||||
'\r\n\x1b[31mConnection error. Please try reconnecting.\x1b[0m\r\n'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ws.onclose = () => {
|
|
||||||
console.log('Terminal disconnected:', id)
|
|
||||||
termRef.current?.write('\r\n\x1b[31mConnection closed.\x1b[0m\r\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
ws.close()
|
TerminalManager.disconnectTerminal(id, handleData)
|
||||||
}
|
}
|
||||||
}, [id])
|
}, [id])
|
||||||
|
|
||||||
const handleInput = (data: string) => {
|
const handleInput = (data: string) => {
|
||||||
const ws = wsRef.current
|
TerminalManager.sendInput(id, data)
|
||||||
if (ws?.readyState === WebSocket.OPEN) {
|
|
||||||
try {
|
|
||||||
ws.send(JSON.stringify({ type: 'input', data }))
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to send terminal input:', error)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn('WebSocket is not in OPEN state')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <XTerm ref={termRef} onInput={handleInput} className="h-full" />
|
return <XTerm ref={termRef} onInput={handleInput} className="w-full h-full" />
|
||||||
}
|
}
|
||||||
|
@@ -25,12 +25,13 @@ export type XTermRef = {
|
|||||||
export interface XTermProps
|
export interface XTermProps
|
||||||
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onInput'> {
|
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onInput'> {
|
||||||
onInput?: (data: string) => void
|
onInput?: (data: string) => void
|
||||||
|
onKey?: (key: string, event: KeyboardEvent) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const XTerm = forwardRef<XTermRef, XTermProps>((props, ref) => {
|
const XTerm = forwardRef<XTermRef, XTermProps>((props, ref) => {
|
||||||
const domRef = useRef<HTMLDivElement>(null)
|
const domRef = useRef<HTMLDivElement>(null)
|
||||||
const terminalRef = useRef<Terminal | null>(null)
|
const terminalRef = useRef<Terminal | null>(null)
|
||||||
const { className, onInput, ...rest } = props
|
const { className, onInput, onKey, ...rest } = props
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!domRef.current) {
|
if (!domRef.current) {
|
||||||
@@ -40,9 +41,7 @@ const XTerm = forwardRef<XTermRef, XTermProps>((props, ref) => {
|
|||||||
allowTransparency: true,
|
allowTransparency: true,
|
||||||
fontFamily: '"Fira Code", "Harmony", "Noto Serif SC", monospace',
|
fontFamily: '"Fira Code", "Harmony", "Noto Serif SC", monospace',
|
||||||
cursorInactiveStyle: 'outline',
|
cursorInactiveStyle: 'outline',
|
||||||
drawBoldTextInBrightColors: false,
|
drawBoldTextInBrightColors: false
|
||||||
letterSpacing: 0,
|
|
||||||
lineHeight: 1.0
|
|
||||||
})
|
})
|
||||||
terminalRef.current = terminal
|
terminalRef.current = terminal
|
||||||
const fitAddon = new FitAddon()
|
const fitAddon = new FitAddon()
|
||||||
@@ -74,6 +73,12 @@ const XTerm = forwardRef<XTermRef, XTermProps>((props, ref) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
terminal.onKey((event) => {
|
||||||
|
if (onKey) {
|
||||||
|
onKey(event.key, event.domEvent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
fitAddon.fit()
|
fitAddon.fit()
|
||||||
})
|
})
|
||||||
|
88
napcat.webui/src/controllers/terminal_manager.ts
Normal file
88
napcat.webui/src/controllers/terminal_manager.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
type TerminalCallback = (data: string) => void
|
||||||
|
|
||||||
|
interface TerminalConnection {
|
||||||
|
ws: WebSocket
|
||||||
|
callbacks: Set<TerminalCallback>
|
||||||
|
isConnected: boolean
|
||||||
|
buffer: string[] // 添加缓存数组
|
||||||
|
}
|
||||||
|
|
||||||
|
class TerminalManager {
|
||||||
|
private connections: Map<string, TerminalConnection> = new Map()
|
||||||
|
private readonly MAX_BUFFER_SIZE = 1000 // 限制缓存大小
|
||||||
|
|
||||||
|
connectTerminal(id: string, callback: TerminalCallback): WebSocket {
|
||||||
|
let conn = this.connections.get(id)
|
||||||
|
|
||||||
|
if (!conn) {
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
url.protocol = url.protocol.replace('http', 'ws')
|
||||||
|
url.pathname = `/api/ws/terminal`
|
||||||
|
url.searchParams.set('id', id)
|
||||||
|
const token = JSON.parse(localStorage.getItem('token') || '')
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('No token found')
|
||||||
|
}
|
||||||
|
url.searchParams.set('token', token)
|
||||||
|
const ws = new WebSocket(url.toString())
|
||||||
|
conn = {
|
||||||
|
ws,
|
||||||
|
callbacks: new Set([callback]),
|
||||||
|
isConnected: false,
|
||||||
|
buffer: [] // 初始化缓存
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const data = event.data
|
||||||
|
// 保存到缓存
|
||||||
|
conn?.buffer.push(data)
|
||||||
|
if ((conn?.buffer.length ?? 0) > this.MAX_BUFFER_SIZE) {
|
||||||
|
conn?.buffer.shift()
|
||||||
|
}
|
||||||
|
conn?.callbacks.forEach((cb) => cb(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
if (conn) conn.isConnected = true
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
if (conn) conn.isConnected = false
|
||||||
|
}
|
||||||
|
|
||||||
|
this.connections.set(id, conn)
|
||||||
|
} else {
|
||||||
|
conn.callbacks.add(callback)
|
||||||
|
// 恢复历史内容
|
||||||
|
conn.buffer.forEach((data) => callback(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn.ws
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectTerminal(id: string, callback: TerminalCallback) {
|
||||||
|
const conn = this.connections.get(id)
|
||||||
|
if (!conn) return
|
||||||
|
|
||||||
|
conn.callbacks.delete(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTerminal(id: string) {
|
||||||
|
const conn = this.connections.get(id)
|
||||||
|
if (conn?.ws.readyState === WebSocket.OPEN) {
|
||||||
|
conn.ws.close()
|
||||||
|
}
|
||||||
|
this.connections.delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
sendInput(id: string, data: string) {
|
||||||
|
const conn = this.connections.get(id)
|
||||||
|
if (conn?.ws.readyState === WebSocket.OPEN) {
|
||||||
|
conn.ws.send(JSON.stringify({ type: 'input', data }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const terminalManager = new TerminalManager()
|
||||||
|
|
||||||
|
export default terminalManager
|
@@ -150,41 +150,6 @@ export default class WebUIManager {
|
|||||||
return data.data
|
return data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async sendTerminalInput(
|
|
||||||
id: string,
|
|
||||||
input: string
|
|
||||||
): Promise<void> {
|
|
||||||
await serverRequest.post(`/Log/terminal/${id}/input`, { input })
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getTerminalStream(id: string, onData: (data: string) => void) {
|
|
||||||
const token = localStorage.getItem('token')
|
|
||||||
if (!token) throw new Error('未登录')
|
|
||||||
|
|
||||||
const _token = JSON.parse(token)
|
|
||||||
const eventSource = new EventSourcePolyfill(
|
|
||||||
`/api/Log/terminal/${id}/stream`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${_token}`,
|
|
||||||
Accept: 'text/event-stream'
|
|
||||||
},
|
|
||||||
withCredentials: true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
eventSource.onmessage = (event) => {
|
|
||||||
try {
|
|
||||||
const { data } = JSON.parse(event.data)
|
|
||||||
onData(data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return eventSource
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async closeTerminal(id: string): Promise<void> {
|
public static async closeTerminal(id: string): Promise<void> {
|
||||||
await serverRequest.post(`/Log/terminal/${id}/close`)
|
await serverRequest.post(`/Log/terminal/${id}/close`)
|
||||||
}
|
}
|
||||||
@@ -205,9 +170,15 @@ export default class WebUIManager {
|
|||||||
if (!token) throw new Error('未登录')
|
if (!token) throw new Error('未登录')
|
||||||
|
|
||||||
const _token = JSON.parse(token)
|
const _token = JSON.parse(token)
|
||||||
const ws = new WebSocket(
|
|
||||||
`ws://${window.location.host}/api/ws/terminal?id=${id}&token=${_token}`
|
const url = new URL(window.location.origin)
|
||||||
)
|
url.protocol = "ws://"
|
||||||
|
url.pathname = "/api/ws/terminal"
|
||||||
|
url.searchParams.set('token', _token)
|
||||||
|
url.searchParams.set("id", id)
|
||||||
|
console.log(url.toString())
|
||||||
|
|
||||||
|
const ws = new WebSocket(url.toString())
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
|
@@ -1,4 +1,11 @@
|
|||||||
import { DndContext, DragEndEvent, closestCenter } from '@dnd-kit/core'
|
import {
|
||||||
|
DndContext,
|
||||||
|
DragEndEvent,
|
||||||
|
PointerSensor,
|
||||||
|
closestCenter,
|
||||||
|
useSensor,
|
||||||
|
useSensors
|
||||||
|
} from '@dnd-kit/core'
|
||||||
import {
|
import {
|
||||||
SortableContext,
|
SortableContext,
|
||||||
arrayMove,
|
arrayMove,
|
||||||
@@ -9,10 +16,11 @@ import { useEffect, useState } from 'react'
|
|||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { IoAdd, IoClose } from 'react-icons/io5'
|
import { IoAdd, IoClose } from 'react-icons/io5'
|
||||||
|
|
||||||
import { SortableTab } from '@/components/sortable_tab'
|
|
||||||
import { TabList, TabPanel, Tabs } from '@/components/tabs'
|
import { TabList, TabPanel, Tabs } from '@/components/tabs'
|
||||||
|
import { SortableTab } from '@/components/tabs/sortable_tab.tsx'
|
||||||
import { TerminalInstance } from '@/components/terminal/terminal-instance'
|
import { TerminalInstance } from '@/components/terminal/terminal-instance'
|
||||||
|
|
||||||
|
import terminalManager from '@/controllers/terminal_manager'
|
||||||
import WebUIManager from '@/controllers/webui_manager'
|
import WebUIManager from '@/controllers/webui_manager'
|
||||||
|
|
||||||
interface TerminalTab {
|
interface TerminalTab {
|
||||||
@@ -29,9 +37,9 @@ export default function TerminalPage() {
|
|||||||
WebUIManager.getTerminalList().then((terminals) => {
|
WebUIManager.getTerminalList().then((terminals) => {
|
||||||
if (terminals.length === 0) return
|
if (terminals.length === 0) return
|
||||||
|
|
||||||
const newTabs = terminals.map((terminal, index) => ({
|
const newTabs = terminals.map((terminal) => ({
|
||||||
id: terminal.id,
|
id: terminal.id,
|
||||||
title: `Terminal ${index + 1}`
|
title: terminal.id
|
||||||
}))
|
}))
|
||||||
|
|
||||||
setTabs(newTabs)
|
setTabs(newTabs)
|
||||||
@@ -44,7 +52,7 @@ export default function TerminalPage() {
|
|||||||
const { id } = await WebUIManager.createTerminal(80, 24)
|
const { id } = await WebUIManager.createTerminal(80, 24)
|
||||||
const newTab = {
|
const newTab = {
|
||||||
id,
|
id,
|
||||||
title: `Terminal ${tabs.length + 1}`
|
title: id
|
||||||
}
|
}
|
||||||
|
|
||||||
setTabs((prev) => [...prev, newTab])
|
setTabs((prev) => [...prev, newTab])
|
||||||
@@ -58,10 +66,16 @@ export default function TerminalPage() {
|
|||||||
const closeTerminal = async (id: string) => {
|
const closeTerminal = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
await WebUIManager.closeTerminal(id)
|
await WebUIManager.closeTerminal(id)
|
||||||
setTabs((prev) => prev.filter((tab) => tab.id !== id))
|
terminalManager.removeTerminal(id)
|
||||||
if (selectedTab === id) {
|
if (selectedTab === id) {
|
||||||
setSelectedTab(tabs[0]?.id || '')
|
const previousIndex = tabs.findIndex((tab) => tab.id === id) - 1
|
||||||
|
if (previousIndex >= 0) {
|
||||||
|
setSelectedTab(tabs[previousIndex].id)
|
||||||
|
} else {
|
||||||
|
setSelectedTab(tabs[0]?.id || '')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
setTabs((prev) => prev.filter((tab) => tab.id !== id))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('关闭终端失败')
|
toast.error('关闭终端失败')
|
||||||
}
|
}
|
||||||
@@ -78,25 +92,49 @@ export default function TerminalPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(PointerSensor, {
|
||||||
|
activationConstraint: {
|
||||||
|
distance: 8
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full gap-2 p-4">
|
<div className="flex flex-col gap-2 p-4">
|
||||||
<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
<DndContext
|
||||||
<Tabs activeKey={selectedTab} onChange={setSelectedTab}>
|
sensors={sensors}
|
||||||
<div className="flex items-center gap-2">
|
collisionDetection={closestCenter}
|
||||||
<TabList className="flex-1">
|
onDragEnd={handleDragEnd}
|
||||||
|
>
|
||||||
|
<Tabs
|
||||||
|
activeKey={selectedTab}
|
||||||
|
onChange={setSelectedTab}
|
||||||
|
className="h-full overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 flex-shrink-0 flex-grow-0">
|
||||||
|
<TabList className="flex-1 !overflow-x-auto hide-scrollbar">
|
||||||
<SortableContext
|
<SortableContext
|
||||||
items={tabs}
|
items={tabs}
|
||||||
strategy={horizontalListSortingStrategy}
|
strategy={horizontalListSortingStrategy}
|
||||||
>
|
>
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<SortableTab key={tab.id} id={tab.id} value={tab.id}>
|
<SortableTab
|
||||||
|
key={tab.id}
|
||||||
|
id={tab.id}
|
||||||
|
value={tab.id}
|
||||||
|
isSelected={selectedTab === tab.id}
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
>
|
||||||
{tab.title}
|
{tab.title}
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
|
radius="full"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="ml-2"
|
className="min-w-0 w-4 h-4"
|
||||||
onPress={() => closeTerminal(tab.id)}
|
onPress={() => closeTerminal(tab.id)}
|
||||||
|
color={selectedTab === tab.id ? 'danger' : 'default'}
|
||||||
>
|
>
|
||||||
<IoClose />
|
<IoClose />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -106,15 +144,21 @@ export default function TerminalPage() {
|
|||||||
</TabList>
|
</TabList>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
|
color="danger"
|
||||||
|
size="sm"
|
||||||
|
variant="flat"
|
||||||
onPress={createNewTerminal}
|
onPress={createNewTerminal}
|
||||||
startContent={<IoAdd />}
|
startContent={<IoAdd />}
|
||||||
|
className="text-xl"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{tabs.map((tab) => (
|
<div className="flex-grow overflow-hidden">
|
||||||
<TabPanel key={tab.id} value={tab.id} className="flex-1">
|
{tabs.map((tab) => (
|
||||||
<TerminalInstance id={tab.id} />
|
<TabPanel key={tab.id} value={tab.id} className="h-full">
|
||||||
</TabPanel>
|
<TerminalInstance id={tab.id} />
|
||||||
))}
|
</TabPanel>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -35,6 +35,20 @@ body {
|
|||||||
.font-noto-serif {
|
.font-noto-serif {
|
||||||
font-family: 'Noto Serif SC', serif;
|
font-family: 'Noto Serif SC', serif;
|
||||||
}
|
}
|
||||||
|
.hide-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
|
}
|
||||||
|
.hide-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
width: 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
.hide-scrollbar::-webkit-scrollbar-track {
|
||||||
|
width: 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
|
@@ -29,6 +29,11 @@ export default defineConfig(({ mode }) => {
|
|||||||
base: '/webui/',
|
base: '/webui/',
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
|
'/api/ws/terminal': {
|
||||||
|
target: backendDebugUrl,
|
||||||
|
ws: true,
|
||||||
|
changeOrigin: true
|
||||||
|
},
|
||||||
'/api': backendDebugUrl
|
'/api': backendDebugUrl
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -17,18 +17,16 @@
|
|||||||
"dev:depend": "npm i && cd napcat.webui && npm i"
|
"dev:depend": "npm i && cd napcat.webui && npm i"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"json5": "^2.2.3",
|
|
||||||
"esbuild": "0.24.0",
|
|
||||||
"@babel/preset-typescript": "^7.24.7",
|
"@babel/preset-typescript": "^7.24.7",
|
||||||
"@eslint/compat": "^1.2.2",
|
"@eslint/compat": "^1.2.2",
|
||||||
"@eslint/eslintrc": "^3.1.0",
|
"@eslint/eslintrc": "^3.1.0",
|
||||||
"@eslint/js": "^9.14.0",
|
"@eslint/js": "^9.14.0",
|
||||||
"@log4js-node/log4js-api": "^1.0.2",
|
"@log4js-node/log4js-api": "^1.0.2",
|
||||||
"@napneko/nap-proto-core": "^0.0.4",
|
"@napneko/nap-proto-core": "^0.0.4",
|
||||||
"@rollup/plugin-typescript": "^12.1.2",
|
|
||||||
"@rollup/plugin-node-resolve": "^16.0.0",
|
"@rollup/plugin-node-resolve": "^16.0.0",
|
||||||
"@types/cors": "^2.8.17",
|
"@rollup/plugin-typescript": "^12.1.2",
|
||||||
"@sinclair/typebox": "^0.34.9",
|
"@sinclair/typebox": "^0.34.9",
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/node": "^22.0.1",
|
"@types/node": "^22.0.1",
|
||||||
"@types/qrcode-terminal": "^0.12.2",
|
"@types/qrcode-terminal": "^0.12.2",
|
||||||
@@ -39,6 +37,7 @@
|
|||||||
"async-mutex": "^0.5.0",
|
"async-mutex": "^0.5.0",
|
||||||
"commander": "^13.0.0",
|
"commander": "^13.0.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"esbuild": "0.24.0",
|
||||||
"eslint": "^9.14.0",
|
"eslint": "^9.14.0",
|
||||||
"eslint-import-resolver-typescript": "^3.6.1",
|
"eslint-import-resolver-typescript": "^3.6.1",
|
||||||
"eslint-plugin-import": "^2.29.1",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
@@ -46,6 +45,7 @@
|
|||||||
"file-type": "^20.0.0",
|
"file-type": "^20.0.0",
|
||||||
"globals": "^15.12.0",
|
"globals": "^15.12.0",
|
||||||
"image-size": "^1.1.1",
|
"image-size": "^1.1.1",
|
||||||
|
"json5": "^2.2.3",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"typescript-eslint": "^8.13.0",
|
"typescript-eslint": "^8.13.0",
|
||||||
"vite": "^6.0.1",
|
"vite": "^6.0.1",
|
||||||
@@ -57,6 +57,7 @@
|
|||||||
"@ffmpeg.wasm/core-mt": "^0.13.2",
|
"@ffmpeg.wasm/core-mt": "^0.13.2",
|
||||||
"@ffmpeg.wasm/main": "^0.13.1",
|
"@ffmpeg.wasm/main": "^0.13.1",
|
||||||
"express": "^5.0.0",
|
"express": "^5.0.0",
|
||||||
|
"node-pty": "^1.1.0-beta22",
|
||||||
"piscina": "^4.7.0",
|
"piscina": "^4.7.0",
|
||||||
"qrcode-terminal": "^0.12.0",
|
"qrcode-terminal": "^0.12.0",
|
||||||
"silk-wasm": "^3.6.1",
|
"silk-wasm": "^3.6.1",
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
import { createServer } from 'http';
|
||||||
import { LogWrapper } from '@/common/log';
|
import { LogWrapper } from '@/common/log';
|
||||||
import { NapCatPathWrapper } from '@/common/path';
|
import { NapCatPathWrapper } from '@/common/path';
|
||||||
import { WebUiConfigWrapper } from '@webapi/helper/config';
|
import { WebUiConfigWrapper } from '@webapi/helper/config';
|
||||||
@@ -15,7 +16,7 @@ import { terminalManager } from '@webapi/terminal/terminal_manager';
|
|||||||
|
|
||||||
// 实例化Express
|
// 实例化Express
|
||||||
const app = express();
|
const app = express();
|
||||||
|
const server = createServer(app);
|
||||||
/**
|
/**
|
||||||
* 初始化并启动WebUI服务。
|
* 初始化并启动WebUI服务。
|
||||||
* 该函数配置了Express服务器以支持JSON解析和静态文件服务,并监听6099端口。
|
* 该函数配置了Express服务器以支持JSON解析和静态文件服务,并监听6099端口。
|
||||||
@@ -47,7 +48,9 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
|
|||||||
// 挂载静态路由(前端),路径为 [/前缀]/webui
|
// 挂载静态路由(前端),路径为 [/前缀]/webui
|
||||||
app.use('/webui', express.static(pathWrapper.staticPath));
|
app.use('/webui', express.static(pathWrapper.staticPath));
|
||||||
// 初始化WebSocket服务器
|
// 初始化WebSocket服务器
|
||||||
terminalManager.initialize(app);
|
server.on('upgrade', (request, socket, head) => {
|
||||||
|
terminalManager.initialize(request, socket, head, logger);
|
||||||
|
});
|
||||||
// 挂载API接口
|
// 挂载API接口
|
||||||
app.use('/api', ALLRouter);
|
app.use('/api', ALLRouter);
|
||||||
// 所有剩下的请求都转到静态页面
|
// 所有剩下的请求都转到静态页面
|
||||||
@@ -64,7 +67,7 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
|
|||||||
// ------------路由挂载结束------------
|
// ------------路由挂载结束------------
|
||||||
|
|
||||||
// ------------启动服务------------
|
// ------------启动服务------------
|
||||||
app.listen(config.port, config.host, async () => {
|
server.listen(config.port, config.host, async () => {
|
||||||
// 启动后打印出相关地址
|
// 启动后打印出相关地址
|
||||||
const port = config.port.toString(),
|
const port = config.port.toString(),
|
||||||
searchParams = { token: config.token };
|
searchParams = { token: config.token };
|
||||||
|
@@ -1,54 +1,64 @@
|
|||||||
import { WebUiConfig } from '@/webui';
|
import { WebUiConfig } from '@/webui';
|
||||||
import { AuthHelper } from '../helper/SignToken';
|
import { AuthHelper } from '../helper/SignToken';
|
||||||
import { spawn, type ChildProcess } from 'child_process';
|
import { LogWrapper } from '@/common/log';
|
||||||
import * as os from 'os';
|
|
||||||
import { WebSocket, WebSocketServer } from 'ws';
|
import { WebSocket, WebSocketServer } from 'ws';
|
||||||
|
import os from 'os';
|
||||||
|
import { spawn, ChildProcess } from 'child_process';
|
||||||
|
import { IPty, spawn as ptySpawn } from 'node-pty';
|
||||||
|
|
||||||
interface TerminalInstance {
|
interface TerminalInstance {
|
||||||
process: ChildProcess;
|
pty: IPty; // 改用 PTY 实例
|
||||||
lastAccess: number;
|
lastAccess: number;
|
||||||
dataHandlers: Set<(data: string) => void>;
|
sockets: Set<WebSocket>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class TerminalManager {
|
class TerminalManager {
|
||||||
private terminals: Map<string, TerminalInstance> = new Map();
|
private terminals: Map<string, TerminalInstance> = new Map();
|
||||||
private wss: WebSocketServer | null = null;
|
private wss: WebSocketServer | null = null;
|
||||||
|
|
||||||
initialize(server: any) {
|
initialize(req: any, socket: any, head: any, logger?: LogWrapper) {
|
||||||
|
logger?.log('[NapCat] [WebUi] terminal websocket initialized');
|
||||||
this.wss = new WebSocketServer({
|
this.wss = new WebSocketServer({
|
||||||
server,
|
noServer: true,
|
||||||
path: '/api/ws/terminal',
|
verifyClient: async (info, cb) => {
|
||||||
});
|
// 验证 token
|
||||||
|
const url = new URL(info.req.url || '', 'ws://localhost');
|
||||||
this.wss.on('connection', async (ws, req) => {
|
|
||||||
try {
|
|
||||||
const url = new URL(req.url || '', 'ws://localhost');
|
|
||||||
const token = url.searchParams.get('token');
|
const token = url.searchParams.get('token');
|
||||||
const terminalId = url.searchParams.get('id');
|
const terminalId = url.searchParams.get('id');
|
||||||
|
|
||||||
if (!token || !terminalId) {
|
if (!token || !terminalId) {
|
||||||
ws.close();
|
cb(false, 401, 'Unauthorized');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证 token
|
|
||||||
// 解析token
|
// 解析token
|
||||||
let Credential: WebUiCredentialJson;
|
let Credential: WebUiCredentialJson;
|
||||||
try {
|
try {
|
||||||
Credential = JSON.parse(Buffer.from(token, 'base64').toString('utf-8'));
|
Credential = JSON.parse(Buffer.from(token, 'base64').toString('utf-8'));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ws.close();
|
cb(false, 401, 'Unauthorized');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const config = await WebUiConfig.GetWebUIConfig();
|
const config = await WebUiConfig.GetWebUIConfig();
|
||||||
const validate = AuthHelper.validateCredentialWithinOneHour(config.token, Credential);
|
const validate = AuthHelper.validateCredentialWithinOneHour(config.token, Credential);
|
||||||
|
|
||||||
if (!validate) {
|
if (!validate) {
|
||||||
ws.close();
|
cb(false, 401, 'Unauthorized');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
cb(true);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.wss.handleUpgrade(req, socket, head, (ws) => {
|
||||||
|
this.wss?.emit('connection', ws, req);
|
||||||
|
});
|
||||||
|
this.wss.on('connection', async (ws, req) => {
|
||||||
|
logger?.log('建立终端连接');
|
||||||
|
try {
|
||||||
|
const url = new URL(req.url || '', 'ws://localhost');
|
||||||
|
const terminalId = url.searchParams.get('id')!;
|
||||||
|
|
||||||
const instance = this.terminals.get(terminalId);
|
const instance = this.terminals.get(terminalId);
|
||||||
|
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
ws.close();
|
ws.close();
|
||||||
return;
|
return;
|
||||||
@@ -59,21 +69,24 @@ class TerminalManager {
|
|||||||
ws.send(JSON.stringify({ type: 'output', data }));
|
ws.send(JSON.stringify({ type: 'output', data }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
instance.dataHandlers.add(dataHandler);
|
|
||||||
|
|
||||||
ws.on('message', (message) => {
|
instance.sockets.add(ws);
|
||||||
try {
|
instance.lastAccess = Date.now();
|
||||||
const data = JSON.parse(message.toString());
|
|
||||||
if (data.type === 'input') {
|
ws.on('message', (data) => {
|
||||||
this.writeTerminal(terminalId, data.data);
|
if (instance) {
|
||||||
|
const result = JSON.parse(data.toString());
|
||||||
|
if (result.type === 'input') {
|
||||||
|
instance.pty.write(result.data);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to process terminal input:', error);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on('close', () => {
|
ws.on('close', () => {
|
||||||
instance.dataHandlers.delete(dataHandler);
|
instance.sockets.delete(ws);
|
||||||
|
if (instance.sockets.size === 0) {
|
||||||
|
instance.pty.kill();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('WebSocket authentication failed:', err);
|
console.error('WebSocket authentication failed:', err);
|
||||||
@@ -84,64 +97,52 @@ class TerminalManager {
|
|||||||
|
|
||||||
createTerminal(id: string) {
|
createTerminal(id: string) {
|
||||||
const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
|
const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
|
||||||
const shellProcess = spawn(shell, [], {
|
const pty = ptySpawn(shell, [], {
|
||||||
env: process.env,
|
name: 'xterm-256color',
|
||||||
shell: true,
|
cols: 80,
|
||||||
|
rows: 24,
|
||||||
|
cwd: process.cwd(),
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
// 统一编码设置
|
||||||
|
LANG: os.platform() === 'win32' ? 'chcp 65001' : 'zh_CN.UTF-8',
|
||||||
|
TERM: 'xterm-256color',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const instance: TerminalInstance = {
|
const instance: TerminalInstance = {
|
||||||
process: shellProcess,
|
pty,
|
||||||
lastAccess: Date.now(),
|
lastAccess: Date.now(),
|
||||||
dataHandlers: new Set(),
|
sockets: new Set(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 修改这里,使用 shellProcess 而不是 process
|
pty.onData((data: any) => {
|
||||||
shellProcess.stdout.on('data', (data) => {
|
instance.sockets.forEach((ws) => {
|
||||||
const str = data.toString();
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
instance.dataHandlers.forEach((handler) => handler(str));
|
ws.send(JSON.stringify({ type: 'output', data }));
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
shellProcess.stderr.on('data', (data) => {
|
pty.onExit(() => {
|
||||||
const str = data.toString();
|
this.closeTerminal(id);
|
||||||
instance.dataHandlers.forEach((handler) => handler(str));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.terminals.set(id, instance);
|
this.terminals.set(id, instance);
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTerminal(id: string) {
|
|
||||||
return this.terminals.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
closeTerminal(id: string) {
|
closeTerminal(id: string) {
|
||||||
const instance = this.terminals.get(id);
|
const instance = this.terminals.get(id);
|
||||||
if (instance) {
|
if (instance) {
|
||||||
instance.process.kill();
|
instance.pty.kill();
|
||||||
|
instance.sockets.forEach((ws) => ws.close());
|
||||||
this.terminals.delete(id);
|
this.terminals.delete(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onTerminalData(id: string, handler: (data: string) => void) {
|
getTerminal(id: string) {
|
||||||
const instance = this.terminals.get(id);
|
return this.terminals.get(id);
|
||||||
if (instance) {
|
|
||||||
instance.dataHandlers.add(handler);
|
|
||||||
return () => {
|
|
||||||
instance.dataHandlers.delete(handler);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return () => {};
|
|
||||||
}
|
|
||||||
|
|
||||||
writeTerminal(id: string, data: string) {
|
|
||||||
const instance = this.terminals.get(id);
|
|
||||||
if (instance && instance.process.stdin) {
|
|
||||||
instance.process.stdin.write(data, (error) => {
|
|
||||||
if (error) {
|
|
||||||
console.error('Failed to write to terminal:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getTerminalList() {
|
getTerminalList() {
|
||||||
|
@@ -4,7 +4,16 @@ import { resolve } from 'path';
|
|||||||
import nodeResolve from '@rollup/plugin-node-resolve';
|
import nodeResolve from '@rollup/plugin-node-resolve';
|
||||||
import { builtinModules } from 'module';
|
import { builtinModules } from 'module';
|
||||||
//依赖排除
|
//依赖排除
|
||||||
const external = ['silk-wasm', 'ws', 'express', 'qrcode-terminal', 'piscina', '@ffmpeg.wasm/core-mt', "@ffmpeg.wasm/main"];
|
const external = [
|
||||||
|
'silk-wasm',
|
||||||
|
'ws',
|
||||||
|
'express',
|
||||||
|
'qrcode-terminal',
|
||||||
|
'piscina',
|
||||||
|
'@ffmpeg.wasm/core-mt',
|
||||||
|
'@ffmpeg.wasm/main',
|
||||||
|
'node-pty',
|
||||||
|
];
|
||||||
const nodeModules = [...builtinModules, builtinModules.map((m) => `node:${m}`)].flat();
|
const nodeModules = [...builtinModules, builtinModules.map((m) => `node:${m}`)].flat();
|
||||||
|
|
||||||
let startScripts: string[] | undefined = undefined;
|
let startScripts: string[] | undefined = undefined;
|
||||||
@@ -56,7 +65,6 @@ const FrameworkBaseConfigPlugin: PluginOption[] = [
|
|||||||
nodeResolve(),
|
nodeResolve(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
const ShellBaseConfigPlugin: PluginOption[] = [
|
const ShellBaseConfigPlugin: PluginOption[] = [
|
||||||
cp({
|
cp({
|
||||||
targets: [
|
targets: [
|
||||||
@@ -101,7 +109,6 @@ const UniversalBaseConfig = () =>
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const ShellBaseConfig = () =>
|
const ShellBaseConfig = () =>
|
||||||
defineConfig({
|
defineConfig({
|
||||||
resolve: {
|
resolve: {
|
||||||
|
Reference in New Issue
Block a user