Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ bd8ac3ebd916eafbce1040cea39ff9d966bb3493
bb94c967934cf3c355a143435bc2b292a917275f
05ab77d55f9b0c073708b17a0e3c34580b92ac78
295041e71071b2fa66bbc3c8517a1697c9d6c00b

# P2-06 纯搬迁与 include 改写(2026-09-28)
51c37dea8be09e73b823f0799cef301352fbf2ff
f095f76256a47f86a403a66fde160e8d96782039
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@ CLI `--worktree [name]` / `-w`(TUI):启动即建 worktree,name 可为 PR 引用(

## Skills, Memory, And Project Instructions

P2-06 module boundaries: generic YAML frontmatter lives in `src/utils/frontmatter.*`; TUI skill command registration lives in `src/tui/commands/skill_commands.*`; reusable command expansion lives under `src/skills/`. Provider-dependent hook payload builders live in `src/agent/hook_bridge/hook_events.*`, while `src/hooks/hook_payload.*` owns the generic startup payload and `src/hooks/hook_seeds.*` owns read-only hook seed definitions.

[src/skills/](src/skills) discovers `SKILL.md` files from configured global, project, and external skill directories. Skill metadata is read from YAML frontmatter at startup; full bodies are loaded lazily through skill invocation or the `skill_view` tool.

Proactive skill discovery (`inject-skill-index-into-context`): a compact skill index (name + description + optional `whenToUse` frontmatter, grouped by category) is injected each request into the session-context system-reminder built by `build_session_context_prompt` — the same per-request, never-persisted block that carries project instructions and the memory index. Budget is 1% of the context window in chars (4 chars/token, 8000-char fallback), 250 chars per entry (UTF-8-safe truncation); over budget degrades to names-only, then tail-cut with a `(+N more — call skills_list)` marker. The static `# Skills` prompt section points at this index ("err on the side of loading"); `skills_list` remains the fallback enumerator only. Without this index the model has zero visibility into installed skills and never invokes them proactively.
Expand Down Expand Up @@ -564,7 +566,7 @@ SidePanel 折叠 UI:`ChatView` 把 `SidePanel` 包到 `<div class="ace-side-pane
| `GET /api/files?cwd=&path=&show_hidden=` → `[{name,path,kind,size?,modified_ms?}]` | SidePanel 文件 tab 的 lazy 文件树。`cwd` 必须 ∈ `{deps.cwd}` 白名单;`path` 走 `weakly_canonical(cwd/path)` + prefix 检查防越权。硬编码 noise 黑名单(.git/node_modules/dist/build/__pycache__/.venv/venv/target/.next/.cache)始终过滤。隐藏文件(dot 开头)默认过滤,`show_hidden=1` 透出 |
| `GET /api/files/content?cwd=&path=` → `text/plain; charset=utf-8` body | SidePanel 预览 tab 读文件原文。> 5MB → 415 `{error:"file too large",size:N}`;前 512 字节出现 `\0` → 415 `{error:"binary"}`;不存在 → 404。实现:`src/workspace/files_handler.{hpp,cpp}`(D21 从 web 移出,纯函数,17 个 unit test 覆盖路径越权 / 噪音过滤 / 排序 / 二进制嗅探) |
| `GET /api/commands?workspace=<hash>` → `{builtins:[{name,description}][, skills:[{name,description}]]}` | InputBar 斜杠下拉的命令清单。builtins **硬编码白名单 = init + compact**(描述与 TUI `register_builtin_commands` / `register_init_command` 对齐)。**`workspace` 参数(由 `expand-webui-skill-commands` 引入)**:缺省 → 不返回 `skills` 字段(向后兼容旧客户端);提供 → handler 用 `acecode::initialize_skill_registry(tmp, *cfg, workspace_cwd)` 临时构造一个 SkillRegistry 扫该 workspace 的项目链(`.agent/skills`、`.acecode/skills` + 全局 + external_dirs),与 daemon 全局 SkillRegistry 合并(workspace local 优先,first-wins by name),按字典序输出 skills 字段。`/init` `/compact` 在 web 端选中后只插入输入框 + chip 高亮,daemon 端**不**做特殊执行(原 add-webui-slash-commands 决策);`/<skill-name> args` 由下面新加的 expander 真展开。实现:`src/web/handlers/commands_handler.{hpp,cpp}`(纯函数 `build_commands_payload` + gtest case)|
| `POST /api/sessions/:id/messages`(行为扩展) | `expand-webui-skill-commands` 引入:在 `send_input` 之前调 `try_expand_skill_command(text, registry)`(`src/web/handlers/skill_command_expander.{hpp,cpp}`)。命中已知 skill 名(按 session 的 workspace cwd 临时 scan)→ text 被替换为 `build_skill_invocation_hint(meta, args)` 的**轻量提示**:`[SYSTEM: User invoked /<name> skill] + Description + Use skill_view(name=...) to load full SKILL.md + User's request: <args>`。**不**注入 SKILL.md body / supporting_files,LLM 第一次看到提示后主动 invoke `skill_view` tool 把 SKILL.md 拉一次进 context,后续重复同名 `/skill` 调用不再注入(避免 context 膨胀)。TUI `src/skills/skill_commands.cpp::cmd.execute` 也走同一个 `build_skill_invocation_hint`,跨端行为统一。Builtin (`/init`/`/compact`) 不在 SkillRegistry 中 → 透传走普通 user message;未知命令 (`/foobar`) 同样透传。**不**新增执行端点,**不**改 AgentLoop API |
| `POST /api/sessions/:id/messages`(行为扩展) | `expand-webui-skill-commands` 引入:在 `send_input` 之前调 `try_expand_skill_command(text, registry)`(`src/skills/skill_command_expander.{hpp,cpp}`)。命中已知 skill 名(按 session 的 workspace cwd 临时 scan)→ text 被替换为 `build_skill_invocation_hint(meta, args)` 的**轻量提示**:`[SYSTEM: User invoked /<name> skill] + Description + Use skill_view(name=...) to load full SKILL.md + User's request: <args>`。**不**注入 SKILL.md body / supporting_files,LLM 第一次看到提示后主动 invoke `skill_view` tool 把 SKILL.md 拉一次进 context,后续重复同名 `/skill` 调用不再注入(避免 context 膨胀)。TUI `src/tui/commands/skill_commands.cpp::cmd.execute` 也走同一个 `build_skill_invocation_hint`,跨端行为统一。Builtin (`/init`/`/compact`) 不在 SkillRegistry 中 → 透传走普通 user message;未知命令 (`/foobar`) 同样透传。**不**新增执行端点,**不**改 AgentLoop API |
| `POST /api/open-in-explorer` body `{path}` → `{ok:true}` / 400 `{ok:false,error}` / 501 | webapp 兼容模式(Edge --app,无 webview bridge)右键菜单「在资源管理器中打开」的 REST 通路。门控 = `WebServerDeps.open_in_explorer` 回调是否注入(worker.cpp 仅在 `native_folder_picker_enabled` 时填,复用 `desktop::open_path_in_file_manager`:接受任意现存本地绝对普通文件或目录,不做 workspace/managed-root 边界判断)。Windows 文件用 Explorer `/select` 选中,目录保持直接打开。前端 `DesktopContextMenu::openTargetInExplorer` bridge 优先、REST 兜底 |
| `GET /api/fs/roots` → `{host, os, home, roots[{path,label,drive_type,total_bytes?,free_bytes?}], quick[{kind,path}], workspaces[{hash,name,path}]}` / `GET /api/fs/list?path=<abs>&show_hidden=1` → `{path, parent, truncated, entries[{name,path,kind,hidden,size?,modified_ms?,link_target?}]}` | Web「选择文件 / 文件夹」弹窗(`add-web-path-picker`)的服务端目录浏览器:**已鉴权即可只读浏览 daemon 全盘**,不走 `/api/files` 的 workspace 白名单,噪音目录不过滤,junction 照常列出并附 `link_target`;路径只做词法归一**不 canonical**(经 `C:\Users\shao` junction 进入返回的仍是 `C:/...`,与原生对话框一致,否则同一目录会注册出两个 workspace hash);5000 条上限置 `truncated`;`ec.message()` 在中文 Windows 是 GBK,入 JSON 前必须 `ensure_utf8`,否则 dump 抛异常整个请求变裸 500。触发策略:有 Desktop bridge 走原生对话框,没有(普通浏览器 / Edge --app 兼容模式 / 远程 Web)一律 web 弹窗,不再调用 daemon 的原生 REST 对话框(那会在 daemon 所在机器桌面弹框并把请求挂死)。实现:`src/web/handlers/fs_browser_handler.{hpp,cpp}` 纯函数 + `routes_fs.cpp`;前端 `lib/pathPicker.js`(纯逻辑)+ `lib/pathPickerHost.js`(Promise 形态的单实例请求)+ `components/PathPickerModal.jsx` / `PathPickerHost.jsx`(App 顶层挂载),三处接入点 `lib/workspacePicker.js` / `lib/environmentSettings.js::pickEnvironmentPath` / `lib/desktopPreviewFilePicker.js::pickPreviewFile` |

Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
set(ACECODE_WEB_DIST "${CMAKE_SOURCE_DIR}/web/dist")
set(ACECODE_WEB_EMBED_DIR "${ACECODE_WEB_DIST}")
if(NOT EXISTS "${ACECODE_WEB_DIST}")
message(WARNING

Check warning on line 108 in CMakeLists.txt

View workflow job for this annotation

GitHub Actions / unit-tests (linux-x64)

[acecode] web/dist/ not found — embedding a minimal fallback page. Run
"[acecode] web/dist/ not found — embedding a minimal fallback page. "
"Run `pnpm install && pnpm build` inside web/ before re-configuring for the full UI.")
set(ACECODE_WEB_EMBED_DIR "${CMAKE_BINARY_DIR}/generated/web-dist-fallback")
Expand Down Expand Up @@ -182,7 +182,7 @@
${CMAKE_SOURCE_DIR}/src/markdown)
set(ACECODE_TUI_TESTABLE_SUBSETS
commands/ resume/ path_reference/ markdown/
drag_scroll.cpp text_input_ops.cpp skill_commands.cpp slash_command_usage.cpp)
drag_scroll.cpp text_input_ops.cpp slash_command_usage.cpp)
set(ACECODE_TUI_SOURCES)
foreach(_tui_dir IN LISTS ACECODE_TUI_DIRS)
if(NOT IS_DIRECTORY "${_tui_dir}")
Expand Down
8 changes: 4 additions & 4 deletions docs/skills-implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@

