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
23 changes: 15 additions & 8 deletions docs/proposed/task/TASK058-DSH引擎深度对齐与DTO统一.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 前端发触发权限的消息实测
3 changes: 3 additions & 0 deletions native_gui/apps/NativeGui.Host/core/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
9 changes: 4 additions & 5 deletions native_gui/apps/NativeGui.Host/hosting/JccChatSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -558,13 +558,12 @@ public Task<RewindResult> RewindLastTurnAsync(CancellationToken cancellationToke
=> _chat.RewindLastTurnAsync(cancellationToken);

/// <summary>
/// 获取可用斜杠命令清单 — 从 DI 解析 <c>ISlashCommandCatalog</c>(源码生成器在 Composition 中生成)。
/// 未注册时返回空列表(兜底)。
/// 获取可用斜杠命令清单 — 从 DI 解析 <c>ISlashCommandCatalog</c>,未注册时兜底直接实例化
/// <c>GeneratedSlashCommandCatalog</c>(对齐 TUI SlashCommandSource.Load 先例 — GUI 会话模块列表
/// 不含 Composition slash 注册,靠源码生成目录兜底)。仍为空时返回空列表(降级)。
/// </summary>
public IReadOnlyList<SlashCommandMetadata> GetAvailableSlashCommands() {
var catalog = _services.GetService<ISlashCommandCatalog>();
if (catalog is null)
return [];
var catalog = _services.GetService<ISlashCommandCatalog>() ?? new GeneratedSlashCommandCatalog();
return catalog.ByCategory.SelectMany(g => g.Value).Where(c => !c.IsHidden).ToList();
}

Expand Down
6 changes: 5 additions & 1 deletion native_gui/src/NativeGui.Core/web_host/DshSessionStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,13 @@ internal async Task<Dictionary<string, DshSessionProjectionSnapshot>> 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down