Windows Terminal 连接 Azure Cloud Shell 的完整设计与源码实现
Windows Terminal 允许用户把 Azure Cloud Shell 当作一个"连接"直接打开,本文以规格文档 [Azure cloud shell connector](https://gitcode.com/GitHub_Trending/term/terminal/blob/20588130d8ef2ba40eb56bdae88e04cce7fc5b5d/doc/specs/?utm_source=gitcode_repo_files#1564 - Settings UI/spec.md) 为骨架,完整讲清这一特性的设计动机、认证流程、连接生命周期,并深入仓库源码,逐条印证 ITerminalConnection 接口是如何被 AzureConnection 实现的。读完你能掌握:终端"连接"的抽象模型、基于设备码(Device Code Flow)的无浏览器认证方案,以及令牌存储、多租户选择、WebSocket 建立等关键实现细节。
一、设计目标:把 Azure 服务带进 Windows Terminal
规格文档(issue #1235)开篇的摘要非常直接:这份规格描述了一个功能——让 Windows Terminal 用户连接到 Azure Cloud Shell,并包含实现与设计考量。其灵感(Inspiration)是:让开发者在 Windows Terminal 应用内就能顺滑地访问自己的 Azure 服务,以方便的方式与 Azure 技术打交道。
从仓库的实际落地看,这个"方便"被落实为一项产品能力:当平台支持时,用户会看到一个名为 "Azure Cloud Shell" 的动态配置文件(见上方配图,实际实现中该配置拥有独立图标),选中它即可发起一次完整的云端连接。这一能力对应源码中的配置文件生成器 AzureCloudShellGenerator。
规格文档同时明确了整个功能必须"以隔离方式实现"——它对 Windows Terminal 应用本体几乎没有依赖。这样一旦终端支持插件/扩展,这个连接器就能直接变成一个插件。这一点在源码中得到忠实贯彻:AzureConnection 没有侵入终端主流程,而是作为一个普通的连接类型存在。
二、认证方案:设备码流程与 Azure AD v1.0
规格文档对"认证用户"这一步的设计是全文最关键的部分,原因有二:
- 为什么用设备码流程(Device Code Flow):因为 Windows Terminal(当时)不支持拉起浏览器。设备码流程允许用户在一个带屏幕的设备(这里就是终端)上输入一段短码,然后在另一台设备的浏览器里完成登录,终端这边通过轮询拿到令牌。
- 为什么用 Azure AD v1.0 而不是 v2.0:因为 v2.0(即 Microsoft Identity Platform)彼时不支持个人账号(personal accounts)走设备码流程。而 Azure Cloud Shell 用户中个人账号很常见,因此选 v1.0。
关于令牌的存放,规格文档的要求是:认证成功后把登录/令牌信息存下来,避免用户每次都走一遍设备码流程;由于这是敏感信息,令牌需加密存储。规格当时指向的是 Windows Storage 加上 Windows Security Data Protection(DPAPI)。
这里需要特别指出实现与规格的差异:从源码看,真正落地采用的是 Windows.Security.Credentials 命名空间下的 PasswordVault / PasswordCredential(而非规格里设想的 Storage + DPAPI 组合)。以 AzureConnection.cpp 为例,_StoreCredential 通过 PasswordVault 把"用户名"字段(JSON 序列化的租户信息)与"密码"字段(JSON 序列化的 accessToken / refreshToken / expiry)打包存入,资源名固定为 Terminal:
// src/cascadia/TerminalConnection/AzureConnection.cpp
void AzureConnection::_StoreCredential()
{
WDJ::JsonObject userName;
userName.SetNamedValue(L"ver", WDJ::JsonValue::CreateNumberValue(CurrentCredentialVersion));
_packTenant(userName, *_currentTenant);
WDJ::JsonObject passWord;
passWord.SetNamedValue(L"accessToken", WDJ::JsonValue::CreateStringValue(_accessToken));
passWord.SetNamedValue(L"refreshToken", WDJ::JsonValue::CreateStringValue(_refreshToken));
passWord.SetNamedValue(L"expiry", WDJ::JsonValue::CreateStringValue(std::to_wstring(_expiry)));
PasswordVault vault;
PasswordCredential newCredential{ PasswordVaultResourceName, userName.Stringify(), passWord.Stringify() };
vault.Add(newCredential);
}
其中 CurrentCredentialVersion 是一个"令牌版本"常量(当前为 2)。源码在 _RunAccessState 里会读取每条已存凭据的 ver 字段,凡是版本不一致的旧凭据都会被直接移除(vault.Remove(entry)),只保留最新格式。这是一种平滑的存储格式迁移机制。令牌临近过期时(timeNow + _expireLimit > _expiry,其中 _expireLimit 为 2700 秒)会自动走 _RefreshTokens 刷新并回写。
结论:规格文档给出了"隔离实现 + 设备码认证 + 加密令牌存储"的设计意图,而源码在"存储用什么 API"这一细节上与原始设想不同,但总体方向(避免重复登录、安全存储、自动刷新)完全一致。
三、连接生命周期:ITerminalConnection 与状态机
规格文档中反复强调的核心设计原则是:连接器应遵循现有的 ITerminalConnection 接口,使 Azure 只是"Windows Terminal 能建立的一种连接类型"。
这个接口定义在 ITerminalConnection.idl,是终端所有后端连接(本地 ConPTY、远程、Azure 等)的统一抽象:
interface ITerminalConnection
{
void Initialize(Windows.Foundation.Collections.ValueSet settings);
void Start();
void WriteInput(Char[] data);
void Resize(UInt32 rows, UInt32 columns);
void Close();
event TerminalOutputHandler TerminalOutput;
event Windows.Foundation.TypedEventHandler<ITerminalConnection, Object> StateChanged;
Guid SessionId { get; };
ConnectionState State { get; };
};
配套的 ConnectionState 枚举(NotConnected / Connecting / Connected / Closing / Closed / Failed)就是连接状态的通用词汇。规格文档所说的"以隔离方式实现、将来可变成插件",本质就是让 AzureConnection 实现这五个方法加两个事件,从而"即插即用"地挂进终端的连接体系。
AzureConnection 的内部状态机
AzureConnection 在 AzureConnection.h 里定义了一个更细的业务状态机(AzureState),用来描述"从拿到账号到进入云端终端"的完整过程:
| 状态 | 含义 | 对应实现方法 |
|---|---|---|
AccessStored |
检查是否已有保存的凭据,让用户选择"复用/新登录/删除" | _RunAccessState |
DeviceFlow |
无凭据或用户选择新账号,走设备码认证 | _RunDeviceFlowState |
TenantChoice |
账号有多个租户,需用户选择 | _RunTenantChoiceState |
StoreTokens |
询问是否保存本次凭据供下次使用 | _RunStoreState |
TermConnecting |
已备齐 tenantID / 令牌,发起连接 | _RunConnectState |
TermConnected |
已进入云端终端,循环读取 WebSocket | (在 _OutputThread 中内联处理) |
驱动这台状态机的是 Start() 里创建的一条输出线程 _OutputThread(见 AzureConnection.cpp)。线程内是一个 switch(_state) 大循环:每处理完一个状态就推进到下一个,直到 TermConnected 后进入 WebSocket 读取循环。这里体现了规格中"认证、申请 cloud shell、申请终端走 HTTP,连接终端走 WebSocket"的分层设计。
几个值得注意的实现事实(均可在源码确认):
ConnectionTypeGUID:每个连接类型都有全局唯一标识,AzureConnection的是{0xd9fcfdfa, 0xa479, 0x412c, {0x83, 0xb7, 0xc5, 0x64, 0xe, 0x61, 0xcd, 0x62}}。配置文件正是靠这个 GUID 关联到AzureConnection。IsAzureConnectionAvailable():返回AzureClientID != L"0"。规格与源码注释都说明,客户端 ID 只在正式发布流水线里注入,本地构建会得到占位值"0",因此本地构建会主动禁用 Azure 连接,避免"连不上还白报错"。Initialize解析初始尺寸与会话:从settings里读取initialRows / initialCols / sessionId,若sessionId为空则用Utils::CreateGuid()生成一个。Resize在已连接时会向{cloudShellUri}terminals/{id}/size?cols=&rows=&version=2019-01-01发请求,从而把本地窗口缩放同步到云端终端。
四、从规格到落地:HTTP + WebSocket 的实现
规格文档提出前三步(认证、请求 cloud shell、请求 terminal)用 HTTP,最后一步(连接终端)用 WebSocket,并点名了 cpprestsdk 作为 HTTP 客户端库(理由:同为微软维护,出问题便于内部协同)。
这里再次出现实现与规格的偏差,值得如实说明:从源码看,HTTP 请求用的是 WinRT 的 winrt::Windows::Web::Http::HttpClient(封装在 _SendRequestReturningJson 中),而 WebSocket 升级用的是 WinHttp 系列 API(WinHttpOpen → WinHttpConnect → WinHttpOpenRequest → WinHttpWebSocketCompleteUpgrade),并没有引入 cpprestsdk。也就是说,规格里的"库选型"在落地时被替换成了 Windows 原生的 WinRT / WinHttp 方案。这一点对理解"规格文档与最终代码的关系"很典型:规格记录的是设计阶段的判断,代码才是最终事实。
关键的连接建立逻辑集中在 _RunConnectState 与 _GetTerminal:
- 拉取用户云控制台设置(
_GetCloudShellUserSettings),从properties.preferredShellType解析用户偏好的 shell,缺省回退到pwsh(_ParsePreferredShellType)。 - 申请一个 cloud shell(
_GetCloudShell),向providers/Microsoft.Portal/consoles/default发PUT,请求体固定{"properties": {"osType": "linux"}},拿到properties.uri作为_cloudShellUri。 - 为该 cloud shell 申请一个 terminal(
_GetTerminal),向{uri}terminals?cols=&rows=&version=2019-01-01&shell={shellType}发POST,拿到终端id。 - 推导 WebSocket 端点:源码对两种形态做了区分处理——若 cloud shell URI 不含
servicebus,直接把它https换成wss再拼上terminals/{id};若含servicebus,则按 cloud shell 团队自己的规则把 socket URI 重排为.../$hc/{ns}/terminals/{id}。注释明确写道"这里的逻辑基于 cloud shell 团队自身的方式"。 - 升级到 WebSocket 后进入
TermConnected,在输出线程里用WinHttpWebSocketReceive循环读取,把 UTF8/BINARY 消息经til::u8u16转码后通过TerminalOutput事件抛给终端 UI。
用户输入侧,WriteInput 在"已连接且已进入 TermConnected"时直接把数据以 WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE 发到 WebSocket;否则(认证阶段)会做本地回显、处理退格、按 InputMode::Line 收集整行输入,供认证流程 _ReadUserInput 读取。
五、配置与使用:动态配置文件如何出现
规格文档在 UI/UX 一节只说"会多出一个新配置文件选项(实现时会配独立图标)"。落到仓库里,这个"多出来"的配置文件是由 AzureCloudShellGenerator 作为动态配置文件生成器注入的,而非写死在用户 JSON 里:
// src/cascadia/TerminalSettingsModel/AzureCloudShellGenerator.cpp
void AzureCloudShellGenerator::GenerateProfiles(
std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const
{
if (AzureConnection::IsAzureConnectionAvailable())
{
auto azureCloudShellProfile{ CreateDynamicProfile(L"Azure Cloud Shell") };
azureCloudShellProfile->StartingDirectory(winrt::hstring{ DEFAULT_STARTING_DIRECTORY });
azureCloudShellProfile->DefaultAppearance().DarkColorSchemeName(L"Vintage");
azureCloudShellProfile->DefaultAppearance().LightColorSchemeName(L"Vintage");
azureCloudShellProfile->ConnectionType(AzureConnection::ConnectionType());
profiles.emplace_back(std::move(azureCloudShellProfile));
}
}
可以推断出几个使用层面的事实:
- 可用性与构建相关:
IsAzureConnectionAvailable()为假(如本地构建)时,这个配置文件根本不会生成——这也解释了为什么有些 Windows Terminal 安装里看不到 "Azure Cloud Shell"。 - 外观走 Vintage 配色:无论深色还是浅色外观,默认都用
Vintage配色方案,并设置了独立的生成器图标(ProfileGeneratorIcons/AzureCloudShell.png)。 - 连接类型由 GUID 关联:
ConnectionType(AzureConnection::ConnectionType())把配置文件绑定到前面那个固定 GUID,打开时终端据此实例化AzureConnection。
六、能力边界:可访问性、安全、可靠性与性能
规格文档专设了 Capabilities 一节评估各维度影响,这里完整继承其结论:
- 可访问性:该功能不影响 Windows Terminal 的无障碍能力。
- 安全:任何联网功能都引入安全风险;文档认为,通过正确使用 Azure AD v1.0 并谨慎保管服务端下发的令牌,可把风险降到可控范围。从源码看,令牌只经
PasswordVault这类系统级凭据存储,不落明文。 - 可靠性 / 性能 / 功耗 / 效率:文档判断这些维度均不受影响。
- 兼容性:正因为实现与终端本体基本解耦,文档认为不会有既有代码或行为被破坏。
七、潜在风险与未来方向
规格文档如实列出了三条潜在问题,值得读者保留:
- 依赖 cpprestsdk 这一开源项目——其代码问题会波及本功能(注:落地实现已改用 WinRT/WinHttp,此条在实现中实际已被规避)。
- Azure AD v1.0 可能被弃用——目前仍受支持,但未来有风险;文档给出的兜底是"最坏情况可切换到 Microsoft Identity Platform,只需少量修改 HTTP 请求"。
- Azure Cloud Shell 的 API 并非公开——正式落地需要 Azure Cloud Shell 团队授予应用权限,构成一项额外依赖。
文档结尾还提出一个展望:一旦 Windows Terminal 允许插件/扩展,这个 Azure 连接器"有可能是终端的第一个插件"。这与全文反复出现的"隔离实现、可插拔"主线呼应,也解释了为何源码始终让 AzureConnection 只通过 ITerminalConnection 与终端对话。
小结:本文以 [规格文档](https://gitcode.com/GitHub_Trending/term/terminal/blob/20588130d8ef2ba40eb56bdae88e04cce7fc5b5d/doc/specs/?utm_source=gitcode_repo_files#1564 - Settings UI/spec.md) 为主线,还原了"认证 → 请求 cloud shell → 请求 terminal → WebSocket 连接"的完整链路,并逐条对照仓库源码 AzureConnection.cpp / AzureConnection.h / AzureCloudShellGenerator.cpp / ITerminalConnection.idl 印证了实际实现。需要记住的关键差异是:令牌存储从规格的 "Storage + DPAPI" 变成了 PasswordVault,HTTP/WebSocket 从 "cpprestsdk" 变成了 WinRT/WinHttp 原生 API——规格记录设计意图,代码才是最终事实,两者结合阅读才能完整理解这一特性的全貌。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00