feat(acp): support desktop qwen integration#4728
Conversation
📋 Review SummaryThis PR expands ACP support by exposing command/skill metadata, improving session handling with mid-turn message draining, and tightening the development CLI launcher. The changes are well-structured and focused on non-desktop code as claimed. The implementation demonstrates solid attention to type safety and backward compatibility. 🔍 General Feedback
🎯 Specific Feedback🟡 High
🟢 Medium
🔵 Low
✅ Highlights
|
|
Thanks for the PR, @DragonnZhang! Template looks good — all required sections are present and filled in ✓ On direction: ACP desktop integration is squarely within the product's channel strategy. Qwen Code already supports VSCode, ACP, SDK, and CI — adding a rich set of On approach: This PR has grown significantly since the last triage pass — from ~3.5k to ~9.5k lines across 28 files. The scope now covers three distinct concerns bundled together:
The core ACP work (item 1) is well-scoped and necessary. Items 2 and 3 could potentially be separate PRs — they're related to "desktop" thematically but don't depend on the ACP changes. Not blocking, but worth considering for future PRs to keep reviews manageable. One structural flag: the Moving on to code review. 🔍 中文说明感谢贡献 @DragonnZhang! 模板完整 ✓ 方向判断: ACP 桌面集成完全符合产品的 channel 策略。Qwen Code 已支持 VSCode、ACP、SDK、CI——为桌面客户端新增一组丰富的 方案评估: 这个 PR 自上次 triage 以来大幅增长——从约 3.5k 到约 9.5k 行,跨 28 个文件。范围现在涵盖三个不同的关注点:
核心 ACP 工作(第 1 项)范围合理且必要。第 2、3 项理论上可以独立成 PR——与 "desktop" 主题相关但不依赖 ACP 改动。不 block,但建议未来 PR 考虑拆分以保持 review 可控。 结构性提醒: 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
|
Code review: The production code is well-structured for its size. The ACP extMethod handlers in No critical blockers found. Two observations worth noting:
Lockfile check: Clean — no unexpected dependency additions or removals. ✓ Unit tests: All 478 tests pass across 6 test files (2 skipped). Real-scenario testing (tmux)This PR adds ACP extMethods for desktop client integration — these handlers are invoked by the desktop app via the ACP protocol, not directly from the CLI. So tmux testing focuses on verifying no regressions in the CLI surface. CLI starts, responds correctly, help output shows all expected commands. No regressions in the CLI surface. The ACP extMethod handlers themselves require a desktop client to invoke and cannot be tested via CLI alone. 中文说明代码审查: 生产代码结构良好,考虑到其规模。 未发现 critical blocker。两点观察:
Lockfile 检查: 干净 ✓ 单元测试: 6 个测试文件共 478 个测试全部通过(2 个跳过)。 tmux 验证: CLI 正常启动、响应正确、help 输出显示所有预期命令。CLI 层面无回归。ACP extMethod handler 需要桌面客户端调用,无法仅通过 CLI 测试。 — Qwen Code · qwen3.7-max |
|
This PR has grown considerably since the last triage pass — from ~3.5k to ~9.5k lines — but the core remains sound. Let me lay out where I land after seeing the full picture. The ACP extMethod handlers are the heart of this PR, and they're well done. Consistent patterns, solid security (SSRF blocking, path traversal prevention, atomic writes, secret redaction), good error handling. My independent proposal before reading the diff would have covered the same extMethods — provider listing, skill management, settings CRUD, MCP server config, hooks, memory, permissions. The PR didn't miss a simpler path for any of these; they're all necessary for the desktop client to function. The test results back it up: 478 tests pass, tmux shows no CLI regressions, the build is clean. My reservations are about scope, not quality. This PR bundles three things that could have been three PRs: the ACP handlers (core, necessary, hard to split), the desktop sync script (useful tooling, but orthogonal), and the desktop release workflow (CI/CD, also orthogonal). None of them are wrong to include — they're all "desktop" themed — but the combined size (~9.5k lines, 28 files) makes this harder to review than it needs to be. For future work, I'd suggest splitting tooling and CI into separate PRs. The Net: the feature is real, the implementation is solid, tests pass, and the security posture is good. The scope bundling is a process concern, not a code concern. Approved. ✅ 中文说明这个 PR 自上次 triage 以来大幅增长——从约 3.5k 到约 9.5k 行——但核心依然扎实。看完全局后,以下是我的判断。 ACP extMethod handler 是 PR 的核心,做得不错。一致的模式、扎实的安全性(SSRF 阻止、路径遍历防护、原子写入、密钥脱敏)、良好的错误处理。我在读 diff 前的独立提案会覆盖相同的 extMethod——provider 列表、skill 管理、设置 CRUD、MCP 服务器配置、hooks、memory、permissions。PR 没有遗漏更简路径;这些对桌面客户端都是必要的。 测试结果支持:478 个测试通过,tmux 显示 CLI 无回归,构建干净。 我的顾虑在于范围而非质量。这个 PR 捆绑了三件本可以分开的事:ACP handler(核心、必要、难拆分)、desktop 同步脚本(有用的工具但正交)、desktop 发布工作流(CI/CD,也正交)。包含它们没有错——都与 "desktop" 相关——但合计规模(约 9.5k 行、28 文件)使 review 比必要更难。建议未来将工具和 CI 拆分为独立 PR。
总结:功能真实,实现扎实,测试通过,安全姿态良好。范围捆绑是流程问题而非代码问题。批准。✅ — Qwen Code · qwen3.7-max |
pomelo-nwu
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
Verification Report — PR #4728Commit: Test Results
Note on Initial Core Test RunThe first run of Test File Breakdown
Execution Environment
VerdictAll 6 checks pass. 414 total tests (8 core + 406 cli) cover the changed ACP agent, session, config, provider preset, and PTY utility files. No regressions detected. |
wenshao
left a comment
There was a problem hiding this comment.
Two blocking issues in the new skill-management RPC surface — a path traversal via the skill slug (can write into / rm -rf ~/.qwen), and a lossy YAML rewrite on skill enable/disable that strips comments and corrupts nested hooks: blocks — plus several suggestions. Details inline.
Security - Reject path-traversal skill slugs (`.`/`..`) and assert the resolved skill dir stays under `<globalQwenDir>/skills` before install/delete, so a crafted slug can no longer write into or delete the global config dir. - Restrict skill `sourceUrl` to the GitHub host set and use `redirect: 'manual'` in `fetchBytes` to close the SSRF vector toward internal/metadata endpoints. - Decompress skill archives asynchronously with compressed/decompressed size caps (and a Content-Length guard) to avoid event-loop blocking and OOM from malicious archives. - Stop returning raw provider API keys over the ACP wire: expose `hasApiKey` in `qwen/providers/list` and let `qwen/providers/connect` fall back to the stored key when the client omits it. Correctness - Toggle skill `disable-model-invocation` via a surgical frontmatter text edit instead of a lossy minimal-YAML round-trip, preserving comments and nested `hooks:` blocks. - Latch `midTurnDrainUnavailable` on the JSON-RPC code (-32601) since the ACP SDK rejects with a raw error object, not an `Error`. - Add `'auto'` to the `tools.approvalMode` enum so the supported mode is selectable. - Treat an out-of-range `setHook` index as an append to avoid sparse arrays that serialize to `null` in settings.json. - Reload `setMemory` through a fresh settings object like the other mutation handlers, and drop the redundant post-`setValue` reload. - Add the missing `modelInvocable` field to the companion `availableSkillDetails` type. Adds regression tests for slug traversal, frontmatter preservation, `auto` approval mode, and provider key reuse.
…correctness) - Skills: require HTTPS source URLs (reject http: to prevent MITM of skill content), add redirect:'manual' + Content-Length pre-check to the tarball and GitHub API fetches (SSRF/DoS), clean the skill dir before reinstall to avoid orphaned files, log the API->archive fallback instead of swallowing the error, and bound deleteGlobalSkill's recursive rm to the validated skill directory. - Settings: reject out-of-range hook indices in removeHook; label merged MCP servers/hooks with their real user/workspace scope (dedupe MCP by name, workspace wins) instead of mislabeling everything as workspace; collapse setRules' double settings load into one (removes a concurrency window). - Session: derive availableSkills from availableSkillDetails so bundled skills appear in both; surface partial/replayError from loadUpdates when history replay aborts midway.
- Export normalizeCoreSettingValue and add tests for all four type branches (boolean, number with min, enum, string) including the invalid-value paths. - Make extractFilesFromTarGz's size limits injectable (defaults unchanged) and export it; add tests for the happy path plus the three DoS guards (compressed-too-large, decompression failure, decompressed-too-large).
Add Session tests verifying #drainMidTurnUserMessages latches off after a permanent JSON-RPC -32601 error (drain not retried on the next tool batch) but stays enabled after a transient error (drain retried).
Add tests for getCore (happy), setMcpServer (missing-name + invalid-transport validation, valid persist), removeMcpServer (missing-name validation, removal), setHook (invalid-event validation, valid append), removeHook (negative and out-of-range index validation), and setExtensionSetting (required-param validation). Adds a forScope() stub to the makeCoreSettings test helper so the scope-aware handlers can read existing mcpServers/hooks.
- qwen/permissions/setRules: validate scope/ruleType and persist normalized rules to the requested scope. - qwen/session/loadUpdates: reject invalid sessionId, return empty updates when no conversation exists, replay history and lift _meta.timestamp to the top level, and surface partial + replayError when replay throws. Adds a module mock for HistoryReplayer to drive the success and failure paths.
- Skill install is now atomic: stage files in a sibling temp dir and swap with a single rename, so a mid-write failure no longer deletes the old skill and leaves a partial install. - setGlobalSkillEnabled refuses to write to anything but the validated SKILL.md manifest (mirrors the deleteGlobalSkill guard). - parseGitHubBlobSkillUrl is HTTPS-only, consistent with assertAllowedSkillSourceUrl. - The GitHub Contents-API directory walk is bounded (max depth, file count, and cumulative bytes) and validates each download_url through the host allowlist / HTTPS check before fetching (SSRF + DoS parity with the archive path). - syncLivePermissionManagers isolates per-session failures so one broken permission manager can't abort syncing the rest. - getMemoryPaths (resolveQwenMemoryPaths) is now resolve-only: no ensureMemoryFile / fs.mkdir side effects on a read query (and no mkdir against a client-supplied projectRoot).
- Follow GitHub CDN redirects safely: fetchAllowedGitHub follows 3xx hops but validates each target is HTTPS on an allowed *.githubusercontent.com/github host, fixing legit 302s that redirect:'manual' surfaced as download failures while keeping the SSRF guard. URI-encode the archive ref. - setHook/removeHook reject non-integer indices (a float like 1.5 created a sparse, non-integer array property that JSON.stringify silently dropped). - setMcpServer restores the real stored secret when a client echoes back the __redacted__ sentinel, instead of persisting the literal sentinel over an API key / auth header. - normalizeOptionalNumber rejects 0 to match its 'positive number' message and readPositiveNumber. - extractFilesFromTarGz treats directoryPath '.' (root SKILL.md) as the empty prefix so root-level skills extract. - Tests: control-char stripping, redacted-secret restore, non-integer index.
Local Verification ReportBranch: Unit Tests
Note: ESLintFiles linted: TypeScript Type Check
Typecheck delta analysis (main: 39 errors → PR: 42 errors):
Whitespace CheckSummary
All verification checks pass. The PR is ready for merge. Verified locally by wenshao |
Resolve additive conflicts: keep both the mid-turn-drain const and main's BackgroundNotification queue types in Session.ts; combine availableSkillDetails with main's source/qwenDiscreteMessage/rewritten/backgroundTask fields in acpTypes.ts; keep both the mid-turn-drain tests and main's sleep-inhibitor wrap test in Session.test.ts.
- Redact command-hook env and http-hook headers in getCore (same secret class as MCP) and restore the __redacted__ sentinel on setHook, mirroring the MCP read/write scheme. - Drop *.github.io from the redirect allowlist (user-controlled GitHub Pages). - Strip the C1 range + U+2028/U+2029 (not just C0/DEL) from string settings so the outputLanguage prompt-injection guard covers all line terminators. - Fix the copy-pasted 'setValue already persisted' comment on setExtensionSetting (it persists via updateSetting). - Guard #drainMidTurnUserMessages against a JSON-RPC result:null (don't throw a TypeError that gets misclassified as a transient error). - Tests: hook secret redaction + restore, setHook in-place/append index branches, and skill-install reject paths (http, non-GitHub host, lookalike host).
- fetchAllowedGitHub: export it and cover no-redirect, allowed-CDN follow,
disallowed-host reject, non-https reject, max-redirects, and relative-Location
resolution (via a stubbed global fetch).
- buildAvailableCommandsSnapshot: combined case where both skillManager skills
and a skill slash-command contribute, asserting availableSkills lists both and
stays in lockstep with availableSkillDetails.
- SkillTool: disabled-skill delegation path where the executor returns { error }.
- config: --acp --channel desktop keeps the explicit channel (the desktop
invocation that exercises the !channel guard).
🔍 Local verification report — real merge build & runtime test
Verdict: ✅ Mechanically sound. The merge-into- Environment
✅ Test results (post-build, all green)
Headline files:
|
tanzhenxin
left a comment
There was a problem hiding this comment.
Review
This adds a broad ACP surface for desktop clients, and the download/skill-install path is well-defended — host allowlisting, HTTPS-only with redirect re-validation, and write-time path-traversal guards all look solid.
The issues below cluster in one area: the settings-reflection endpoints a desktop client reads to render and edit configuration report an effective state that differs from what the runtime actually applies. A client driving off these will display — and on save, persist — the wrong state. Two standalone issues (transcript recording and the Windows dev launcher) round it out.
1. Memory settings are reported with the wrong defaults and scope (severity: medium · confidence: high)
When a user's settings don't explicitly set enableManagedAutoDream or enableAutoSkill, the memory endpoint reports them as off, but the runtime treats both as on by default. A desktop client that reads and then saves these values back will silently turn off features the user never opted out of. The endpoint also only consults user-scope settings, ignoring workspace and system scope, so the reported values can disagree with what actually takes effect.
2. Mid-turn messages are sent to the model but not recorded (severity: medium · confidence: high)
Messages a user sends while a tool is running are injected into the model's input, but they're never written to the session transcript the way the interactive UI records them. Because session replay and resume read from that transcript, a session that received mid-turn instructions will replay a history that's missing them — diverging from what the model actually saw.
3. The effective-settings view includes integrations the runtime ignores (severity: medium · confidence: high)
The merged settings view reports workspace-defined MCP servers and hooks even in an untrusted folder, where the runtime deliberately drops workspace settings — so a client shows integrations that aren't actually active. The same view also includes MCP servers and hooks from disabled extensions, which the runtime skips. Both cases surface inactive integrations as effective.
4. Some supported hook events are invisible and uneditable (severity: medium · confidence: high)
The hook-event list backing the settings endpoints omits PostToolBatch and UserPromptExpansion, both supported events. A user with hooks configured under either one won't see them in the reported settings and can't edit them through the settings endpoint — they're silently dropped from the desktop surface.
5. A malformed rules update can clear existing permission rules (severity: medium · confidence: high)
The setRules handler validates the scope and rule type but not the rules themselves. A request with a missing or non-array rules value is normalized to an empty list and persisted, silently wiping the allow/ask/deny list for that scope. Since clearing is already expressible by passing an explicit empty array, a malformed payload shouldn't be able to erase rules by accident.
6. The dev launcher fails on Windows paths with spaces (severity: medium · confidence: high)
When launching the source CLI directly through Node on Windows, the command still goes through the shell, so a Node path containing a space (the default Program Files location) gets split and the launch fails. npm run dev is broken on Windows as a result; macOS and Linux are unaffected.
Verdict
COMMENT — flagging rather than blocking: the settings-reflection endpoints misreport effective configuration in four ways (memory defaults and scope, untrusted-workspace and inactive-extension integrations, and a couple of unsupported hook events), so a desktop client shows and persists wrong state; mid-turn messages also diverge the resumed transcript, a malformed rules update can clear permission rules, and the Windows dev launcher regresses. Worth resolving before desktop clients depend on these endpoints.
DragonnZhang
left a comment
There was a problem hiding this comment.
/review — Automated Review Summary
Verdict: No new findings (deterministic clean, manual review passed)
Deterministic checks: tsc clean, eslint clean (0 findings).
Manual review: Thoroughly reviewed all 21 changed files (+5,478/-204). This is a substantial feature PR adding desktop Qwen integration via ACP, covering provider management, skill install/delete/enable, settings management (core, memory, MCP, hooks, extensions, permissions), session history replay, and mid-turn user message draining.
Security posture is strong. The code implements defense-in-depth across multiple attack vectors:
- SSRF protection: GitHub host allowlist + HTTPS-only + manual redirect following with per-hop validation
- Path traversal:
validateSkillSlugrejects./../separators;resolveManagedSkillDirandresolveSkillInstallPathassert containment under the skills root - Secret handling: MCP env/headers redacted with
__redacted__sentinel on read, restored on write; provider API keys never serialized (onlyhasApiKey: true) - Prompt injection: string settings strip control characters before persistence
- Archive safety: compressed/decompressed size caps, async gunzip, Content-Length pre-check
- Atomic skill installs via staging dir + rename
All previously flagged issues (slug validation, SSRF, API key exposure, YAML frontmatter corruption, hook index bounds, approval mode enum, etc.) have been properly addressed in the latest commits.
— qwen-code via Qwen Code /review
DragonnZhang
left a comment
There was a problem hiding this comment.
CI failures (Test (macos-latest, Node 22.x), Test (ubuntu-latest, Node 22.x)) appear pre-existing — caused by BaseTextInput.tsx ink subpath imports on the base branch, not by this PR's changes.
Summary: 3 Critical + 15 Suggestion findings. 438 tests pass. Key areas:
- Skill download security: gzip decompression bomb + response memory exhaustion (see inline)
modelInvocablesemantics flip between skillManager and slashCommands paths (see inline)findLatestTrailermissingescapeRegExp(see inline)- Additional suggestions:
buildCoreSettingsExtensionManager recreation,setCoreValueoutputLanguage scope,extractFilesFromTarGzdefense-in-depth,cwdvalidation in extMethod handlers, and test coverage gaps for new ACP handlers
— qwen3.7-max via Qwen Code /review
The acpAgent.worktree.test.ts mock of @qwen-code/qwen-code-core did not
export HookEventName, but acpAgent.ts calls Object.values(HookEventName),
causing the suite to fail to load ("No HookEventName export is defined on
the mock"). Add the full HookEventName enum to the mock.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve review threads on this PR: - Stream skill archive decompression (createGunzip + size-bounded Writable) and abort once the inflated size crosses the cap, so a gzip bomb can no longer fully inflate into memory before the post-hoc length check. - Enforce the download size cap against actually-streamed bytes via a ReadableStream reader (readBodyWithLimit) instead of trusting the advisory Content-Length and buffering the whole body with arrayBuffer(). - Gate output-language.md rewrite on user scope so a workspace-scoped general.outputLanguage change no longer clobbers the global file. - normalizeCoreSettingValue: collapse an all-control/whitespace string to undefined so empty != literal-empty for settings like model.name. - escapeRegExp the trailer name in findLatestTrailer, matching its siblings. - Include sessionId in the mid-turn drain warning for correlation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@qwen-code /triage |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
The two Windows-targeted dev.js launcher tests added in #4728 mock existsSync with forward-slash suffix matching and assert spawn args via forward-slash stringContaining. On a real windows-latest runner dev.js builds these paths with path.join, which yields backslashes, so the existsSync mock never matches, dev.js takes the bare tsx.cmd shell fallback, and both tests fail. On macOS/Linux the platform() mock plus real forward-slash joins keep them green, which is why main CI has been red only on the Windows job since 423cac1. Normalize separators in both the existsSync mocks and the received spawn arguments before asserting, so the tests pass on every host OS. Same content as the fix bundled into #4840's merge commit 0e104e1, extracted into a standalone test-only change so main recovers without waiting on a core-behavior PR; both merge cleanly in either order. Co-authored-by: qqqys <qys177@gmail.com>
What this PR does
This PR expands ACP support so Qwen Code can provide the command, skill, session, and message metadata needed by a desktop client while keeping the desktop application code out of this repository change.
It also tightens the development runtime path for launching the source CLI, updates ACP type coverage for companion clients, adds a focused PTY utility regression test, and syncs the non-desktop support tooling needed by the desktop split: the root OpenWork desktop sync skill/script entrypoint and the VSCode companion packaging dependency.
Why it's needed
The desktop repository will consume Qwen Code through the npm package/runtime surface rather than carrying a copy of the CLI and core source. These changes make the non-desktop package surface expose the ACP behavior, metadata, and repo-level support tooling that desktop needs after that split.
Reviewer Test Plan
How to verify
Review that the PR contains only non-
packages/desktop/changes: ACP session handling, command and skill metadata, config/runtime support, companion ACP/package metadata, the development CLI launcher, and the root OpenWork sync tooling. Confirm that nopackages/desktop/application files are included.Run the focused CLI ACP/config tests and the focused core PTY utility test from the earlier validation. For the newly synced support tooling, run the lockfile check and the
desktop-openwork-synchelp smoke test. The lockfile check should pass, and the sync command should print usage without requiring an OpenWork checkout.Evidence (Before & After)
N/A. This PR changes ACP metadata/runtime contracts, repo-level sync tooling, dependency metadata, and tests, not a direct TUI or visual workflow.
Tested on
Environment (optional)
Validated in a temporary worktree based on
origin/main, and later synced in the PR worktree. The PR worktree reuses local workspace dependencies via symlinks.Commands run:
npm run check:lockfileandbun run desktop-openwork-sync --helppassed after the latest sync.npm run build && npm run typecheckwas attempted in the PR worktree but did not complete because the symlinked localnode_moduleshas@google/genai1.30.0 underpackages/core/node_modules/packages/cli/node_moduleswhile the lockfile expects 2.6.0; the failure is in existingopenaiContentGeneratorreferences to newerFinishReasonmembers, not in the files changed by the latest sync commit.Risk & Scope
Linked Issues
N/A
中文说明
What this PR does
这个 PR 扩展了 ACP 支持,让 Qwen Code 可以向桌面客户端提供所需的 command、skill、session 和 message metadata,同时不把 desktop 应用代码放进这个仓库的改动里。
它还收紧了源码 CLI 的开发启动路径,补充了 companion client 的 ACP 类型覆盖,新增了一个 PTY 工具的定向回归测试,并同步了 desktop split 需要的非
packages/desktop支撑工具:root OpenWork desktop sync skill/script 入口,以及 VSCode companion packaging dependency。Why it's needed
拆分之后,desktop 仓库会通过 npm package/runtime surface 使用 Qwen Code,而不是复制 CLI 和 core 源码。这些改动让非 desktop 的包接口暴露 desktop 所需的 ACP 行为、metadata 和仓库级支撑工具。
Reviewer Test Plan
How to verify
确认这个 PR 只包含非
packages/desktop/改动:ACP session handling、command 和 skill metadata、config/runtime support、companion ACP/package metadata、开发 CLI launcher,以及 root OpenWork sync tooling。确认没有packages/desktop/应用文件。运行前面已经验证过的定向 CLI ACP/config 测试和 core PTY 工具测试。对这次新同步的支撑工具,运行 lockfile check 和
desktop-openwork-synchelp smoke test。lockfile check 应通过,sync command 应能直接打印 usage,不需要 OpenWork checkout。Evidence (Before & After)
N/A。这个 PR 修改的是 ACP metadata/runtime contract、仓库级 sync tooling、dependency metadata 和测试,不是直接的 TUI 或视觉流程。
Tested on
Environment (optional)
先前验证基于
origin/main的临时 worktree;本次同步在 PR worktree 中完成。PR worktree 通过 symlink 复用了本地 workspace dependencies。Commands run:
最新同步后,
npm run check:lockfile和bun run desktop-openwork-sync --help已通过。npm run build && npm run typecheck在 PR worktree 中尝试过,但没有完成:该 worktree 复用的 symlink 本地node_modules里,packages/core/node_modules/packages/cli/node_modules的@google/genai仍是 1.30.0,而 lockfile 期望 2.6.0;失败点是既有openaiContentGenerator代码引用新的FinishReason成员,不在本次最新同步 commit 修改的文件里。Risk & Scope
Linked Issues
N/A