feat: Pro 买断门禁、SFTP 精简与批量多机改造

新增 entitlement/license IPC 与 Waffo 一次性结账服务骨架,挂载 AI/转发/代理/串口/广播/批量导入导出门禁;批量执行改为结果矩阵并复用连接池,SFTP 工具栏图标化并去掉已连接条,同时完善跨平台本地终端与窗口配置。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Logan.LiandCursor committed 2026-08-07 11:38:30 +08:00
1 parent 101d2164b4
commit fea6d8db04
72 files changed
+3307 -396

No files matched your search

+13 -18
View File
@@ -7,7 +7,7 @@
import { useState, useEffect, useCallback, useRef } from 'react'
import { useSessionStore } from '../stores'
import { useTabStore, type ConnectionInstance } from '../stores/tabStore'
import { useConnectionPool, getSessionConfig } from '../stores/connection'
import { useConnectionPool, getConnectionStore, getSessionConfig } from '../stores/connection'
import { createLogger } from '../utils/logger'
import type { FileItem, TransferTask, SortField, SortOrder } from '../../shared/types/sftp.types'
import {
@@ -16,6 +16,7 @@ import {
remotePath as toRemotePath,
uploadRemoteFile,
} from '../platform/sftp'
import { joinLocalPath } from '../utils/localPath'
const logger = createLogger('useSFTP')
@@ -155,11 +156,6 @@ function fileNameFromPath(filePath: string): string {
return filePath.split(/[\\/]/).filter(Boolean).pop() || filePath
}
function joinLocalPath(directory: string, fileName: string): string {
const separator = directory.includes('\\') && !directory.includes('/') ? '\\' : '/'
return `${directory.replace(/[\\/]$/, '')}${separator}${fileName}`
}
export function useSFTP({ tabId }: UseSFTPOptions): UseSFTPReturn {
const { sessions } = useSessionStore()
const { updateTabConnectionInstance, updateTab } = useTabStore()
@@ -169,7 +165,7 @@ export function useSFTP({ tabId }: UseSFTPOptions): UseSFTPReturn {
const [localPath, setLocalPath] = useState('')
const [localFiles, setLocalFiles] = useState<FileItem[]>([])
const [localSelected, setLocalSelected] = useState<Set<string>>(new Set())
const [localHistory, setLocalHistory] = useState<string[]>(['/'])
const [localHistory, setLocalHistory] = useState<string[]>([])
const [localHistoryIndex, setLocalHistoryIndex] = useState(0)
// Remote state
@@ -246,7 +242,7 @@ export function useSFTP({ tabId }: UseSFTPOptions): UseSFTPReturn {
const type = localEntryType(f)
return {
name: f.name,
path: f.path || `${path}/${f.name}`.replace(/\/+/g, '/'),
path: f.path || joinLocalPath(path, f.name),
type,
size: f.size || 0,
modified: f.modified ?? f.mtime ?? Date.now() / 1000,
@@ -467,20 +463,19 @@ export function useSFTP({ tabId }: UseSFTPOptions): UseSFTPReturn {
// Disconnect
const disconnect = useCallback(async () => {
// Close SFTP session via SSH disconnect (SFTP is cleaned up when connection closes)
if (connectionId && window.opsxApi) {
if (connectionId) {
try {
await window.opsxApi.sshDisconnect(connectionId)
logger.info('SFTP connection closed', { connectionId, sftpId })
await getConnectionStore().releaseConnection(connectionId, tabId)
logger.info('SFTP connection released', { connectionId, sftpId })
} catch (err) {
logger.error('Error closing SFTP connection', err)
logger.error('Error releasing SFTP connection', err)
}
}
setIsConnected(false)
setSftpId(null)
setConnectionId(null)
setRemoteFiles([])
}, [connectionId, sftpId])
}, [connectionId, sftpId, tabId])
// Upload files (real implementation via IPC)
const uploadFiles = useCallback(
@@ -845,17 +840,17 @@ export function useSFTP({ tabId }: UseSFTPOptions): UseSFTPReturn {
[sftpId, remotePath, loadRemoteDirectory]
)
// Cleanup on unmount — use refs to avoid re-running when connectionId changes
// Cleanup on unmount — release pool ref instead of hard-disconnecting shared sessions
const connectionIdRef = useRef(connectionId)
connectionIdRef.current = connectionId
useEffect(() => {
return () => {
const cid = connectionIdRef.current
if (cid && window.opsxApi) {
window.opsxApi.sshDisconnect(cid).catch(() => {})
if (cid) {
void getConnectionStore().releaseConnection(cid, tabId)
}
}
}, [])
}, [tabId])
return {
sessions,