终端模块初版
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* SSH 主机表单的默认值与规整。
|
||||
*
|
||||
* 抽成独立模块而不是写在 `HostManager.vue` 里,是因为「新建主机的默认值」
|
||||
* 有两个使用场景:主机管理面板(`HostManager.vue`)与侧栏的快速新建入口。
|
||||
* 若各写一份,两处的默认端口、默认超时迟早会不一致——而这种不一致
|
||||
* 极难被发现,只会表现为「从这里建的主机连不上,从那里建的可以」。
|
||||
*
|
||||
* 默认值的选取依据:
|
||||
* - `port: 22` —— SSH 标准端口,覆盖绝大多数场景。
|
||||
* - `connectTimeoutMs: 15000` —— 跨公网 + 需要 DNS 解析时,10 秒常不够;
|
||||
* 30 秒又让用户在网络不通时等太久。15 秒是「能容忍慢链路,又不至于
|
||||
* 像卡死」的折中值。
|
||||
* - `keepaliveSecs: 30` —— 小于多数家用路由/NAT 的 60 秒空闲回收阈值,
|
||||
* 足以穿透常见 NAT 超时,同时心跳包本身开销可忽略。
|
||||
* - `encoding: 'utf-8'` —— 现代 Linux 发行版默认 locale;GBK 只留给老系统。
|
||||
*/
|
||||
|
||||
import type { SshHost } from '@/types/terminal'
|
||||
|
||||
export type { SshHost }
|
||||
|
||||
/**
|
||||
* 构造一个空白主机配置。
|
||||
*
|
||||
* `id` 由调用方提供 —— 必须来自后端 `terminal_new_host_id`,
|
||||
* 不能在前端自行生成。原因见 `HostManager.vue` 的文件头说明:
|
||||
* 密码在系统凭据管理器里按 hostId 存取,id 必须与后端规则一致。
|
||||
*/
|
||||
export function blankHost(id: string): SshHost {
|
||||
return {
|
||||
id,
|
||||
name: '',
|
||||
host: '',
|
||||
port: 22,
|
||||
username: '',
|
||||
authMethod: 'key',
|
||||
keyId: '',
|
||||
group: '',
|
||||
note: '',
|
||||
color: '',
|
||||
favorited: false,
|
||||
connectTimeoutMs: 15000,
|
||||
keepaliveSecs: 30,
|
||||
remoteCwd: '',
|
||||
startupCommand: '',
|
||||
useProxy: false,
|
||||
jumpIds: [],
|
||||
encoding: 'utf-8'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 补齐缺失字段的主机配置。
|
||||
*
|
||||
* 用于编辑历史配置:早先版本保存的 `SshHost` 可能缺少后来新增的字段
|
||||
* (如 `encoding`、`useProxy`)。直接绑到表单上,缺失字段会是 `undefined`,
|
||||
* 而 `<Input v-model.number>` 拿到 `undefined` 会显示成空串并可能在
|
||||
* 失焦时写回 `NaN`——反而把好配置改坏。这里统一补齐。
|
||||
*/
|
||||
export function normalizeHost(raw: Partial<SshHost>, id: string): SshHost {
|
||||
const base = blankHost(raw.id || id)
|
||||
return {
|
||||
...base,
|
||||
...raw,
|
||||
// 数值字段单独兜底:`NaN` 是合法 number,不会触发 `??` 的默认值,
|
||||
// 但塞进 `port` 会让后端校验失败,且表单上显示为空,用户看不出问题所在。
|
||||
port: Number.isFinite(raw.port) ? (raw.port as number) : base.port,
|
||||
connectTimeoutMs: Number.isFinite(raw.connectTimeoutMs)
|
||||
? (raw.connectTimeoutMs as number)
|
||||
: base.connectTimeoutMs,
|
||||
keepaliveSecs: Number.isFinite(raw.keepaliveSecs)
|
||||
? (raw.keepaliveSecs as number)
|
||||
: base.keepaliveSecs,
|
||||
authMethod: raw.authMethod || base.authMethod,
|
||||
// 数组字段单独兜底:旧配置没有该字段时是 undefined,直接展开会盖掉 base 的 []
|
||||
jumpIds: Array.isArray(raw.jumpIds) ? raw.jumpIds : base.jumpIds,
|
||||
encoding: raw.encoding || base.encoding
|
||||
}
|
||||
}
|
||||
|
||||
/** 认证方式的中文展示名 */
|
||||
export function authMethodLabel(method: string): string {
|
||||
switch (method) {
|
||||
case 'key':
|
||||
return '密钥认证'
|
||||
case 'password':
|
||||
return '密码认证'
|
||||
case 'agent':
|
||||
return 'SSH Agent'
|
||||
case 'keyboard':
|
||||
return '键盘交互'
|
||||
default:
|
||||
return method || '未设置'
|
||||
}
|
||||
}
|
||||
|
||||
/** 是否是可用的认证方式(P0 只实现 key 与 password) */
|
||||
export function isSupportedAuthMethod(method: string): boolean {
|
||||
return method === 'key' || method === 'password'
|
||||
}
|
||||
Reference in New Issue
Block a user