/** * 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`, * 而 `` 拿到 `undefined` 会显示成空串并可能在 * 失焦时写回 `NaN`——反而把好配置改坏。这里统一补齐。 */ export function normalizeHost(raw: Partial, 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' }