Orca SSH 配置主机选择器(Fill from ~/.ssh/config)E2E 测试方案:隔离 HOME 下验证真实解析与 ssh -G 表单预填
【免费下载链接】orcaOrca is the ADE for working with a fleet of parallel agents. Run any coding agent with your own subscription. Available on desktop, mobile and remote runtime.项目地址: https://gitcode.com/GitHub_Trending/orca48/orca
本篇指南围绕仓库内的 E2E 测试计划 ssh-config-host-picker.PLAN.md 展开,系统讲解 Orca 中“从
~/.ssh/config挑选 Host 预填添加 SSH 主机表单”这一产品能力应如何被端到端测试覆盖。你读完将掌握:为什么这类功能必须跑在“隔离 HOME + 真实 OpenSSHssh -G”之上、如何搭建可并行的 E2E Harness、P1–P9 九个必做用例与 N1–N3 三个锦上添花用例的断言要点,以及批量“Add all to Orca”与 Settings→SSH→Import 在删除墓碑(tombstone)语义上的本质区别。文中还结合src/main/ssh下真实实现,给出搜索、别名归一、ssh -G解析与-F隔离路径的源码级依据。
一、功能背景:让“添加 SSH 主机”从手填变成从配置里挑
在“新增远程主机”的入口上,用户通常会先问一个问题:~/.ssh/config里已经写好的 Host 别名,能不能直接导入 Orca,而不是在表单里把 HostName、User、Port 再抄一遍?
本仓库实现的答案分两层:
- 主路径:在Add SSH host表单对话框上提供
Fill from ~/.ssh/config…链接,打开一个“Choose from ~/.ssh/config”主机选择器。用户点选某个 Host 别名后,表单字段由ssh -G对该别名的真实解析结果预填,随后照常点 Save 持久化。 - 次要批量动作:选择器里提供Add all N to Orca,一次性把“尚未在 Orca 中”的配置主机批量建为 SSH Target。测试计划原文特别强调:该批量路径不做 reAdopt(重新收养),删除过的主机不会被悄悄复活。
- 对比路径:Settings → SSH →Import仍然是“显式重新收养”的地方,只有它才会把被删除的配置主机重新纳入 Orca。
换句话说,同一份~/.ssh/config,在“添加主机”入口里是增量挑选/增量导入(尊重用户的删除决定),在“设置”入口里则是全量再同步(用户主动表达“把配置里的都给我”)。这份 E2E 计划最核心的验证目标之一,就是把这两条语义用可见的 DOM 结果区分开。
计划中列出的被测提交(相对 main)如下:
| Commit | 说明 |
|---|---|
bd0b594dee | feat(ssh): 为 add-host 表单新增 SSH config host picker |
dc8339369a | fix(ssh): 保持导入筛选并补充 label 回退 |
5982c8108f | fix(ssh): 加固 config picker 导入、别名折叠与 host 定位 |
5cfb6369e7 | refactor(ssh): 集中 host 结果上限 / 目录组 helper |
二、为什么必须用 E2E:单元测试与端到端的分工边界
测试计划开篇就用一张表格划清了“已被单元测试覆盖、E2E 不要重复测”的清单,这是这份计划最值得学习的设计决策——E2E 成本高、易抖动,只应覆盖那些“假不了”的部分:
| 已被覆盖的领域 | 覆盖位置(仓库内路径) |
|---|---|
listConfigHosts/resolveConfigHost的 IPC 注册 | ssh-target-crud-handlers.ts 与 IPC 注册覆盖测试 ssh-target-registry.test.ts |
| 搜索、结果上限、被抑制别名、alreadyInOrca | src/main/ssh/ssh-config-host-picker.test.ts |
| 生成守卫(generation guard)、解析期间冻结、迟到解析(late resolve) | AddRemoteHostDialog.config-picker.test.tsx |
批量importConfig()且不带reAdopt | add-remote-host-ssh-actions.test.ts |
| 别名折叠 / 保存前重复检查 | ssh-target-duplicate.test.ts |
configured-only主机注册表 / 设置阶段的 fail-closed | shared 侧单元测试 + project-host-workspace-target 相关测试 |
| Settings 弹层视口稳定性 | ssh-host-form-modal.spec.ts |
计划因此明确:E2E 只留给三件单元测试给不了的事——真实的 Electron HOME 隔离、真实的~/.ssh/config解析、真实的ssh -G解析,以及用户可见的 DOM 结果。mock 掉 IPC、mock 掉文件系统、mock 掉ssh子进程之后,单元测试已经覆盖了所有“逻辑正确性”;E2E 需要回答的是“整台机器上、真实 OpenSSH、真实文件权限、真实 Electron 进程里,用户到底看到了什么”。
三、E2E Harness 的五个硬性要求
计划用编号列出了 Harness 层面的五项要求,它们共同决定这套用例能否稳定、并行、无副作用地跑通。
3.1 隔离 HOME:测试不与开发者真实 SSH 配置打架
E2E 运行框架已经将HOME指向{userDataDir}/home。测试在应用内取得真正的 home 路径再写入种子配置,而不是在 Node 侧猜测:
const home = await electronApp.evaluate(({ app }) => app.getPath('home')) mkdirSync(path.join(home, '.ssh'), { recursive: true, mode: 0o700 }) writeFileSync(path.join(home, '.ssh/config'), configBody, { mode: 0o600 })这段逻辑在共享 helper tests/e2e/helpers/ssh-config-host-picker.ts 中实现为seedIsolatedSshConfig:.ssh目录按0o700创建、config 文件按0o600写入——与 OpenSSH 对敏感配置的权限要求一致,也从权限层面杜绝了“换了一台机器/一个 CI runner 就扫到真实 SSH key”的风险。config 文件体由buildSshConfigBody生成:
export function buildSshConfigBody(hosts: SeededSshConfigHost[]): string { return hosts .map( (host) => `Host ${host.alias}\n HostName ${host.hostname}\n User ${host.user}\n Port ${host.port}\n` ) .join('\n') }3.2 唯一别名前缀:让多个 worker 永不互相污染
并行 E2E 里最经典的坑是“两个 worker 在同一毫秒共用Date.now()”。helper 提供的makeSshConfigHostPrefix在时间戳之外再加了 6 位随机段:
export function makeSshConfigHostPrefix(): string { return `e2e-ssh-cfg-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}` }每个 spec 文件在模块顶层调用一次生成模块级常量HOST_PREFIX,所有种子 Host 别名与 Orca label 都以它开头(如e2e-ssh-cfg-mxxx-alpha);afterEach里再通过window.api.ssh.removeTarget按 label/configHost 前缀做清理。两个 spec 文件(picker 与 import)还各自把afterEach清理包在.catch(() => undefined)里,保证“测试失败也要清干净”。
3.3 打开 Picker 的路径:必须是 Add SSH host 表单,不是 Settings
计划特别埋了一个重要提示:Settings 里的SshTargetForm没有 config picker,测试若从设置页进入将永远看不到被测入口。正确的 UI 路径(helperopenAddSshHostDialog完整复现)是:
- 打开Add Project(侧边栏 / landing 控件);
- 在对话框里点开 Host 下拉(combobox,cmdk 实现);
- 选Add remote host;
- 在弹出 popover 中点Add SSH host(其可访问名还带 “existing machine over SSH” 细节行);
- 出现标题为Add SSH host的表单对话框,上面有
Fill from ~/.ssh/config…链接。
点击链接后进入标题为Choose from ~/.ssh/config的选择器对话框,并且 helper 会先等待 loading 文案Reading ~/.ssh/config…消失,再开始断言行内容,避免把空态误判成“无主机”。
3.4 断言原则:只看 DOM,store/API 只用于铺数据
计划把断言纪律写得很清楚:断言只走 DOM(getByRole、toHaveValue、可见 badge/toast);window.api.ssh/ store 只在 setup(预置 Orca target)、清理(removeTarget)与复验(listTargets)时使用。配合 helper 中对“可访问名包含别名 +user@host:port摘要行”的configHostRow定位器,整个测试读起来几乎就是一份用户操作说明书。
3.5 前置条件:机器上要有真 OpenSSH 客户端
ssh -G是真实解析的核心,macOS / Linux CI 均自带。计划允许在ssh -G不可用时 skip 或 soft-fail,但要求在测试注释里写明原因——绝不能在缺ssh的机器上假装跑过解析。
四、Spec 文件划分与共享 helper
计划指定两个 spec 文件:
- tests/e2e/ssh-config-host-picker.spec.ts:承载P1、P2、P3、P4、P8(单主机挑选、表单预填、筛选、返回),P3 顺带覆盖 N3 的 identity 提示断言;
- tests/e2e/ssh-config-host-import.spec.ts:承载P5、P6、P7、P9(In Orca badge、批量导入、删除不复活、Settings Import 复收养)。
若将来 Settings Import 场景膨胀到需要独立文件,计划预留了可选的ssh-config-import-settings.spec.ts;在现状下,Import 用例保持在同一个文件即可。文件划分原则是:“表单上的选择器”一类、“批量导入 vs 重新收养语义”一类,读者按文件即可判断用例族。两个文件都复用ssh-host-form-modal.spec.ts的既有模式(session ready、target 清理、临时 announcement 关闭),并优先沉淀在共享 helper tests/e2e/helpers/ssh-config-host-picker.ts 里,而不是新建共享模块——除非 helper 会被多处复用。
五、九个必做用例(P1–P9)逐条拆解
P1 — 空配置的空态
| Setup | 不创建~/.ssh/config(或写入空文件) |
| Steps | 打开 Add SSH host → 点Fill from ~/.ssh/config… |
| Expect | 对话框标题Choose from ~/.ssh/config;正文No hosts in ~/.ssh/config;Add all to Orca按钮 disabled;Back返回表单 |
对应实现 ssh-config-host-picker.spec.ts 还额外断言了空态引导文案“Add a Host entry there, or go back and type the details manually.”,以及Back之后表单三个字段(Host/Username/Label)全部为空——把“空态可退出”也变成一等断言。
P2 — 种子主机列出且带摘要行
| Setup | 写入 ≥2 个具体 Host,如e2e-alpha/e2e-bravo,含 HostName、User、Port |
| Steps | 打开 picker |
| Expect | Host 列表(可访问名SSH config hosts)同时显示两个别名;每行副标题为user@hostname:port;Add all 2 to Orca按钮可用 |
摘要行由 helper 的hostEndpointSummary拼成${user}@${hostname}:${port},行定位器要求可访问名同时包含“别名”与“摘要”,从根本上防止“列表渲出来但文字对不上”的假阳性。
P3 — 点选预填表单(且 Save 能持久化)
| Setup | Config Hoste2e-prod→ HostNameprod.example.test、Userdeploy、Port2222 |
| Steps | 点选e2e-prod→ 等表单出现 → 点Save |
| Expect | 点选后:Host/别名字段 =prod.example.test,Username =deploy,Port =2222,Label =e2e-prod(或别名);可选 toastFilled from e2e-prod;Identity file 保持为空并显示 config 提示。Save 后:对话框关闭;Settings → SSH(或 listTargets)能看到该 target |
这是整个功能链的“主场景”。实现侧的数据流是:renderer 通过ssh:resolveConfigHostIPC 让主进程用ssh -G解析该别名,再把返回的 hostname/port/username/label 写入表单。spec 用toHaveValue逐一校验四个字段,其中 Label 校验的是别名本身——这是 Orca 侧用于标识该 target 的来源键(configHost)。N3 在本用例内一并验证:Identity file 留空时,表单会显示以 “Left empty on purpose:” 开头的提示,解释多 key 场景下会在连接时按 config 解析。
P4 — 筛选收窄列表
| Setup | Hostse2e-alpha、e2e-bravo |
| Steps | 打开 picker;筛选框输入bravo |
| Expect | 只剩 bravo 行,alpha 消失;输入无意义词时显示No matching hosts |
spec 注释里点明了一个工程细节:picker 的筛选 IPC 有约 200ms 防抖,所以断言要容忍这段延迟(toBeVisible({ timeout: 5_000 }))。无结果时除了 “No matching hosts”,还会显示“Try another filter, or go back and type manually.”的退路引导。实现上,主进程每收到一次ssh:listConfigHosts都会把 query 与 Host/HostName/User 做不区分大小写的子串匹配(见 ssh-config-host-picker.ts),而解析~/.ssh/config的结果在本次 picker 会话内被缓存,逐键筛选不重复解析文件(同文件 L15-L26)。
P5 — 已在 Orca 的 Host:In Orca badge + 禁用行
| Setup | Config hosts alpha + bravo;用ssh.addTarget预置一个configHost/label等于 alpha 的 Orca target |
| Steps | 打开 picker |
| Expect | alpha 显示In Orcabadge 且不可点击;bravo 仍可选;按钮显示Add all 1 to Orca(而非 2) |
这条用例守卫的是“去重展示”。实现侧searchSshConfigHosts会先把所有既有 target 的configHost与label收进一个Set(都经过别名归一),凡是命中者标alreadyInOrca(ssh-config-host-picker.ts);统计newHostCount时已存在的别名不计入,于是按钮数字自动变成“纯新增量”。
P6 — Add all N 只导入新主机
| Setup | Config 含 2 个全新 Host,Orca 无对应 target |
| Steps | 点Add all 2 to Orca→ 等成功 toast / 返回表单或列表刷新 |
| Expect | 两个 target 都存在(Settings SSH DOM 与/或 listTargets 可见);重开 picker 显示All hosts already in Orca/ 两行都标In Orca |
spec 里可看到导入成功的 toast 文案为 “Added 2 hosts to Orca.”,随后断言两个对话框(picker 与 Add SSH host 表单)都会关闭。重开 picker 后两行均 disabled 并带 In Orca badge,按钮文案切换为No new hosts to add(disabled)。这验证了“批量导入”的计数与状态闭环。
P7 — Add all 不会重新收养被删主机(suppress tombstone)
| Setup | Config 含 alpha + bravo;先 Add all,再用 APIremoveTarget删掉 alpha(产生 suppress 墓碑) |
| Steps | 重开 picker;注意计数;可选再点一次 Add all |
| Expect | alpha 从 picker 消失(被抑制)或不被重建;只有新主机被计数;第二次 Add all 后listTargets仍没有已删除的 alpha |
这一条与 P9 是整套语义的“对照组”。实现里有个容易误读的细节值得展开:被抑制的别名在 picker 中仍然列出(标Removed from Orca、可再次点选),但永远不计入“new”(ssh-config-host-picker.ts 注释解释了原因:墓碑只阻止被动批量导入——若把该 Host 从列表里彻底拿掉,用户删除一个 Orca target 后反而会觉得“~/.ssh/config空了”)。spec 断言 alpha 行可见且带 “Removed from Orca”、按钮为 “No new hosts to add”,并在 Settings 页确认 alpha 保持缺席(helperexpectSshHostAbsentFromSettings)。
P8 — Back 放弃未决的挑选路径
| Setup | 已播种 config |
| Steps | 打开 picker → 不做任何选择直接Back |
| Expect | 表单字段仍为空(Host 空白);无 filled toast |
spec 顺带断言页面不存在任何/Filled from /i文案——防止“没选也填了”或“残留 toast”这类 UI 状态泄漏。
P9 — Settings Import 会重新收养(与 P7 对照)
| Setup | 与 P7 删除后状态相同 |
| Steps | Settings → SSH →Import(显式 reAdopt 路径) |
| Expect | 被删的 config host 重新成为 Orca target;同步 toast 计数 ≥1 |
spec 中断言 toast 形如Synced N servers,随后 Settings 页里 alpha 与 bravo 双双回归。两条用例 P7/P9 合起来回答了产品最微妙的问题:“我不想在 Orca 里用它”与“我想让 Orca 和配置保持一致”是两种不同的用户意图,UI 必须用两个不同入口表达,测试必须把两个入口的行为差异钉死。
六、锦上添花用例(N1–N3)与明确跳过项
计划在 P1–P9 全部落地后才考虑以下低成本项,避免“nice-to-have 拖累主交付”:
- N1ProxyCommand / JumpHost:选一个带 ProxyJump 的 Host → Advanced 区自动展开且 jump 字段被填好(同时证明 advanced 预填与
preferAdvancedOpen行为); - N2大小写不敏感别名:config 写
Prod、已有 labelprod→ 判定为In Orca。这条背后的实现依据是别名比较统一走 ssh-config-alias.ts 的normalizeSshConfigAlias(trim + toLowerCase),因为 OpenSSH 对 Host 模式的匹配本身不区分大小写; - N3config 填充后可见 “Identity file 留空” 的提示(已在 P3 用例中随附验证)。
明确跳过(均有单元测试兜底):100 主机截断、解析竞态(freeze/late resolve)、GSSAPI 系统默认值、composer 主机可用性的 fail-closed。这一“跳过清单 + 理由”的做法,让后来者不会为了凑覆盖率而重复劳动。
七、实现侧纵深:搜索、计数、解析与删除墓碑
7.1 一次 picker 会话只解析一次配置
主进程模块 src/main/ssh/ssh-config-host-picker.ts 维护模块级cachedConfigHosts,理由注释写得很直白:筛选框每次按键都会重新查询主进程,而解析(含 Include 展开)~/.ssh/config是全部成本;文件在两个按键之间不可能变化,所以本次 picker 会话持有解析结果、重开时刷新(refresh: true强制重读)。invalidateUserSshConfigHostCache()用于需要立刻失效的场景。
7.2 结果对象:total / new / match 三分量
searchSshConfigHosts一次返回totalHostCount(去重后的全部 Host)、newHostCount(既不在 Orca、也不在抑制集的可新增数)、matchCount(命中当前筛选的行数)与hasMore。Picker 顶部的 “Add all N to Orca” 直接用newHostCount渲染;行级状态由每个 summary 上的alreadyInOrca/previouslyRemoved决定 badge。结果上限常量SSH_CONFIG_HOST_RESULT_LIMIT = 100定义于 src/shared/ssh-types.ts,因此 100 主机截断是单元覆盖范围,E2E 不必制造 100+ 主机。
7.3 解析前先确认别名还在配置文件里
resolveUserSshConfigHost有个反直觉但关键的守卫:先重读配置(而非用会话缓存)确认该别名仍存在,才调用ssh -G。注释解释:ssh -G对未知别名也会应答,并把别名本身回显成 hostname——若不守卫,用户在打开 picker 之后把某行从 config 里删掉,再点选那一行就会凭空造出一个“已不再配置”的 target(ssh-config-host-picker.ts)。这条“迟到解析校验”同时解释了对表单解析期间做 freeze(防重复提交)的必要性。
7.4 为什么 E2E 能测出单元测试测不出的东西:ssh -G 输出解析
真实解析路径在 src/main/ssh/ssh-g-config-resolution.ts:用runProcess执行ssh -G -- <host>(--防止以-开头的别名被当成 ssh 参数),5 秒超时,非零退出或超时都返回 null。输出解析有大量“防呆”细节:proxycommand none/proxyjump none不会被当成真实命令;identityfile逐行收集并做~展开;known_hosts 路径列表按带引号、空格切分还原。这些只有在接上真实 OpenSSH 输出时才可能被端到端验证——这正是本套 E2E 存在的意义。
八、产品修复:getpwuid 与$HOME的分歧(HOME 隔离的关键前提)
计划单列一节“Product fix required for E2E (and real HOME isolation)”,这是本套测试最容易被忽视的深坑:
OpenSSH 解析默认用户配置用的是getpwuid(passwd 记录里的家目录),而不是环境变量
$HOME;而 Node 侧os.homedir()是感知$HOME的。E2E 把HOME指到隔离目录后,loadUserSshConfig(读~/.ssh/config)与ssh -G(按 getpwuid 读 passwd home 下的 config)可能读到两个不同文件。
修复落在 ssh-g-config-resolution.ts 的sshGArgsForHost:
const homeConfigPath = join(homedir(), '.ssh', 'config') if (existsSync(homeConfigPath) && homeConfigPath !== passwdHomeSshConfigPath()) { return ['-F', homeConfigPath, '-G', '--', host] } return ['-G', '--', host]即:当$HOME下确实存在 config 且与 passwd home 不同路径时,向ssh -G显式传-F <homedir>/.ssh/config,让解析器看到的就是 picker 列出的那份文件。代码注释同时提示两个衍生效应:-F会一并排除/etc/ssh/ssh_config(因此该路径是“纯用户解析”);existsSync是承重逻辑——若传了不存在的-F文件,ssh会以 255 退出,于是没有 HOME config 时回退到 passwd home 解析,而resolveUserSshConfigHost会在信任ssh -G之前先拒绝那些 picker 无法列出的别名。对普通安装(HOME = passwd home)行为完全不变。
九、明确的不测范围(Out of scope)
计划诚实列出以下内容不做 E2E,避免无谓的复杂度与不稳定:
- 真实 SSH 连接 / relay / PTY(那是另一套测试的领域);
- Docker SSH fixtures;
- Web 客户端 stub 路径(如
listConfigHosts恒返回空); - 非英语 i18n。
十、实现状态与共享 helper
计划文档中的“Implementation status (done)”表记录了每个用例落地的 spec 归属,可作为读者定位用例时的索引:
| 用例 | Spec |
|---|---|
| P1 空态 | ssh-config-host-picker.spec.ts |
| P2 列表 + Add all 可用 | ssh-config-host-picker.spec.ts |
| P3 点选 + Save(+ N3 identity 提示) | ssh-config-host-picker.spec.ts |
| P4 筛选 | ssh-config-host-picker.spec.ts |
| P5 In Orca badge / 计数 | ssh-config-host-import.spec.ts |
| P6 Add all 导入 | ssh-config-host-import.spec.ts |
| P7 删除后不复收养 | ssh-config-host-import.spec.ts |
| P8 未选择即返回 | ssh-config-host-picker.spec.ts |
| P9 Settings Import 复收养 | ssh-config-host-import.spec.ts |
共享 helper 全部收敛在 tests/e2e/helpers/ssh-config-host-picker.ts,包括:种子写入seedIsolatedSshConfig、唯一前缀makeSshConfigHostPrefix、UI 导航openSshConfigHostPicker/openSshHostSettings/returnToAppShell、表单字段addSshHostFormFields、行定位configHostRow、Settings 断言expectSshHostListedInSettings/expectSshHostAbsentFromSettings、数据铺底与清理seedOrcaSshTargetMatchingAlias/removeSshTargetsByPrefix/removeSshTargetByAlias。所有定位器坚持用 role/label 与用户可见文案,而非 id/data-*——这让 spec 在 UI 文案变化时能明确报错,而不是悄悄通过。
十一、运行方式
先构建 e2e 模式的 electron bundle,再用 Playwright 以 electron-headless 项目、单 worker 顺序执行两个 spec:
pnpm exec electron-vite build --mode e2e SKIP_BUILD=1 pnpm exec playwright test \ tests/e2e/ssh-config-host-picker.spec.ts \ tests/e2e/ssh-config-host-import.spec.ts \ --config tests/playwright.config.ts --project=electron-headless --workers=1要点拆解:
--mode e2e:保证产物带 e2e 所需能力;SKIP_BUILD=1让 playwright 直接用刚构建好的产物,避免二次构建;--project=electron-headless:本套用例运行在无头 Electron,HOME 由框架隔离到{userDataDir}/home;--workers=1:两个 spec 共享隔离 HOME 语义且涉及真实子进程解析,串行执行换来确定性;- 前置条件:执行机上必须有可用的
ssh(ssh -G),macOS/Linux CI 自带;若缺失应 skip 或 soft-fail 并在注释中说明。
十二、小结
这份计划的价值不止于“九个用例”,更在于它示范了一套可并行、只测真东西的 E2E 分层方法论:
- 分工先行:凡是单元测试能确证的(IPC 注册、搜索计数、别名折叠、解析竞态、fail-closed),一律不进 E2E;
- 环境求真:隔离 HOME + 真实文件权限 + 真实
ssh -G,并为此引入-F修复,让“读配置”与“解析配置”永远指向同一份文件; - 断言可见:只对用户能看到的 DOM 负责,同时用唯一前缀 + afterEach 清理保证 worker 间零污染;
- 语义对照:P7 与 P9 这对“批量不复活 / Import 显式复收养”的对照组,把产品里最容易含混的删除-再同步语义变成了可回归的机器断言。
对想深入源码的读者,建议按此顺序阅读:ssh-config-host-picker.ts(搜索/计数/解析守卫)→ ssh-g-config-resolution.ts(-F与输出解析)→ ssh-target-crud-handlers.ts(ssh:listConfigHosts/ssh:resolveConfigHost/ssh:importConfig三个 IPC 的注册语义),再回到本计划与两个 spec 文件对照验证。
【免费下载链接】orcaOrca is the ADE for working with a fleet of parallel agents. Run any coding agent with your own subscription. Available on desktop, mobile and remote runtime.项目地址: https://gitcode.com/GitHub_Trending/orca48/orca
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考