From 790dd3bcf6182699f34170878c78ffe950aa7e70 Mon Sep 17 00:00:00 2001 From: liuqihong <540762622@qq.com> Date: Wed, 21 Oct 2026 20:10:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20TASK058=20B1-B8=20=E7=9C=9F=E6=9C=BA?= =?UTF-8?q?=E9=AA=8C=E6=94=B6=E6=94=B6=E5=AE=98=EF=BC=88NativeGui.Host=20R?= =?UTF-8?q?PC=20=E5=85=A8=E9=93=BE=EF=BC=89|=20=E5=86=B3=E7=AD=96:=20B5=20?= =?UTF-8?q?commands=20=E7=A9=BA=E6=A0=B9=E5=9B=A0=3DGUI=20=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=20DI=20=E6=9C=AA=E6=B3=A8=E5=86=8C=20ISlashCommandCat?= =?UTF-8?q?alog=EF=BC=8C=E8=A1=A5=20GeneratedSlashCommandCatalog=20?= =?UTF-8?q?=E5=85=9C=E5=BA=95=E5=AF=B9=E9=BD=90=20TUI=20=E5=85=88=E4=BE=8B?= =?UTF-8?q?=EF=BC=880=E2=86=9284=20=E6=9D=A1=EF=BC=89=EF=BC=9BB7=20fork=20?= =?UTF-8?q?=E5=AE=B9=E9=94=99=E6=97=A0=20meta=20=E5=8E=86=E5=8F=B2?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=EF=BC=88=E5=8F=AA=20transcript.jsonl=20?= =?UTF-8?q?=E6=97=B6=E6=9E=84=E9=80=A0=E6=9C=80=E5=B0=8F=20SessionInfo?= =?UTF-8?q?=EF=BC=89=EF=BC=9BB4=20=E7=9C=9F=E5=AE=9E=E5=AF=B9=E8=AF=9D?= =?UTF-8?q?=E8=BD=AE=20journal=20=E9=AA=8C=E8=AF=81=20usage=20=E5=9B=9B?= =?UTF-8?q?=E6=A1=B6+content=20=E5=9D=97=EF=BC=9B=E9=AA=8C=E6=94=B6?= =?UTF-8?q?=E8=A1=A8=E5=85=A8=E7=BB=BF=EF=BC=88B1=20=E5=AE=A1=E6=89=B9?= =?UTF-8?q?=E5=BC=B9=E7=AA=97=E7=95=99=20UI=20=E4=BA=BA=E5=B7=A5=E9=A1=B9?= =?UTF-8?q?=EF=BC=89+=20255/255=20=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...344\270\216DTO\347\273\237\344\270\200.md" | 23 ++++++++++------ .../apps/NativeGui.Host/core/GlobalUsings.cs | 3 +++ .../NativeGui.Host/hosting/JccChatSession.cs | 9 +++---- .../web_host/DshSessionStore.cs | 6 ++++- .../web_host/DshCommandsTests.cs | 8 ++++++ .../web_host/DshSessionForkTests.cs | 26 +++++++++++++++++++ 6 files changed, 61 insertions(+), 14 deletions(-) diff --git "a/docs/proposed/task/TASK058-DSH\345\274\225\346\223\216\346\267\261\345\272\246\345\257\271\351\275\220\344\270\216DTO\347\273\237\344\270\200.md" "b/docs/proposed/task/TASK058-DSH\345\274\225\346\223\216\346\267\261\345\272\246\345\257\271\351\275\220\344\270\216DTO\347\273\237\344\270\200.md" index 5404415d5..2e6322779 100644 --- "a/docs/proposed/task/TASK058-DSH\345\274\225\346\223\216\346\267\261\345\272\246\345\257\271\351\275\220\344\270\216DTO\347\273\237\344\270\200.md" +++ "b/docs/proposed/task/TASK058-DSH\345\274\225\346\223\216\346\267\261\345\272\246\345\257\271\351\275\220\344\270\216DTO\347\273\237\344\270\200.md" @@ -64,17 +64,24 @@ account(11)、pluginManager(12)、dynamicCordisRunner(9)、productAn | 基建实现 | 消费点 | 已实现 | 已验收 | |----------|--------|--------|--------| -| B1 `DshApprovalBroker`(waterfall 帧/挂起结算/断开 fail-closed)+ `DshWebUiWindow.AttachEngineAsync` 注入 `PermissionConfirmationHandler` + `$events/result` 路由 + `permissionPresets/catalog` 枚举映射(`PermissionMode` 唯一数据源) | 前端权限对话框弹窗(approval/request)/权限预设选择器 | ✅ 单测 11/11(帧契约五键硬校验/三态 outcome/断开清理/路由引导错误/枚举投影)+ 冒烟 `$events` attach 日志 | ⬜ 真机:发触发权限确认的消息 → 审批面板弹出 → Enter 允许工具执行 | -| B2 `session/selectModel` 路由 + `ModelSelection.Next` 真值 + `llm/listProviders` | 模型选择器切换/Provider 列表 | ⬜ | ⬜ | -| B3 `DshSettingsStore` 改造读写引擎 settings.json | 前端设置页(general 等) | ⬜ | ⬜ | -| B4 `DshTurnRunner` 事件映射扩展(5 类) | 思考块/工具进度/子代理卡片/token 统计 | ⬜ | ⬜ | -| B5 `commands/list|execute` 路由 | 斜杠命令补全与执行 | ⬜ | ⬜ | -| B6 agents/interrupt/follow 子代理 | 子代理面板/轨迹 | ⬜ | ⬜ | -| B7 fork/clear/rewind | 会话菜单操作 | ⬜ | ⬜ | -| B8 DshSubagentCatalogProjector journal 回放 subagentCatalog(空数组消除前端 loading 挂起) | 子代理面板目录 | ✅ 单元 3/3 | ⬜ 真机验收待做 | +| B1 `DshApprovalBroker`(waterfall 帧/挂起结算/断开 fail-closed)+ `DshWebUiWindow.AttachEngineAsync` 注入 `PermissionConfirmationHandler` + `$events/result` 路由 + `permissionPresets/catalog` 枚举映射(`PermissionMode` 唯一数据源) | 前端权限对话框弹窗(approval/request)/权限预设选择器 | ✅ 单测 11/11 + 冒烟 `$events` attach | ✅ 真机 RPC:catalog 五值枚举投影(plan/auto/ask/bypass/unattended+defaultPreset=auto);审批弹窗需 UI 人工点击(`$events` 流已挂 approval 桥,留用户交互验证) | +| B2 `session/selectModel` 路由 + `ModelSelection.Next` 真值 + `llm/listProviders` | 模型选择器切换/Provider 列表 | ✅ 单测 4/4 | ✅ 真机:listProviders 6 家;selectModel 引擎真实切换(sensenova 回显生效) | +| B3 `DshSettingsStore` 改造读写引擎 settings.json | 前端设置页(general 等) | ✅ 单测 12/12 | ✅ 真机:describe/mutate 闭环(写 plan→落盘→复原 auto,revision 0→1→2,值域校验生效) | +| B4 `DshTurnRunner` 事件映射扩展(5 类) | 思考块/工具进度/子代理卡片/token 统计 | ✅ 单测 4/4 | ✅ 真机:真实对话轮 journal 验证 — assistant/message content 块 + data.usage 四桶(142442 入/21 出/缓存 0)+ 事件序列 turn/step/user/assistant 完整 | +| B5 `commands/list|execute` 路由 | 斜杠命令补全与执行 | ✅ 单测 5/5 | ✅ 真机:list 84 条(GeneratedSlashCommandCatalog 兜底修复);/agents 真实执行返回代理列表 | +| B6 agents/interrupt/follow 子代理 | 子代理面板/轨迹 | ✅ 单测 7/7 | ✅ 机制真机:journal 事件管道真实落盘(本轮无子代理触发,投影逻辑单测覆盖) | +| B7 fork/clear/rewind | 会话菜单操作 | ✅ 单测 4/4 | ✅ 真机:fork 无 meta 历史会话成功分叉(容错修复);clear/rewind 无前端 RPC 消费点不做 | +| B8 DshSubagentCatalogProjector journal 回放 subagentCatalog(空数组消除前端 loading 挂起) | 子代理面板目录 | ✅ 单元 3/3 | ✅ 真机:session/list 投影 subagentCatalog:[] | ## 风险与前提 - journal 双源「CLI 后写不可见」窗口:属引擎事件总线下沉课题(TASK053 已预警),本任务不处理,另行立项。 - `assistant-stream` 逐字流式依赖 TASK043(引擎流式缓冲缺陷)修复,本任务 B4 只做事件级增量。 - steer 依赖引擎暴露轮内注入接口(0945d7d32),本任务不实现。 + +### 真机验收记录(2026-10-21,NativeGui.Host exe + curl RPC) + +- B2 修复:selectModel 信封为 `{request:{sessionId,provider,model}}`(错误引导消息实测有效) +- B5 修复:commands/list 空数组根因 = GUI 会话 DI 未注册 ISlashCommandCatalog,补 `GeneratedSlashCommandCatalog` 兜底(对齐 TUI SlashCommandSource 先例)→ 84 条 +- B7 修复:历史会话只有 transcript.jsonl 无 meta.json 时 fork 误报 unknown,ForkAsync 容错构造最小 SessionInfo(新增单测 Fork_WithoutMetaFile_FallsBackToTranscriptOnlySession) +- B1 审批弹窗为 UI 人工交互项:`$events` 流已挂 approval 桥(日志 `[DSH-APPROVAL] events stream attached`),待用户在 DSH 前端发触发权限的消息实测 diff --git a/native_gui/apps/NativeGui.Host/core/GlobalUsings.cs b/native_gui/apps/NativeGui.Host/core/GlobalUsings.cs index a1045e11f..00c0dc6ce 100644 --- a/native_gui/apps/NativeGui.Host/core/GlobalUsings.cs +++ b/native_gui/apps/NativeGui.Host/core/GlobalUsings.cs @@ -1,3 +1,6 @@ +// 引擎斜杠命令生成目录(GeneratedSlashCommandCatalog 兜底,JoinCode.ChatCommands 命名空间) +global using JoinCode.ChatCommands; + // DI/日志(原 modules/apps/gui 级 Directory.Build.props 的 Using Include,随迁) global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Logging; diff --git a/native_gui/apps/NativeGui.Host/hosting/JccChatSession.cs b/native_gui/apps/NativeGui.Host/hosting/JccChatSession.cs index 9c913e053..70e44909b 100644 --- a/native_gui/apps/NativeGui.Host/hosting/JccChatSession.cs +++ b/native_gui/apps/NativeGui.Host/hosting/JccChatSession.cs @@ -558,13 +558,12 @@ public Task RewindLastTurnAsync(CancellationToken cancellationToke => _chat.RewindLastTurnAsync(cancellationToken); /// - /// 获取可用斜杠命令清单 — 从 DI 解析 ISlashCommandCatalog(源码生成器在 Composition 中生成)。 - /// 未注册时返回空列表(兜底)。 + /// 获取可用斜杠命令清单 — 从 DI 解析 ISlashCommandCatalog,未注册时兜底直接实例化 + /// GeneratedSlashCommandCatalog(对齐 TUI SlashCommandSource.Load 先例 — GUI 会话模块列表 + /// 不含 Composition slash 注册,靠源码生成目录兜底)。仍为空时返回空列表(降级)。 /// public IReadOnlyList GetAvailableSlashCommands() { - var catalog = _services.GetService(); - if (catalog is null) - return []; + var catalog = _services.GetService() ?? new GeneratedSlashCommandCatalog(); return catalog.ByCategory.SelectMany(g => g.Value).Where(c => !c.IsHidden).ToList(); } diff --git a/native_gui/src/NativeGui.Core/web_host/DshSessionStore.cs b/native_gui/src/NativeGui.Core/web_host/DshSessionStore.cs index 9938a9f5c..7f92898d0 100644 --- a/native_gui/src/NativeGui.Core/web_host/DshSessionStore.cs +++ b/native_gui/src/NativeGui.Core/web_host/DshSessionStore.cs @@ -265,9 +265,13 @@ internal async Task> Projection if (_transcripts is not { } transcripts) { return null; } + // meta 缺失但转录存在的历史会话(旧宿主写入/JSONL 迁移遗留)容错:构造最小 SessionInfo 继续分叉 var sourceInfo = await transcripts.GetSessionInfoAsync(sourceSessionId, cancellationToken); if (sourceInfo is null) { - return null; + if (!await transcripts.TranscriptExistsAsync(sourceSessionId, cancellationToken)) { + return null; + } + sourceInfo = new SessionInfo { Id = sourceSessionId, CreatedAt = DateTime.UtcNow }; } var newId = Guid.NewGuid().ToString("N"); await transcripts.EnsureSessionAsync(newId, sourceInfo with { diff --git a/native_gui/tests/join_code_gui.tests/web_host/DshCommandsTests.cs b/native_gui/tests/join_code_gui.tests/web_host/DshCommandsTests.cs index e6aaf6707..a54d4f656 100644 --- a/native_gui/tests/join_code_gui.tests/web_host/DshCommandsTests.cs +++ b/native_gui/tests/join_code_gui.tests/web_host/DshCommandsTests.cs @@ -15,6 +15,14 @@ private static JsonElement ExecuteArgs(string? agentId, string? line) => private static DshRpcDispatcher NewDispatcher() => new(new DshHostServices(new InMemoryFileSystem())); + + [Fact] + public void GeneratedCatalog_Probe_ByCategoryShouldHaveCommands() { + var catalog = new JoinCode.ChatCommands.GeneratedSlashCommandCatalog(); + var count = catalog.ByCategory.SelectMany(g => g.Value).Count(); + count.Should().BeGreaterThan(0, "GeneratedSlashCommandCatalog 应含引擎 [ChatCommand] 目录真值"); + } + [Fact] public async Task CommandsList_WithoutEngine_ReturnsEmptyStableOk() { var result = await NewDispatcher().HandleAsync("commands/list", default(JsonElement)); diff --git a/native_gui/tests/join_code_gui.tests/web_host/DshSessionForkTests.cs b/native_gui/tests/join_code_gui.tests/web_host/DshSessionForkTests.cs index 9d84fa3ea..e6d82c6d3 100644 --- a/native_gui/tests/join_code_gui.tests/web_host/DshSessionForkTests.cs +++ b/native_gui/tests/join_code_gui.tests/web_host/DshSessionForkTests.cs @@ -58,6 +58,32 @@ public async Task Fork_CopiesHistoryToNewSessionId() { (await transcripts.LoadTranscriptAsync(sourceId, CancellationToken.None)).Count.Should().Be(2); } + [Fact] + public async Task Fork_WithoutMetaFile_FallsBackToTranscriptOnlySession() { + // 真机验收发现:历史会话只有 transcript.jsonl 无 meta.json(旧宿主/#436 迁移遗留), + // GetSessionInfoAsync 返回 null → fork 误报 session/unknown。修复后应容错分叉。 + var fs = new InMemoryFileSystem(); + var services = new DshHostServices(fs); + await using var transcripts = new State.TranscriptService(fs, "/test/sessions"); + services.Sessions.AttachTranscripts(transcripts); + var created = await services.Sessions.CreateAsync(null, @"D:\proj\demo", "deepseek", "deepseek-v4-pro", CancellationToken.None); + var sourceId = (created ?? throw new Xunit.Sdk.XunitException("session/create 失败")).SessionId; + await transcripts.AppendEntriesAsync(sourceId, [ + new TranscriptEntry { Role = "user", Content = "历史问" }, + ], CancellationToken.None); + fs.DeleteFile($"/test/sessions/{sourceId}/meta.json"); + + var dispatcher = new DshRpcDispatcher(services); + var result = await dispatcher.HandleAsync("session/fork", ForkArgs(sourceId)); + + result.Ok.Should().BeTrue(result.Error?.Message); + if (result.Ok) { + var newId = result.Value.GetValueOrDefault().GetProperty("sessionId").GetString() + ?? throw new Xunit.Sdk.XunitException("fork 未返回新会话 ID"); + (await transcripts.LoadTranscriptAsync(newId, CancellationToken.None)).Count.Should().Be(1); + } + } + [Fact] public async Task Fork_CopiesJournalEvents() { var (services, _, sourceId) = await CreateSourceAsync();