## 3. SKILL.md frontmatter

acecode 只识别 YAML frontmatter 的一个子集,解析器位于 `src/skills/frontmatter.{hpp,cpp}`。仅读取文件首 8KB 足以抓到 frontmatter。
acecode 只识别 YAML frontmatter 的一个子集,解析器位于 `src/utils/frontmatter.{hpp,cpp}`。仅读取文件首 8KB 足以抓到 frontmatter。

| 字段 | 类型 | 用途 |
|------|------|------|
Expand Down Expand Up @@ -108,7 +108,7 @@ struct SkillMetadata {

## 5. 激活路径 A — 用户输入 `/<skill-name>`

相关代码:`src/skills/skill_commands.{hpp,cpp}`、`src/skills/skill_activation.{hpp,cpp}`、`main.cpp` 对 `CommandRegistry` 的集成。
相关代码:`src/tui/commands/skill_commands.{hpp,cpp}`、`src/skills/skill_activation.{hpp,cpp}`、`main.cpp` 对 `CommandRegistry` 的集成。

### 注册流程

Expand Down Expand Up @@ -232,11 +232,11 @@ struct SkillMetadata {
| 文件 | 职责 |
|------|------|
| `src/skills/skill_metadata.hpp` | `SkillMetadata` 数据结构 |
| `src/skills/frontmatter.{hpp,cpp}` | 受限 YAML frontmatter 解析 |
| `src/utils/frontmatter.{hpp,cpp}` | 受限 YAML frontmatter 解析 |
| `src/skills/skill_loader.{hpp,cpp}` | 单个 `SKILL.md` 目录 → `SkillMetadata`;name 归一化;平台匹配 |
| `src/skills/skill_registry.{hpp,cpp}` | 多根递归扫描、去重、查询、支持文件定位 |
| `src/skills/skill_activation.{hpp,cpp}` | 用户 `/<name>` 触发时向 agent 投递的消息模板 |
| `src/skills/skill_commands.{hpp,cpp}` | 把每个 skill 注册/反注册为 slash 命令;`/skills reload` |
| `src/tui/commands/skill_commands.{hpp,cpp}` | 把每个 skill 注册/反注册为 slash 命令;`/skills reload` |
| `src/tool/skills_tool.{hpp,cpp}` | `skills_list` 工具(tier-1) |
| `src/tool/skill_view_tool.{hpp,cpp}` | `skill_view` 工具(tier-2/3,含路径安全校验) |
| `src/prompt/system_prompt.cpp` | 系统提示词 Skills 段 |
Expand Down
2 changes: 1 addition & 1 deletion docs/竞品调研报告_完整版_2026-08.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
| 2 | 计划模式 `/plan` + 权限模式 `/mode` | `builtin_commands.cpp:1964,1968`、`src/tool/plan_mode_tool.cpp` |
| 3 | 子代理:spawn_subagent 工具 + `/tasks` 后台任务 + Web Swarm | `src/tool/spawn_subagent_tool.cpp`、`builtin_commands.cpp:2022`、`web/src/components/SwarmModeIcon.jsx` |
| 4 | TodoWrite 工具 | `src/tool/todo_write_tool.cpp` |
| 5 | opencode 式 markdown 自定义命令(`$ARGUMENTS` 插值,注册为斜杠命令) | `src/commands/opencode_command.cpp`、`opencode_command_registry.cpp` |
| 5 | opencode 式 markdown 自定义命令(`$ARGUMENTS` 插值,注册为斜杠命令) | `src/skills/opencode_command.cpp`、`opencode_command_registry.cpp` |
| 6 | OAuth:GitHub Copilot Device Flow + Grok Coding Plan(xAI) | `src/provider/auth/github_auth.{hpp,cpp}`、`xai_auth.{hpp,cpp}`、`web/routes/routes_models.cpp` |
| 7 | 自更新 `acecode upgrade` / `update` | `src/upgrade/`(apply / manifest / package / version)、`src/tui/cli_dispatch.cpp` |
| 8 | 复制助手消息(鼠标选区 OSC52 / 系统剪贴板) | `src/main.cpp` clipboard_copy_status_message 路径、`src/tui/clipboard_helpers.cpp` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -464,3 +464,9 @@ Base: `master` (`e15df9eadfaf05a170fd8b83fb26939452e2d3e9`). Read-only snapshot.
P2-05 不保留旧路径转发头。合并本阶段主线前,先在各自的迁移分支上运行 `normalize_includes.py --scope src` 与 `--scope tests`,再按既有方案演练 rebase / patch。不要改写或删除原 ref。

本阶段除映射表中的文件搬迁外,还拆出了 `config/vocab/permission_mode.hpp`、`provider/models_dev_catalog_cache`、`provider/model_probe_cache`、`tool/{safe_text_write,text_file_errors}`、`tool/web_search/region_cache`、`desktop/workspace_state` 与 `tui/slash_command_usage`。涉及 `state_file`、编码报错或 models.dev 全局缓存的调用方需显式包含对应模块的新头,不能仅依靠旧的间接 include。原状态字段和模型工具名映射保持。

## P2-06 迁移提示(2026-09-28)

本任务把 frontmatter 移到 utils,把可复用的 opencode / skill 命令展开器归到 skills,把技能斜杠命令注册归到 tui/commands,对应五个测试文件同步归位。旧路径不保留转发头。上表九个遗留 ref 继续按映射运行 `migrate_branch.py --apply-map` 或 include 规范化脚本,原 ref 的功能内容与去留仍按 P3-03 逐项处理。

hook 模型事件构造器与只读种子表属于 extract 行,不能用整文件路径替换代替职责拆分:模型事件调用方引用 `agent/hook_bridge/hook_events.hpp`,种子消费者引用 `hooks/hook_seeds.hpp`。原 `hooks/hook_payload.hpp` 只提供无模型依赖的启动事件,种子安装事务仍由 skills 的统一安装器协调。
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# P2-06 hooks 与 skills 拆分验证

任务认领由 PR #82 合入 master,基线 `d993ae5f00c2d4c475a04c05d5509ffbbe3b4d8e`,已包含 P2-03 / P2-04 / P2-05 的验收。工作分支为 `refactor20260927/P2-06`,复用已合入且无活动进程、无未提交改动的原 P2-03 工作区;构建使用新目录。

## 实现边界

- `51c37dea`:13 个文件的纯 `git mv`,逐项 R100,0 行增删。frontmatter 归 utils,opencode_command 与 skill_command_expander 归 skills,skill_commands 归 tui/commands;五个测试同步镜像归位。
- `f095f762`:19 个源/测试文件的 20 处 include 路径改写;CMake 删除失效的根级 skill_commands.cpp 子集项,已登记的 commands/ 子集继续确保该实现参与 acecode_testable。
- 模型注册源、模型加载完成和助手消息完成三个事件构造器及其私有辅助移到 agent/hook_bridge/hook_events。hooks/hook_payload 仅保留无 provider 依赖的启动前事件;调用方直接引用实际定义头,旧路径无转发壳。
- DefaultHookSeed 与只读种子表移到 hooks/hook_seeds。注册表只依赖 hooks;skills 安装器继续协调完整种子事务并读取该表,既有版本/哈希/迁移规则保持。
- 两类启动事件复用已有 platform 进程 ID 原语,保留原来的 int 转换。移除终端探测对 hooks 配置的无用 include。
- 同步 CLAUDE.md、技能说明和对应测试注释中的路径;help-source 生成器未引用此次移动路径,无须改写生成内容。

## 当前检查

src / tests include 规范化第二次检查为 0 改动;映射 strict、分层已有阻断项、行数和所有权 strict 通过。分层违规从 P2-05 的 87 项降为 83 项,本任务相关 hooks→provider、hooks→skills 和 environment→hooks 反向依赖消除。其余项目仍由后续 P2-07 / P2-08 处理。

前端首次测试因本工作区未安装 node_modules,缺少 @babel/core 而未能启动完整套件;已执行 pnpm install --frozen-lockfile,不改锁文件。依赖齐备后的完整 `pnpm test` 与 `pnpm build` 均通过。

对照机械提交前后的函数文本,三个模型事件构造器除了等价的进程 ID 原语调用外保持原样;hook 种子表函数完全相同。

## 本机原生验证

固定源码 `df9ef9567bb24eabb7b6294ea16f49fd991f845d`,使用 MSVC 2022 x64 / Ninja / Release,全新目录 `build/p2-06-hooks-skills`,BUILD_TESTING 与 Desktop 均开启。acecode、acecode-desktop、acecode_unit_tests 及五个 EXCLUDE_FROM_ALL 冒烟目标全部构建成功。

CMake File API 为 59 个目标、3637 个元组;对照已验收 P2-05 的 59 / 3623,目标和原有源文件归属、编译参数全部相同,无删除元组。14 个新增元组仅对应 hook_events 与 hook_seeds 的源、头及生成对象。

- 定向用例 351 / 351 通过,0 SKIP,包括三个 SkillCommandsReload、11 个 TerminalResolver 及 hooks / skills / seeder 用例。
- 独立 HOME/TEMP 下完整用例清单 5117,实际运行 5116,9 SKIP,0 失败,退出码 0。
- 对照 P2-05 完整本机记录,用例名和 SKIP 集合完全一致,未通过过滤隐藏失败。

`migrate_branch.py --check` 已执行,总体仍为非零:documentation 79、layers 2331(含尚未进行的最终分组检查)、migration_paths 60;include_normalization、map、ownership、seed 均为 0。此报告不代表 P3 已完成;过渡布局的实际分层违规仍是上文的 83 项。九个遗留分支的迁移通知已记录在 branch-inventory.md。

## 四平台验证

[refactor-matrix 36341836305](https://github.com/tmoonlight/acecode/actions/runs/36341836305) 固定上述源码提交,Windows / Linux / macOS / Deepin 全部完成构建与采集。PR #84 合入的窗口预告随后已合并到本分支,仅改 AGENTS.md / CLAUDE.md,src、tests、CMake、scripts、assets、web 相对验证源码无差异。

| 平台 | targets / tuples | 用例清单 | 实际执行 / SKIP | 失败 |
|---|---:|---:|---:|---:|
| Windows x64 | 59 / 3637 | 5117 | 5110 / 10 | 4(既有基线) |
| Linux x64 | 50 / 3551 | 5037 | 5028 / 16 | 0 |
| macOS arm64 | 57 / 3696 | 5042 | 5035 / 14 | 10(既有基线) |
| Deepin x64 | 13 / 1400 | 不运行单测 | 不适用 | 构建成功 |

CI 使用 CTest 逐用例运行,实际执行统计包含脚本测试,与本机直接运行 gtest 的计数口径不同。四个平台全部保留既有 target 和源文件归属、编译参数;对照 P2-05,三个完整平台仅增加本轮两个拆出实现的 14 个元组,Deepin 增加 6 个。三个测试平台的用例与 SKIP 集合相对 P2-05 完全相同;相对 G0 仅有 P2-05 已登记的两个 TextFileToolErrors 新增用例。

Windows 与 macOS 的失败名称均在 G0 中,断言或异常特征也逐项对照此前 P2-05 日志一致。保留实际失败,未放宽断言、增加过滤或重跑取绿。Windows 既有失败为:


- `HookRunner.ShellCommandReceivesExactEnvironmentOverride`
- `McpManagerAsync.ProjectOverridesHaveIsolatedToolDiscoveryAndDispatch`
- `dev_desktop_script_unit`
- `verify_package_python_unit`

macOS 既有失败为:

- `BuiltinToolRegistry.NativeBrowserToolsCanBeUnregisteredAsOneGroup`
- `BuiltinToolRegistry.RegistersSharedCoreAndPlatformBrowserTools`
- `ManagedRemoteWebProxy.AutomaticPortFallsBackWhenAdjacentPortIsBusy`
- `ManagedRemoteWebProxy.StartsRealChildAndStopsWithoutOrphaningIt`
- `RemoteWebTcpProxy.TransparentlyForwardsPersistentBidirectionalBytes`
- `SandboxPolicy.ResolveAccessPrefersDeepestEntryThenDenyWriteRead`
- `SettingsCenterRender.NarrowTopRailFollowsDeepLinkedTab`
- `StateFileTest.ClaimFlagIsGrantedExactlyOnceAcrossProcesses`
- `WebServerHttp.ModelRoutesRejectUnauthenticatedRemoteAccess`
- `WebServerHttp.RealProxyRequiresTokenAndForwardsAuthenticatedHealth`

P2-04 曾记录的 `AgentLoopTurnSteering.InterruptStartsStructuredTurnBeforeOrdinaryQueue` 和已知 macOS SSE 时序失败,本轮均未出现。

三个完整平台的 provenance 源码 SHA 与固定提交一致。Deepin 的 source_revision 仍因容器 Git 归属检查为空;额外核对构建 job `108683316416` 的 checkout 命令、HEAD 提示及紧接 `git log -1 --format=%H` 的完整 SHA,均为 `df9ef9567bb24eabb7b6294ea16f49fd991f845d`,未将空 provenance 当作源码证明。

## 不变量与合入条件

本轮完整测试实际执行并通过 `AgentLoopTermination.RequestPrefixIsByteStableAcrossIterationsInATurn`、`SystemPromptTest.NonGptModelStateIsByteIdenticalToLegacyPrompt`、`HookAgentLoop.DispatchesAssistantCompletedAfterTextMessageCommit`、`HookAgentLoop.SessionStartAdditionalContextReachesNextRequestOnly`、`HookAgentLoop.PermissionResolvedPairsWithHookApprovalExactlyOnce` 与三个 `SkillCommandsReloadTest`。CMake 逐元组对照同时覆盖 Desktop 链接边界与按源编译属性。

P2-06 的实现、构建归属、测试清单、静态检查和已知失败复核已完成。任务复选框保持未勾选,PR #83 实际合入后立即登记验收并认领下一项。
Loading
Loading