资讯动态

Kiln Git 同步的 GitHub OAuth Token 自动刷新机制:从 503 误报到 401 精确提示的实现设计

发布时间:2026/10/16 12:30:10 来源:尧图企业网站定制
人工智能AI 应用AI 评测RAG微调提示工程开发工具桌面应用【免费下载链接】KilnBuild, Evaluate, and Optimize AI Systems. Includes evals, RAG, agents, fine-tuning, synthetic data generation, dataset management, MCP, and more.项目地址https://gitcode.com/gh_mirrors/ki/Kiln点击查看免费下载本文以 Kiln 开源仓库中specs/projects/git_oauth_token_refresh/项目的设计与实现计划为核心系统讲解 Kiln AI Git 同步GitHub Sync在 OAuth 用户访问令牌过期后自动续期的完整方案包括问题根因、数据模型改造、后端自动刷新引擎、错误分类与前端贯通以及配套的测试与端到端验证方法。读者阅读后可完整掌握 Kiln 如何在 GitHub App 用户令牌 8 小时短生命周期的约束下实现免人工干预的持续同步并理解如何把授权过期从误导性的 503 网络错误中剥离出来转为可操作的 401 重连提示。背景与问题一夜之间所有同步都失败在 Kiln 中用户通过Kiln AI GitHub Sync这个 GitHub AppConnect with GitHub即auth_modegithub_oauth连接项目后一开始同步一切正常但大约8–24 小时之后通常隔夜每一次同步都会开始失败前端收到 HTTP 503Cannot sync with remote. Check your connection.。这不是网络问题而是认证问题——这正是本项目的出发点详见 project_overview.md。根因短命令牌被一次性丢弃GitHub App 的user access token 是短生命周期令牌当 GitHub App 开启了 Expire user authorization tokens 后默认有效期约8 小时。作为补偿GitHub 在颁发 access token 的同时还会返回一个有效期约6 个月的refresh_token以及expires_in字段供客户端在令牌过期前静默续期。Kiln 在改造前把这些信息全部扔掉了具体体现在三处oauth.py 中的exchange_code_for_token()只返回data[access_token]字符串refresh_token、expires_in、refresh_token_expires_in全部被丢弃config.py 中的GitSyncProjectConfig没有任何字段存储 refresh token 或过期时间全项目不存在任何刷新逻辑——既没有grant_typerefresh_token的调用也没有过期时间检查。当 access token 过期后pygit2 的凭据回调会被 GitHub 拒绝抛出pygit2.GitError(Authentication failed...)。这个错误被GitSyncManager.fetch()包装成RemoteUnreachableError再经中间件映射成 503 Check your connection.。一次授权过期被误标成网络故障用户不手动重连就永远无法恢复。值得注意的是这个问题只影响github_oauth用户PATpat_token与 SSHsystem_keys用户持有长期凭据完全不受影响。方案总览捕获、自动刷新、正确分类项目的核心目标是三层捕获Capture在连接/重连时把 GitHub 返回的 refresh token 与过期时间一并保存自动刷新Auto-refresh在 access token 过期前主动刷新proactive在收到认证拒绝时再刷新一次兜底reactive让 OAuth 同步无限期持续工作正确分类Classify当刷新不可行没有 refresh token、refresh token 过期、授权被撤销时给出清晰准确的重新连接 GitHub 账号错误而不是误导性的检查网络连接。整体数据流如下源自 architecture.mdconnect/reconnect → capture {access, refresh, expiries} (Phase 1) │ persisted in GitSyncProjectConfig ▼ GitSyncManager.fetch()/commit_and_push() → _ensure_valid_oauth_token() ── proactive refresh (Phase 2) → run git op └─ on auth rejection → reactive refresh retry (Phase 3) └─ on unrenewable auth → AuthExpiredError → 401 (Phase 3) frontend threads the new fields through the wizard/settings (Phase 4)四阶段实施计划implementation_plan.md 将工作拆成四个可独立评审、各自以项目自动化检查通过的阶段Phase 1 — Token capture storage后端改造oauth.py新增OAuthTokenResult、exchange_code_for_token返回它、新增refresh_access_token、扩展OAuthFlowState、git_sync_api.py回调保存 refresh/expiryOAuthStatusResponse、SaveConfigRequest、UpdateConfigRequest携带并持久化新字段auth-mode 切换时清空 refresh 字段、config.pyTypedDict 新字段、get_*默认值、update_oauth_tokens辅助函数、libs/core/.../config.pysensitive_keys并配套 oauth config api 持久化测试。Phase 2 — Auto-refresh enginemanager 中的刷新引擎errors.py新增AuthExpiredErrorgit_sync_manager.py增加project_path与_refresh_lock、_ensure_valid_oauth_token主动刷新与持久化并在fetch/push前调用registry.py与调用点middleware.py、desktop_server.py贯通project_path并配套主动刷新、持久化、轮换、并发、no-op 场景测试。Phase 3 — Error classification reactive refreshgit_sync_manager.py的_is_auth_error分类、fetch/commit_and_push中 auth 与网络的分支、反应式刷新与单次重试、ensure_fresh*透传AuthExpiredErrormiddleware.py的ERROR_MAP新增 401 重连提示确认background_sync能存活于AuthExpiredError。Phase 4 — Frontend plumbing重新生成 OpenAPI 绑定generate_schema.sh再同步更新手写类型与 OAuth 令牌路径上的api.ts、oauth_flow.ts、oauth_with_install.ts、git_import_wizard_store.ts、step_credentials.svelte、import_project.svelte、step_complete.svelte、git_sync_status.svelte及对应测试。阶段依赖关系Phase 1→2→3 顺序执行3 依赖 2 的 manager 工作2 依赖 1 的配置/捕获Phase 4 依赖 Phase 1 的后端模型变化schema 重新生成但整体上独立于 2/3。数据模型令牌与过期时间的持久化设计OAuthTokenResult令牌交换/刷新的统一返回体在 oauth.py 中新增 dataclassdataclass class OAuthTokenResult: access_token: str refresh_token: str | None None access_token_expires_at: float | None None # epoch seconds (wall-clock) refresh_token_expires_at: float | None None过期时间在交换/刷新时计算当 GitHub 返回expires_in时expires_at time.time() expires_in否则为None例如 GitHub 未开启令牌过期机制时access token 不过期此时无需刷新。GitSyncProjectConfig新增三个字段在 config.py 的GitSyncProjectConfigTypedDict 中增加oauth_refresh_token: str | None oauth_token_expires_at: float | None oauth_refresh_token_expires_at: float | None关键约束是向后兼容get_git_sync_config()必须通过.get(...)为每个新字段默认None因为历史配置里没有这些键。save_git_sync_config()本来就整体写出整个 dict所以除 TypedDict 扩展外无需改动。update_oauth_tokens只写令牌字段的同步辅助函数为避免整份配置回写带来的并发问题新增一个聚焦的辅助函数设计见 architecture.mddef update_oauth_tokens(project_path: str, result: OAuthTokenResult) - None: Synchronous read-modify-write of only the OAuth token fields for one project. config Config.shared() raw config.git_sync_projects or {} entry raw.get(project_path) if entry is None: return entry[oauth_token] result.access_token entry[oauth_refresh_token] result.refresh_token entry[oauth_token_expires_at] result.access_token_expires_at entry[oauth_refresh_token_expires_at] result.refresh_token_expires_at raw[project_path] entry config.git_sync_projects raw # synchronous persist; no await around this block这段代码的同步 read-modify-write、块内无await特性是并发正确性的基础后文会详细解释。敏感键libs/core配置层同步扩展git_sync_projects在 libs/core/kiln_ai/utils/config.py 中被声明为ConfigProperty当前sensitive_keys[pat_token, oauth_token]。refresh token 与 access token 同级敏感必须扩展为[pat_token, oauth_token, oauth_refresh_token]确保其在配置序列化/展示等场景中同样被脱敏处理。Phase 1 详解令牌捕获与持久化oauth.py交换、刷新与流状态exchange_code_for_token(code, code_verifier) - OAuthTokenResult原签名返回str。改造后解析access_token、refresh_token、expires_in、refresh_token_expires_in计算绝对过期时间并返回OAuthTokenResulterror/error_description路径继续抛OAuthError。当前实现的基础形态见 oauth.py仅解析并返回 access_token。refresh_access_token(refresh_token: str) - OAuthTokenResult新增。POST 到https://github.com/login/oauth/access_token请求体为{client_id, client_secret, grant_type: refresh_token, refresh_token}请求头Accept: application/json超时 30 秒。解析与交换相同的字段——注意 GitHub 的刷新响应会包含轮换后的新refresh_token因此必须把响应里的 refresh token 一并保存。若响应中没有access_token则抛OAuthError。OAuthFlowState增加refresh_token、oauth_token_expires_at、oauth_refresh_token_expires_at字段complete_flow()改为接收OAuthTokenResult或这些额外字段并存入流对象。当前 oauth.py 的OAuthFlowState只有oauth_token与error两个完成态字段。exchange_code_for_token的唯一调用者是git_sync_api.py中的 OAuth 回调二者必须同步修改Phase 1。time模块在 oauth.py 中已导入。git_sync_api.py回调、状态与配置持久化api_oauth_callback改为result await exchange_code_for_token(...)随后oauth_manager.complete_flow(state, result)把完整的OAuthTokenResult含 refresh token 与过期时间挂到 flow 上。当前实现见 git_sync_api.py。OAuthStatusResponse新增oauth_refresh_token、oauth_token_expires_at、oauth_refresh_token_expires_atapi_oauth_status从被消费的 flow 中读出并返回。当前响应模型见 git_sync_api.py。SaveConfigRequestapi_save_config接受并把这三个新字段持久化进GitSyncProjectConfig。UpdateConfigRequestapi_update_config接受三个新字段设置时写入。现有 auth-mode 切换逻辑git_sync_api.py在切出github_oauth时清空oauth_token改造后必须同时清空oauth_refresh_token及两个过期时间而github_oauth分支应接受新的 refresh 字段。可选GitSyncConfigResponse增加has_oauth_refresh_token: bool用于前端判断是否具备自动刷新能力。config.py默认值与 TypedDictget_git_sync_config()对三个新字段一律.get(...)默认None当前实现见 config.py保证旧配置加载不报错、新逻辑能识别无 refresh token的存量用户。Phase 2 详解manager 中的自动刷新引擎新构造参数与锁GitSyncManager当前定义见 git_sync_manager.py新增构造参数project_path: str | None None——这是用于索引配置的project.kiln路径刷新后需要据此读写配置。同时新增self._refresh_lock asyncio.Lock()。_ensure_valid_oauth_token主动刷新的核心该函数同时服务于主动刷新forceFalse与反应式刷新forceTrue核心伪代码设计源自 architecture.mdif self._auth_mode ! github_oauth: return cfg get_git_sync_config(self._project_path) # None-safe expires_at cfg.oauth_token_expires_at refresh_token cfg.oauth_refresh_token if not force: if expires_at is None: return # non-expiring / legacy → no-op if time.time() expires_at - REFRESH_SKEW_SECONDS: return # still valid async with self._refresh_lock: cfg get_git_sync_config(self._project_path) # re-read under lock (double-check) if not force and cfg.expires_at valid: return if cfg.oauth_refresh_token is None: raise AuthExpiredError(...) # nothing to refresh try: result await refresh_access_token(cfg.oauth_refresh_token) except OAuthError as e: raise AuthExpiredError(...) from e update_oauth_tokens(self._project_path, result) # persist rotated tokens self._oauth_token result.access_token # in-memory for this manager几个值得注意的设计点REFRESH_SKEW_SECONDS是在到期前提前刷新的余量避免在令牌即将到期的窗口内发起 git 操作而被拒绝具体数值由实现定义文档未给出固定值。非 OAuth 模式直接返回PAT/SSH 用户零开销、行为完全不变。expires_at is None直接返回GitHub 未返回expires_in或存量旧配置意味着令牌不过期无需刷新。无 refresh token 时抛AuthExpiredError这正是存量 OAuth 用户的正确出路——他们无法被自动续期只能收到清晰的 401 重连提示。接入远程操作fetch()进入现有逻辑前先await self._ensure_valid_oauth_token()。except pygit2.GitError分支变为 auth 与网络分流except pygit2.GitError as e: if _is_auth_error(e) and oauth has refresh not already refreshed: await self._ensure_valid_oauth_token(forceTrue) # reactive await self._run_git(self._fetch_sync) # retry once elif _is_auth_error(e): raise AuthExpiredError(...) from e else: raise RemoteUnreachableError(...) from e若重试后的 fetch 仍抛 auth 错误 → 转AuthExpiredError。commit_and_push()在第一次_push_sync前调用_ensure_valid_oauth_token()对 push 失败同样应用 auth-vs-network 分类当前代码在首次 push 失败后会记录日志并尝试 fetchrebaseretry改造后要确保 auth 拒绝成为AuthExpiredError而不是笼统的冲突/不可达错误见 git_sync_manager.py。ensure_fresh()/ensure_fresh_for_read()透传在现有except RemoteUnreachableError: raise旁新增except AuthExpiredError: raise防止被 catch-allexcept Exception重新包装成RemoteUnreachableError。当前这两个方法的实现见 git_sync_manager.py。registry 与调用点贯通project_pathregistry.py 的get_or_create(...)需要把project_path传进GitSyncManager(...)并在 overwrite 分支现在无条件覆写 token/auth_mode中同步设置existing._project_path project_path防止 manager 复用时携带旧的 project_path。两个调用点middleware.py 的_get_manager_for_request它已经解析出project_path与配置get_git_sync_config(project_path)直接传入即可desktop_server.py 的_start_background_syncs它按project_path遍历git_sync_projectsfor project_path in raw_projects把 key 作为project_path传入。Phase 3 详解错误分类、反应式刷新与 401 映射_is_auth_error认证与网络的分水岭设计文档要求在 git_sync_manager.py 中实现_is_auth_error(exc: Exception) - bool当 pygit2 错误表明凭据被拒绝时返回 True。凭据回调在 clone.py 中会抛pygit2.GitError(Authentication failed. Credentials were rejected by the server.)因此通过大小写不敏感地检查错误消息中是否包含authentication failed/credentials were rejected/401/403来判断其余一律视为网络问题。实际上当前仓库 git_sync_manager.py 已经实现了_AUTH_ERROR_MARKERS与_is_auth_error且fetch()已把认证错误提升为GitAuthError401 语义而非RemoteUnreachableError503 语义。本项目在此基础上进一步引入不可续期的AuthExpiredError语义并让刷新引擎复用该分类。AuthExpiredError不可续期认证的专属异常在 errors.py 中新增当前已有GitAuthError见 errors.pyclass AuthExpiredError(GitSyncError): GitHub authorization expired / revoked and could not be refreshed.middleware401 重连提示在 middleware.py 的ERROR_MAP中新增AuthExpiredError: (401, GitHub authorization expired. Reconnect your GitHub account in the projects Git Sync settings.),同时导入AuthExpiredError。背景同步当前在 background_sync.py 用except Exception吞掉所有异常并记录警告需要确认AuthExpiredError在那里只产生清晰警告、不会让轮询循环崩溃。Phase 4 详解前端贯通新字段先重生成绑定再改手写类型后端请求/响应模型新增字段后第一步是用 generate_schema.sh 重新生成 OpenAPI 绑定并用 check_schema.sh 校验。关键点前端同时存在生成类型与手写类型两套手写类型是生成类型的重复实现两处都必须同步更新。手写类型与 OAuth 令牌路径api.tsOAuthStatusResponse类型当前见 api.ts以及saveConfigapi.ts、updateConfigapi.ts的内联请求类型与请求体统一加入oauth_refresh_token、oauth_token_expires_at、oauth_refresh_token_expires_at。oauth_flow.tsOAuthFlowCallbacks.onSuccess从(token: string)改为负载对象{ oauth_token, oauth_refresh_token, oauth_token_expires_at, oauth_refresh_token_expires_at }轮询代码从 status 响应读取新字段当前在 oauth_flow.ts 只取status.oauth_token。oauth_with_install.tson_success签名与stored_token持有者当前见 oauth_with_install.ts、oauth_with_install.ts改为负载对象check_access原样转发。git_import_wizard_store.tsGitImportWizardState与初始状态加入三个字段保留github_oauth校验。step_credentials.svelte、import_project.svelte、step_complete.svelte把负载带入 store、带入saveConfig请求体。step_branch.svelte只需要 access token 用于 clone/list/test语义不变可继续使用oauth_token。git_sync_status.svelte重连on_success当前见 git_sync_status.svelte把负载传给updateConfig。时间戳是前端不透传opaque pass-through值三个时间戳字段由服务端计算前端从不解释或触发刷新只负责把它们从 OAuth status 搬运到 save/update config。这保证了时区与时钟语义在服务端统一。并发与正确性设计OAuth refresh token 是单次使用的每次刷新后 GitHub 都会轮换一个新 refresh token旧的一个立即作废。这带来两个必须处理的并发问题同一仓库的刷新串行化每个仓库对应一个GitSyncManager单例registry 单例模式见 registry.py后台同步与请求处理共用同一实例。per-manager 的asyncio.Lock_refresh_lock串行化所有刷新保证单次使用的 refresh token 恰好被消费一次。锁内双检double-check协程拿到锁后重新读取配置并再查一次过期时间避免等待中的协程在别人已刷新完成后又重复刷新。同步持久化无事件循环交错update_oauth_tokens是同步 read-modify-write块内没有任何await因此不会在事件循环中跨项目交错执行。错误处理策略可续期的问题静默续期令牌将过期或已过期但 refresh token 可用 → 主动/反应式刷新用户无感知文档场景编号 B2/B3不可续期的认证→AuthExpiredError→ HTTP 401 可操作的重连提示消息该错误后续由独立的git_auth_recovery项目消费并构建恢复 UX网络/远端问题→RemoteUnreachableError→ HTTP 503行为保持不变只在刷新成功后才持久化绝不把部分状态写入配置刷新失败不落盘保留旧令牌与旧过期时间配置保持自洽。测试策略从单元到端到端Pythonpytest与源码同目录app/desktop/git_sync/test_*.py 集成测试oauthexchange_code_for_token解析全部字段并计算expires_atmock httpx缺少expires_in时结果为Nonerefresh_access_token成功路径 令牌轮换 失败时抛OAuthError。用patch(time.time)让过期时间确定性可测。config新字段 round-trip缺少新键的旧配置默认Noneupdate_oauth_tokens只更新令牌字段、对未知project_path是 no-op。manager_ensure_valid_oauth_token覆盖——有效期内无网络调用、处于刷新余量内无调用、过期有 refresh调用刷新、持久化、更新_oauth_token、过期无 refreshAuthExpiredError、非 OAuth 模式no-op、forceTrue并发场景两个 awaiters 只产生一次网络刷新。_is_auth_error分类。fetch()/commit_and_push()反应式刷新 单次重试auth 与网络分支ensure_fresh*透传AuthExpiredError。middlewareAuthExpiredError→ 401 消息RemoteUnreachableError仍为 503。api回调把 refresh 字段存入 flowstatus 返回它们save/update 持久化它们auth-mode 切换清空 refresh 字段。background_syncAuthExpiredError被记录日志且轮询循环存活。前端vitestoauth_flow.test.tsonSuccess收到负载对象status mock 包含新字段。api.test.ts/ wizard store 测试新字段被带入 save/update 请求体。端到端验证运行uv run ./checks.sh --agent-modelint、format、类型检查、pytest、前端 web lint/format/check/test/build、schema check。Schema 重新生成执行app/web_ui/src/lib/generate_schema.sh确认app/web_ui/src/lib/check_schema.sh通过。手工验证可选通过 OAuth 连接一个仓库把配置里的oauth_token_expires_at改为接近过去的值以强制触发主动刷新B2 场景确认一次 fetch 会触发刷新新令牌被持久化且同步继续清空oauth_refresh_token确认下一次操作返回 401 重连消息而非 503。前置检查确认 GitHub App 已开启 Expire user authorization tokensGitHub App 设置 → Optional features——这是 GitHub 愿意签发 refresh token 的前提条件场景 B6否则整个自动刷新机制无从谈起。边界预防与恢复的分工本项目只做预防保持令牌存活与精确报错预防失效时给出正确的 401不包含以下内容它们属于独立的git_auth_recovery项目该消费本项目引入的 401AuthExpiredErrortask-picker 的[Object object]渲染 bug面向用户的恢复 UX重新认证按钮、移除现有并重新同步流程。成功标准对照通过 OAuth 连接的项目在 8 小时/隔夜之后无需任何用户操作仍持续同步约 6 个月后 refresh token 过期或授权被撤销/应用被卸载时用户看到清晰的 reconnect GitHub 错误HTTP 401而非 check your connection503已有 OAuth 用户有 access token 但无存储的 refresh token在令牌过期后得到清晰的 401 重连错误重新连接后自动刷新即刻生效PAT 与 SSH 同步行为完全不变。关键文件速查设计与计划implementation_plan.md、architecture.md、project_overview.md后端核心oauth.py、config.py、git_sync_manager.py、git_sync_api.py、errors.py、middleware.py、registry.py前端api.ts、oauth_flow.ts、oauth_with_install.ts、git_sync_status.svelte赞分享人工智能AI 应用AI 评测RAG微调提示工程开发工具桌面应用【免费下载链接】KilnBuild, Evaluate, and Optimize AI Systems. Includes evals, RAG, agents, fine-tuning, synthetic data generation, dataset management, MCP, and more.项目地址https://gitcode.com/gh_mirrors/ki/Kiln点击查看免费下载相关推荐Kiln 桌面端 GitHub OAuth 令牌自动续期架构解析8 小时过期、静默刷新与 401 精确定位Kiln 桌面端 GitHub OAuth 令牌自动续期架构解析8 小时过期、静默刷新与 401 精确定位 导读 本文围绕 Kiln 仓库中 git_oaut人工智能AI 应用AI 评测RAG微调提示工程开发工具桌面应用MLOpsKiln 桌面端 GitHub OAuth Token 自动续期设计解析从 8 小时失效到 6 个月免干预同步Kiln 桌面端 GitHub OAuth Token 自动续期设计解析从 8 小时失效到 6 个月免干预同步 本文基于 Kiln 仓库中 GitHub OA人工智能AI 应用AI 评测RAG微调提示工程开发工具桌面应用MLOps终极Windows开发环境配置windows-development-environment自动化安装教程终极Windows开发环境配置windows development environment自动化安装教程 windows development envir上一篇5步开启PC畅玩Switch游戏yuzu模拟器终极配置指南下一篇3步找回遗忘压缩包密码的终极免费工具指南ArchivePasswordTestTool完整教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