Files
Thing/src/modules/terminal/components/hostForm.ts
T
2026-09-18 18:28:13 +08:00

102 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* SSH 主机表单的默认值与规整。
*
* 抽成独立模块而不是写在 `HostManager.vue` 里,是因为「新建主机的默认值」
* 有两个使用场景:主机管理面板(`HostManager.vue`)与侧栏的快速新建入口。
* 若各写一份,两处的默认端口、默认超时迟早会不一致——而这种不一致
* 极难被发现,只会表现为「从这里建的主机连不上,从那里建的可以」。
*
* 默认值的选取依据:
* - `port: 22` —— SSH 标准端口,覆盖绝大多数场景。
* - `connectTimeoutMs: 15000` —— 跨公网 + 需要 DNS 解析时,10 秒常不够;
* 30 秒又让用户在网络不通时等太久。15 秒是「能容忍慢链路,又不至于
* 像卡死」的折中值。
* - `keepaliveSecs: 30` —— 小于多数家用路由/NAT 的 60 秒空闲回收阈值,
* 足以穿透常见 NAT 超时,同时心跳包本身开销可忽略。
* - `encoding: 'utf-8'` —— 现代 Linux 发行版默认 localeGBK 只留给老系统。
*/
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'
}