Aspire 定时工作流故障监视器:monitor-scheduled-workflows 的设计、配置与源码解析
【免费下载链接】aspireAspire is the tool for code-first, extensible, observable dev and deploy.项目地址: https://gitcode.com/GitHub_Trending/as/aspire
Aspire 仓库中有大量无人值守的定时 GitHub Actions 工作流(刷新 manifest、更新依赖、清理部署、回合并 release 等)。一旦它们失败,GitHub 只会邮件通知最后修改该工作流文件的人,故障可能数天无人察觉。本文以 docs/ci/monitor-scheduled-workflows.md 为核心,完整讲解monitor-scheduled-workflows看门狗(watchdog)的调度机制、观察列表配置、Issue 去重契约、关闭策略与权限模型,并深入 monitor-scheduled-workflows.js 与共享引擎 tracking-issue.js 的源码实现及其单元测试,帮助你理解并复用这套"每工作流一个去重 Issue"的自动化故障上报模式。
要解决的问题:定时任务失败的"静默盲区"
文档开篇即点明动机:仓库中多个定时工作流(generate diffs、refresh manifests/SDKs、update models/dependencies、clean up deployments、retrain the labeler、backmerge releases 等)长期无人值守运行。当其中一个失败时,GitHub 仅向"最后编辑该工作流文件的人"发送邮件——在大型团队中,这个人往往早已不再关注这条流水线,一个坏掉的定时任务可以沉默地躺上数天。
monitor-scheduled-workflows就是为此设立的"看门狗":它监视一组指定工作流,对每个工作流维护唯一一个去重(deduplicated)的 GitHub Issue,在失败时创建(file)、在重复失败时追加评论(update)、在恢复成功时关闭(close)。文档明确指出,这是仓库内部 AzDO build notifier 在 GitHub Actions 侧的对应物,两者复用同一套file → update → close-on-green契约,区别仅在于按workflow(而非按 branch)作为键。
调度:每 2 小时轮询 + 3 小时回溯窗口 + dry-run 手动触发
工作流定义在 monitor-scheduled-workflows.yml,核心调度与执行结构如下:
on: schedule: - cron: '0 */2 * * *' # every 2 hours workflow_dispatch: inputs: dry_run: description: 'Inspect and log intended issue actions without mutating GitHub' required: false default: false type: boolean permissions: contents: read # checkout to require the local .js module actions: read # list workflow runs / conclusions issues: write # file / comment / close automation-broken issues # One watchdog run at a time; a backlog would only re-evaluate the same state. concurrency: group: monitor-scheduled-workflows cancel-in-progress: false jobs: monitor: runs-on: ubuntu-latest if: ${{ github.repository_owner == 'microsoft' }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: main persist-credentials: false - name: Evaluate scheduled workflows uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }} with: script: | const dryRun = process.env.DRY_RUN === 'true'; await require('./.github/workflows/monitor-scheduled-workflows.js').run({ github, context, core, dryRun });几个关键设计点:
- 每 2 小时一次(
cron: '0 */2 * * *'),并支持workflow_dispatch手动触发。手动触发可传入dry_run布尔值:此时工作流只记录本应采取的 Issue 操作(would file / would comment / would close),不修改 GitHub 上任何内容——这在调试监视器自身行为时非常有用。 concurrency组串行化:同一时刻只允许一个看门狗运行在途。注释写得很直白——排队积压的运行只会重复评估同一状态,毫无收益。dry_run通过环境变量传递而非直接插值进脚本,避免用户输入被内联进github-script,脚本内只做process.env.DRY_RUN === 'true'的安全判断。
为什么用 3 小时轮询窗口
这是该设计中最容易被忽视、却决定正确性的细节。看门狗每次运行时,只处理"3 小时轮询窗口内更新过的已完成定时运行",并按时间从旧到新处理。源码中窗口是硬编码常量(monitor-scheduled-workflows.js):
// Conclusions that count as "the workflow is broken". `cancelled` is excluded: // operator cancellation (and concurrency-superseded runs) is not a workflow defect, // and firing on it would create noise. `timed_out` is NOT excluded — a run that // hits its timeout is treated as broken. const FAILURE_CONCLUSIONS = new Set(['failure', 'timed_out', 'startup_failure']); const SUCCESS_CONCLUSIONS = new Set(['success']); const WORKFLOW_RUN_PAGE_SIZE = 100; // The watchdog runs every two hours. Look back three hours so ordinary GitHub // schedule/queue delay cannot create a gap where a completed run is never seen. const POLLING_WINDOW_MS = 3 * 60 * 60 * 1000;窗口(3 小时)刻意大于调度间隔(2 小时),目的是让相邻两次看门狗 tick 之间存在重叠区:这样即使 GitHub 的调度/排队出现延迟,也不会出现"某次已完成运行永远落在两个 tick 之间的缝里"的盲区。文档还强调,重叠不会导致重复上报——因为每次失败运行记录为评论时携带独立的隐藏运行标记,重复观察到的同一运行会被去重(见下文 Dedup 一节)。
窗口过滤与排序由纯函数selectRunsForPollingWindow实现,按updated_at ?? run_started_at ?? created_at取时间戳,落在[now - 3h, now]内的运行被保留并升序排序:
function selectRunsForPollingWindow(runs, { now = new Date(), pollingWindowMs = POLLING_WINDOW_MS } = {}) { const nowTimestamp = now.getTime(); const cutoff = nowTimestamp - pollingWindowMs; return (runs ?? []) .filter(run => { const timestamp = getRunTimestamp(run); return timestamp !== null && timestamp >= cutoff && timestamp <= nowTimestamp; }) .sort((left, right) => getRunTimestamp(left) - getRunTimestamp(right)); }观察列表:配置与代码分离,两类条目语义不同
文档强调:观察列表不在工作流脚本里,而是独立的配置文件 monitor-scheduled-workflows.config.json,条目格式为{ file, name, enabled?, selfReports?, labels? }数组。当前仓库的实际观察列表为:
{ "watched": [ { "file": "generate-api-diffs.yml", "name": "Generate API Diffs" }, { "file": "generate-ats-diffs.yml", "name": "Generate ATS Diffs" }, { "file": "refresh-manifests.yml", "name": "Refresh Manifests" }, { "file": "update-dependencies.yml", "name": "Update Dependencies" }, { "file": "update-ai-foundry-models.yml", "name": "Update AI Foundry Models" }, { "file": "update-azure-vm-sizes.yml", "name": "Update Azure VM Sizes" }, { "file": "update-aspire-skills-bundle.yml", "name": "Update Aspire Skills Bundle" }, { "file": "deployment-cleanup.yml", "name": "Deployment Cleanup" }, { "file": "labeler-cache-retention.yml", "name": "Labeler Cache Retention" }, { "file": "warm-cli-e2e-image-cache.yml", "name": "Warm CLI E2E Image Cache" }, { "file": "locker.yml", "name": "Lock Threads" }, { "file": "backmerge-release.yml", "name": "Backmerge Release to Main" }, { "file": "sync-main-to-release-14.yml", "name": "Sync Main to Release 14.0" }, { "file": "tests-outerloop.yml", "name": "Outerloop Tests", "selfReports": true }, { "file": "tests-quarantine.yml", "name": "Quarantined Tests", "selfReports": true }, { "file": "tests-daily-smoke.yml", "name": "Daily CLI Smoke Tests", "selfReports": true, "labels": ["area-cli"] }, { "file": "deployment-tests.yml", "name": "Deployment E2E Tests", "selfReports": true, "labels": ["area-testing", "deployment-e2e"] } ] }文件头部的$schema-note字段本身就是一份内联使用手册:条目默认处于监视状态(watched by default);enabled: false可在不删除条目的情况下停止监视;selfReports: true表示该工作流会在流水线内自行上报其常规失败,看门狗随后只兜底startup_failure和timed_out;可选的labels[]会附加到所创建的 Issue 上。
配置文件中的两类条目对应两种完全不同的监视语义:
全监视条目(full-watch)
failure结论无歧义意味着"坏了"的工作流。看门狗在任何 failure 结论下都会提 Issue。包括:
generate-api-diffs、generate-ats-diffsrefresh-manifestsupdate-dependencies、update-ai-foundry-models、update-azure-vm-sizes、update-aspire-skills-bundledeployment-cleanuplabeler-cache-retentionwarm-cli-e2e-image-cachelockerbackmerge-release
兜底条目(backstop,selfReports: true)
这些工作流自身内置了if: failure()的门控 reporter job,会在流水线内为自己的常规失败提 Issue(例如 specialized-test-failure-issues.md 描述的 outerloop/quarantine 测试失败 reporter,以及 pipeline-failure-issues.md 描述的每日冒烟/部署测试 reporter)。对这类工作流,看门狗只记录startup_failure和timed_out两种结论——恰好是流水线内 reporter 无法捕获的两种——并且绝不记录普通failure:如果记录,就会在第二个标记名下重复提 Issue(double-file)。
tests-outerloop、tests-quarantinetests-daily-smoke、deployment-tests
增删改观察条目的操作规则
文档给出四条明确的配置操作规范:
| 操作 | 做法 |
|---|---|
| 添加全监视工作流 | 加一个{ "file", "name" }条目(缺省即被监视) |
| 为自上报工作流添加兜底 | 加"selfReports": true |
给所提 Issue 附加标签(automation-broken之外) | 加"labels": [ ... ]数组 |
| 停止监视某工作流(不删除) | 设"enabled": false,禁用条目会被跳过并记录日志 |
"禁用即跳过"的逻辑在源码中是一个简单的过滤函数,enabled缺省视为启用(monitor-scheduled-workflows.js):
function selectEnabled(config) { const watched = Array.isArray(config?.watched) ? config.watched : []; return watched.filter(entry => entry && typeof entry.file === 'string' && entry.enabled !== false); }为什么兜底条目只记录 startup_failure / timed_out
这一节是理解整个设计的核心。流水线内 reporter 本质是一个门控在if: failure()上的 job,而 GitHub 有两种结论会绕过它:
startup_failure:运行根本没有启动任何 job(坏掉的 YAML、无法解析的uses:、失效的 runner),于是 reporter job 自身也不会运行;timed_out:job 级超时属于cancelled-class状态,failure()表达式为 false,reporter job 因此不会运行。
而普通failure(包括以failure形式呈现的 step 级超时)会触发 reporter,所以看门狗把这些留给流水线内 reporter 处理。
源码中用常量把这层语义固化下来(monitor-scheduled-workflows.js):
const BACKSTOP_CONCLUSIONS = new Set(['startup_failure', 'timed_out']);决策入口是纯函数decideAction:传入结论、现有 Issue 与"哪些结论算坏"的集合,返回record/close/noop三选一:
function decideAction({ conclusion, issue, failureConclusions = FAILURE_CONCLUSIONS }) { const normalized = typeof conclusion === 'string' ? conclusion.toLowerCase() : null; if (normalized !== null && failureConclusions.has(normalized)) { return issue ? { action: 'record', reason: `latest run concluded '${normalized}'; recording on issue #${issue.number}` } : { action: 'record', reason: `latest run concluded '${normalized}'; no open issue` }; } if (normalized !== null && SUCCESS_CONCLUSIONS.has(normalized)) { return issue ? { action: 'close', reason: `latest run concluded 'success'; closing issue #${issue.number}` } : { action: 'noop', reason: 'latest run succeeded; nothing open' }; } // null (no completed run yet), 'cancelled', 'skipped', 'neutral', etc. return { action: 'noop', reason: `latest conclusion '${normalized ?? 'none'}' is not actionable` }; }注意run()主循环中如何按条目类型切换失败集合:const failureConclusions = wf.selfReports ? BACKSTOP_CONCLUSIONS : FAILURE_CONCLUSIONS;——selfReports条目在普通failure时落入noop分支,正好实现"绝不为它提第二个 Issue"的契约。
有意排除在观察范围之外的工作流
文档还专门列出了"刻意不监视"的对象及理由,这部分同样是设计约束的一部分:
backmerge-release:此处为全监视;但它还会在green(成功)运行下提"merge conflicts" Issue——那是其自身工作流内处理的另一件事,与看门狗无关。workflow_call构件(如tests.yml、run-tests.yml):它们的失败会反映在调用方(caller)上,信号属于调用方,不应在看门狗处重复上报。ci.yml:red-main 的 push 失败由ci.yml自己提 Issue(并自关闭),见 ci-failure-issues.md。- Agentic 的
*.lock.yml工作流:gh-aw有自己的错误上报机制。
会创建什么样的 Issue:标题、标签、标记与评论
当一个被监视工作流在main分支、看门狗轮询窗口内出现了结论为failure、timed_out或startup_failure的已完成定时运行时,会触发如下 Issue 契约:
- 标题:
Scheduled workflow failing: <display name> - 标签:
automation-broken(由工作流幂等地创建) - 正文首行标记:隐藏的 HTML 注释
<!-- automation-broken:<workflow-file> -->,作为去重键
几个实现层面的关键约定:
- 每个工作流同时至多存在一个 open Issue。正文是提 Issue 时一次性写定的固定描述(隐藏标记 + 说明文字),之后不再改写正文;每次新观察到的失败运行都记录为一条评论,携带运行链接、commit 与结论——正是评论触发了 @通知。
- 评论按运行去重:扫描器可能在多个 tick 观察到同一次失败运行。每条评论内嵌隐藏的
<!-- run:<id> -->标记;已存在对应评论的运行是 no-op,直到更新的运行完成。 cancelled与skipped被刻意忽略——操作者取消和跳过的运行不是工作流缺陷。反之,timed_out被视为失败并会提 Issue。- 对backstop 条目(
selfReports: true),普通failure同样被忽略(其流水线内 reporter 拥有它);只有startup_failure和timed_out会提 Issue,且正文措辞会明确说明这一点,以区别于流水线内 reporter 提的 Issue。 - Issue 携带
automation-broken加上条目级labels,并被打上autoClose:true戳。
源码中的 Issue 内容构造
标记、标题与正文的构造函数都是纯函数。正文链接指向仓库 Actions 页面的工作流视图,并按selfReports切换导语措辞(monitor-scheduled-workflows.js):
function buildIssueBody({ marker, displayName, workflowFile, selfReports = false }) { const link = `\`${workflowFile}\``; const lead = selfReports ? `The scheduled workflow ${link} (**${displayName}**) had a run that **failed to start or timed out**. Its normal failures are reported separately by an in-pipeline job; this issue backstops runs that never produced a result.` : `The scheduled workflow ${link} (**${displayName}**) is failing.`; return tracking.buildBody({ marker, autoClose: true, lead, note: [ 'Filed and updated automatically by the scheduled-workflow watchdog. Each', 'failed run is added as a comment below, and the issue is **closed', 'automatically** on the next successful run.', 'See [docs/ci/monitor-scheduled-workflows.md](https://link.gitcode.com/i/d122c64f7c7478ab949782b3b825ff4b).', ], }); }每次失败运行产生的评论格式(formatComment):
// Comment recorded per newly-observed failed run. // failure: { runUrl, runNumber, sha, conclusion } function formatComment({ runUrl, runNumber, sha, conclusion }) { const runLink = runUrl ? `run #${runNumber ?? '?'}` : `run #${runNumber ?? '?'}`; const shaPart = sha ? ` (commit \`${String(sha).slice(0, 8)}\`)` : ''; return `The scheduled run concluded \`${conclusion}\` in ${runLink}${shaPart}.`; }单元测试 MonitorScheduledWorkflowsTests 精确锁定了这些输出,例如断言buildMarker("generate-api-diffs.yml")必须等于"<!-- automation-broken:generate-api-diffs.yml -->"、标题必须为"Scheduled workflow failing: Generate API Diffs"、selfReports条目正文必须包含"failed to start or timed out"与"reported separately"、且所有正文都必须包含<!-- autoclose:true -->戳。
什么会被关闭:close-on-green
当轮询窗口内最新的已完成定时运行结论为success时,该工作流对应的 openautomation-brokenIssue 会被追加一条 "latest run succeeded" 评论,并以state_reason: completed关闭。
源码中的关闭路径有两个前置守卫(monitor-scheduled-workflows.js):
const autoClose = tracking.readAutoClose(issue.body); if (autoClose !== true) { core.info(`Issue #${issue.number} for ${wf.file} does not opt into auto-close; leaving it open.`); continue; } // ... await tracking.closeIssue(github, owner, repo, issue.number); await tracking.addComment(github, owner, repo, issue.number, `Latest run succeeded (run #${newest.run_number}). Closing automatically.`);readAutoClose是防御式解析:正文中找不到或解析不出<!-- autoclose:true/false -->戳时返回null,调用方必须把null当作"不自动关闭"处理——这样人工编辑过的正文、或在引入戳记之前创建的旧 Issue,都不会被看门狗从 triager 手中抢着关掉。共享引擎 tracking-issue.js 中的实现:
function readAutoClose(body) { if (typeof body !== 'string') { return null; } const match = /<!--\s*autoclose:(true|false)\s*-->/i.exec(body); if (match === null) { return null; } return match[1].toLowerCase() === 'true'; }去重(Dedup):为什么刻意不用 Search API
这是文档中一个很有工程判断力的决定:Issue 查找使用GET /issues?labels=automation-broken&state=open(强一致的 list API)加上本地正文标记过滤,而不是Search API——因为 Search API 的最终一致性窗口可能让近乎同时的两次运行各自看到"0 命中",从而重复提 Issue。源码 tracking-issue.js 的注释与实现印证了这一点,并且顺手处理了 REST 模型中"PR 也是 issue"的坑:
// Uses the (strongly-consistent) list // API rather than Search, whose eventual-consistency window would let // near-simultaneous pollers each see "0 hits" and file duplicates. async function listIssuesByLabel(github, owner, repo, label, { state = 'all' } = {}) { const items = await github.paginate(github.rest.issues.listForRepo, { owner, repo, labels: label, state, per_page: 100, }); // listForRepo returns pull requests too (they are issues in the REST model). // Exclude them so a labeled PR whose body happens to carry a tracking marker is // never mistaken for the managed issue and then commented/closed in its place. return items.filter(item => !item.pull_request); }若极端情况下出现两个 open Issue 携带同一标记(例如有人手动创建了一个),编号最小(最旧)的视为 canonical,由 TrackingIssueTests 中的FindOpenIssueForMarkerReturnsOldestMatch用例固化(三个候选 issue 11/40/88 中必须返回 11)。
此外,看门狗一次性列出含 closed 状态的 Issue 并跨条目复用,原因是:某工作流再次失败时应当重开既有 canonical tracker,而不是提一个重复的新 Issue;recordRun引擎对 closed 状态命中会自动reopenIssue。dry-run 模式下也会模拟这套状态迁移(占位 issue 推入本地列表、closed 置为 open),保证 dry-run 日志反映真实的多 tick 行为。
权限与鉴权:最小 token,不触发下游
文档给出的权限模型:
- job 使用默认
GITHUB_TOKEN,仅需actions: read(列出运行/结论)与issues: write(提 Issue/评论/关闭);contents: read用于 checkout 本地的.js模块(脚本通过require('./.github/workflows/monitor-scheduled-workflows.js')加载,因此 YAML 只有一层薄壳,逻辑全部在被测试的 JS 模块里)。 - 无需 App token:Issue 创建在同一仓库内;且看门狗刻意不触发下游自动化——避免"监视器自己制造的 Issue 又触发别的机器人"的反馈环。
- 标签
automation-broken由ensureLabel幂等创建(422 视为已存在并静默忽略)。
为什么它"从不喧闹地失败"
run()主循环中,对每个工作流的运行列表查询都包在try/catch里:单个无法读取的工作流只会记一条 warning 并continue跳过,而不是让整个看门狗运行失败(monitor-scheduled-workflows.js):
} catch (error) { core.warning(`Could not list runs for ${wf.file}: ${error.message}`); continue; }同时,查询运行列表时显式带event: 'schedule'过滤——源码注释解释得很清楚:被监视的工作流常常同时有workflow_dispatch(个别如warm-cli-e2e-image-cache.yml还有push:触发器)。若不加该过滤,轮询窗口内的一次手动/push 成功运行会自动关闭真实的定时失败 Issue(掩盖这个看门狗专门要抓的静默故障),而一次手动/push 失败则会提一个假 Issue。
const runs = await github.rest.actions.listWorkflowRuns({ owner, repo, workflow_id: wf.file, branch: 'main', event: 'schedule', status: 'completed', per_page: WORKFLOW_RUN_PAGE_SIZE, });逻辑分层与测试:可复用的 tracking-issue 引擎
文档最后一节描述了代码分层,这也是整个方案可迁移性的来源:
- 通用引擎tracking-issue.js:仓库无关(repo-agnostic)的 tracking-issue 机制——标记去重查找、按运行去重的评论记录循环、octokit 原语(ensureLabel / listByLabel / create / comment / close / reopen / hasCommentForRun),以及唯一承载去重契约的编排函数
recordRun。它不关心任何仓库、标签、工作流或产品,刻意保持纯净。 - 看门狗专属模块monitor-scheduled-workflows.js:标记命名空间(
automation-broken:前缀)、Issue 标题/正文、每运行的失败评论、record/close/noop 决策(均为纯函数),以及run()编排器——读配置、循环轮询、驱动引擎。由工作流的github-script步骤调用。
这套机制在仓库中被多个 reporter 共享:specialized-test 失败 reporter、nightly-pipeline 失败 reporter、red-main CI reporter 都复用同一引擎,各自行使自己的标记与文案策略。
测试链路是"Node harness + C# xUnit"的组合:
| 层 | 文件 | 作用 |
|---|---|---|
| 引擎单测 | TrackingIssueTests | 通过 tracking-issue.harness.js 以 Node 子进程调用纯 helper,断言标记去重、recordRun的 find-or-create + 评论去重等契约 |
| 看门狗单测 | MonitorScheduledWorkflowsTests | 通过 monitor-scheduled-workflows.harness.js 驱动buildMarker/buildIssueTitle/decideAction/selectEnabled/buildIssueBody/formatComment六个操作 |
| 集成测试 | MonitorScheduledWorkflowsIntegrationTests | 通过 monitor-scheduled-workflows.integration.harness.js 以 fake 驱动run()全流程 |
harness 的形态值得借鉴:C# 测试把{ operation, payload }写成 JSON 文件,Node 以 CLI 方式读取并回传 JSON 结果,从而把 JS 纯函数纳入仓库统一的 xUnit 测试体系。decideAction的关键用例矩阵包括:
- 普通条目:
failure/timed_out/startup_failure且无 Issue →record;有 Issue → 仍record("已记录"的去重交给下游recordRun扫评论完成); success+ 有 Issue →close;success+ 无 Issue →noop;cancelled/skipped/null→ 一律noop;selfReports条目:startup_failure→record、timed_out→record、failure→noop(避免 double-file)、success→close。
修改时的对齐清单
文档结尾给维护者的约束同样值得记住:当你改动工作流的 job/step 名称或模块的导出契约时,必须保持以下五者对齐——工作流 YAML、.js模块、harness、测试、以及本文所对应的文档 docs/ci/monitor-scheduled-workflows.md。这是该仓库对"脚本 + 文档 + 测试"三位一体的强制约定。
总结:一个可复用的"无人值守自动化故障上报"模式
纵观 docs/ci/monitor-scheduled-workflows.md 及其实现,这套看门狗沉淀了一套清晰可复用的模式:
- 观察列表外置为 JSON 配置,与脚本解耦,支持
enabled/selfReports/labels三个正交维度; - 每主题一个 canonical Issue,以正文中的隐藏 HTML 标记为去重键,查找走强一致的 list API 而非最终一致的 Search API;
- 正文只写一次,失败以评论累积,评论内嵌每运行标记实现跨 tick 去重,评论本身即通知载体;
- 结论分类学驱动决策:
failure/timed_out/startup_failure算坏,success触发关闭,cancelled/skipped刻意忽略;对自上报工作流只兜底流水线内 reporter 够不着的两种结论; - 调度间隔 < 轮询窗口,用重叠消除静默缝隙,用运行级去重抵消重叠带来的重复观察;
- dry-run 手动触发 + 单实例并发 + 逐工作流容错,让监视器自身安静、可验证、不会成为新的故障源。
如果你在自己的仓库中也有"失败后无人知晓"的定时任务,这套 file → update → close-on-green 的契约、以及"纯决策函数 + Node harness + xUnit"的测试组织方式,都可以直接从 Aspire 仓库的 .github/workflows/monitor-scheduled-workflows.js 与 .github/workflows/tracking-issue.js 中借鉴移植。
【免费下载链接】aspireAspire is the tool for code-first, extensible, observable dev and deploy.项目地址: https://gitcode.com/GitHub_Trending/as/aspire
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考