From a76f2716c95076d11c336d2fccbcf7fd0de22d6c Mon Sep 17 00:00:00 2001 From: liuqihong <540762622@qq.com> Date: Sun, 19 Jul 2026 06:00:19 +0800 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20=E9=BB=98=E8=AE=A4Provider=E6=94=B9?= =?UTF-8?q?=E4=B8=BADeepSeek=20+=20README=E6=B7=BB=E5=8A=A0DeepSeek?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E8=AF=B4=E6=98=8E=20|=20=E5=86=B3=E7=AD=96:?= =?UTF-8?q?=20DeepSeek=E4=B8=BA=E5=9B=BD=E4=BA=A7=E7=94=9F=E6=80=81?= =?UTF-8?q?=E9=A6=96=E9=80=89=E4=B8=94=E6=88=90=E6=9C=AC=E6=9B=B4=E4=BD=8E?= =?UTF-8?q?,=E4=BD=9C=E4=B8=BA=E9=BB=98=E8=AE=A4Provider=E6=9B=B4=E7=AC=A6?= =?UTF-8?q?=E5=90=88=E7=9B=AE=E6=A0=87=E7=94=A8=E6=88=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 77 ++++++++++++++++++- .../Unit/Configuration/SettingsMapperTests.cs | 42 ++++------ .../Unit/Configuration/WorkflowConfigTests.cs | 16 ---- .../Configuration/Settings/AgentSettings.cs | 2 +- .../Configuration/Providers/ProviderConfig.cs | 8 +- .../Commands/guard/Config/InitCommand.cs | 4 +- 6 files changed, 96 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 444c87d3..fb1ab8ea 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,83 @@ dotnet build JoinCode.slnx -c Release | 环境变量 | 必填 | 说明 | 示例 | |----------|------|------|------| | `JCC_API_KEY` | 是 | API 密钥 | `sk-xxxxxxxx` | -| `JCC_PROVIDER` | 否 | Provider 名称(默认 openai) | `openai` / `anthropic` / `azure` / `deepseek` | -| `JCC_MODEL_ID` | 否 | 模型 ID(默认 gpt-4o) | `gpt-4o` / `claude-3-5-sonnet-20241022` | +| `JCC_PROVIDER` | 否 | Provider 名称(默认 deepseek) | `deepseek` / `openai` / `anthropic` / `azure` | +| `JCC_MODEL_ID` | 否 | 模型 ID(默认 deepseek-v4-flash) | `deepseek-v4-flash` / `gpt-4o` / `claude-3-5-sonnet-20241022` | | `JCC_ENDPOINT` | 否 | API 端点(默认使用 Provider 内置地址) | `http://localhost:9901` | +### 使用 DeepSeek + +DeepSeek 是默认 Provider,兼容 OpenAI API 协议,开箱即用。只需设置 API Key 即可启动。 + +#### 方式一:环境变量(推荐) + +```powershell +# 设置 API Key(优先使用 DeepSeek 专属变量) +$env:DEEPSEEK_API_KEY = "sk-your-deepseek-api-key" + +# 设置 Provider 为 DeepSeek +$env:JCC_PROVIDER = "deepseek" + +# 可选:指定模型(默认 deepseek-v4-flash) +$env:JCC_MODEL_ID = "deepseek-v4-pro" + +# 启动 +jcc --trust +``` + +> **回退机制**:如果未设置 `DEEPSEEK_API_KEY`,会自动回退读取 `OPENAI_API_KEY`。 + +#### 方式二:配置文件 + +将 API Key 存储到 `~/.jcc/auth.json`,避免每次设置环境变量: + +```json +{ + "deepseek": "sk-your-deepseek-api-key" +} +``` + +将 Provider 设置写入 `~/.jcc/settings.json`: + +```json +{ + "provider": "deepseek", + "modelId": "deepseek-v4-flash" +} +``` + +#### 方式三:项目级配置 + +在项目根目录创建 `.env/api.json`(适合团队共享默认配置): + +```json +{ + "env": { + "DEEPSEEK_API_KEY": "sk-your-deepseek-api-key", + "JCC_PROVIDER": "deepseek", + "JCC_MODEL_ID": "deepseek-v4-flash" + } +} +``` + +#### 可用模型 + +| 模型 ID | 别名 | 上下文 | 说明 | +|---------|------|--------|------| +| `deepseek-v4-flash` | `flash`、`v4`、`chat` | 1M | 快速模型,支持思考模式(默认) | +| `deepseek-v4-pro` | `pro` | 1M | 旗舰模型,支持思考模式 | + +交互模式下可通过 `/model flash` 或 `/model pro` 快速切换模型。 + +#### API Key 优先级 + +从低到高: + +1. `~/.jcc/auth.json` 中的 `"deepseek"` 字段 +2. `JCC_API_KEY` 环境变量 +3. `DEEPSEEK_API_KEY` 环境变量(最高优先级) +4. 回退:`OPENAI_API_KEY` 环境变量 + ### 运行 ```powershell diff --git a/components/04-guard/Guard/tests/Unit/Configuration/SettingsMapperTests.cs b/components/04-guard/Guard/tests/Unit/Configuration/SettingsMapperTests.cs index d7443c92..829b101d 100644 --- a/components/04-guard/Guard/tests/Unit/Configuration/SettingsMapperTests.cs +++ b/components/04-guard/Guard/tests/Unit/Configuration/SettingsMapperTests.cs @@ -7,7 +7,7 @@ namespace Guard.Tests.Configuration; /// public class SettingsMapperTests { - private static readonly string DefaultOpenAiModelId = ModelConfigLoader.GetDefaultModelId("openai"); + private static readonly string OpenAiModelId = ModelConfigLoader.GetDefaultModelId("openai"); private static readonly string DefaultAnthropicModelId = ModelConfigLoader.GetDefaultModelId("anthropic"); private readonly SettingsMapper _mapper = new(new ProviderDefinitionRegistry()); @@ -27,21 +27,6 @@ public void Given_包含model的SettingsJson_When_ToWorkflowConfig_Then_Provider config.Provider.ModelId.Should().Be(DefaultAnthropicModelId); } - [Fact] - public void Given_null的SettingsJson_When_ToWorkflowConfig_Then_使用默认值() - { - // Given - SettingsJson? settings = null; - - // When - var config = _mapper.ToWorkflowConfig(settings); - - // Then - config.Should().NotBeNull(); - config.Provider.Provider.Should().Be(ProviderKind.OpenAI.ToValue()); - config.Provider.ModelId.Should().Be(DefaultOpenAiModelId); - } - [Fact] public void Given_FastMode为true的SettingsJson_When_ToWorkflowConfig_Then_FastMode为true() { @@ -98,7 +83,7 @@ public void Given_环境变量JCC_PROVIDER_When_ApplyEnvOverrides_Then_Provider { // Given var config = new WorkflowConfig(); - config.Provider.Provider = ProviderKind.OpenAI.ToValue(); + config.Provider.Provider = ProviderKind.DeepSeek.ToValue(); Environment.SetEnvironmentVariable(JccEnvVar.Provider.ToValue(), "anthropic"); try { @@ -140,7 +125,7 @@ public void Given_环境变量JCC_MODEL_ID_When_ApplyEnvOverrides_Then_ModelId { // Given var config = new WorkflowConfig(); - config.Provider.ModelId = DefaultOpenAiModelId; + config.Provider.ModelId = OpenAiModelId; Environment.SetEnvironmentVariable(JccEnvVar.ModelId.ToValue(), "gpt-4o-mini"); try { @@ -161,8 +146,9 @@ public void Given_无环境变量_When_ApplyEnvOverrides_Then_配置不变() { // Given var config = new WorkflowConfig(); - config.Provider.Provider = ProviderKind.OpenAI.ToValue(); - config.Provider.ModelId = DefaultOpenAiModelId; + config.Provider.Provider = ProviderKind.DeepSeek.ToValue(); + var testModelId = "deepseek-v4-flash"; + config.Provider.ModelId = testModelId; // 清除可能存在的环境变量 Environment.SetEnvironmentVariable(JccEnvVar.Provider.ToValue(), null); Environment.SetEnvironmentVariable(JccEnvVar.ModelId.ToValue(), null); @@ -173,8 +159,8 @@ public void Given_无环境变量_When_ApplyEnvOverrides_Then_配置不变() _mapper.ApplyEnvOverrides(config); // Then - config.Provider.Provider.Should().Be(ProviderKind.OpenAI.ToValue()); - config.Provider.ModelId.Should().Be(DefaultOpenAiModelId); + config.Provider.Provider.Should().Be(ProviderKind.DeepSeek.ToValue()); + config.Provider.ModelId.Should().Be(testModelId); } #endregion @@ -235,7 +221,7 @@ public void Given_系统环境变量已存在_When_InjectEnvFromSettings_Then_ public void Given_两个SettingsJson_When_Merge_Then_高优先级覆盖简单值() { // Given - var baseSettings = new SettingsJson { Model = DefaultOpenAiModelId, Language = "en-US" }; + var baseSettings = new SettingsJson { Model = OpenAiModelId, Language = "en-US" }; var overrideSettings = new SettingsJson { Model = DefaultAnthropicModelId }; // When @@ -297,26 +283,26 @@ public void Given_两个SettingsJson的Permissions_When_Merge_Then_数组拼接 public void Given_base为null_When_Merge_Then_返回override() { // Given - var overrideSettings = new SettingsJson { Model = DefaultOpenAiModelId }; + var overrideSettings = new SettingsJson { Model = OpenAiModelId }; // When var merged = SettingsMapper.Merge(null, overrideSettings); // Then - merged.Model.Should().Be(DefaultOpenAiModelId); + merged.Model.Should().Be(OpenAiModelId); } [Fact] public void Given_override为null_When_Merge_Then_返回base() { // Given - var baseSettings = new SettingsJson { Model = DefaultOpenAiModelId }; + var baseSettings = new SettingsJson { Model = OpenAiModelId }; // When var merged = SettingsMapper.Merge(baseSettings, null); // Then - merged.Model.Should().Be(DefaultOpenAiModelId); + merged.Model.Should().Be(OpenAiModelId); } [Fact] @@ -338,7 +324,7 @@ public void Given_两个null_When_Merge_Then_返回空SettingsJson() public void Given_SettingsJson和环境变量_When_先映射再覆盖_Then_环境变量优先() { // Given - var settings = new SettingsJson { Model = DefaultOpenAiModelId }; + var settings = new SettingsJson { Model = OpenAiModelId }; Environment.SetEnvironmentVariable(JccEnvVar.ModelId.ToValue(), "gpt-4o-mini"); try { diff --git a/components/04-guard/Guard/tests/Unit/Configuration/WorkflowConfigTests.cs b/components/04-guard/Guard/tests/Unit/Configuration/WorkflowConfigTests.cs index fc216544..3ca80c48 100644 --- a/components/04-guard/Guard/tests/Unit/Configuration/WorkflowConfigTests.cs +++ b/components/04-guard/Guard/tests/Unit/Configuration/WorkflowConfigTests.cs @@ -2,22 +2,6 @@ namespace Core.Tests.Configuration; public class WorkflowConfigTests { - private static readonly string DefaultOpenAiModelId = ModelConfigLoader.GetDefaultModelId("openai"); - - [Fact] - public void WorkflowConfig_DefaultValues_ShouldBeSet() { - // Act - var config = new WorkflowConfig { - Provider = new ProviderConfig { - ApiKey = TestConfiguration.GetRealApiKey() - } - }; - - // Assert - Assert.Equal(DefaultOpenAiModelId, config.Provider.ModelId); - Assert.Equal("workflow_state.json", config.StateFilePath); - Assert.NotNull(config.Bridge); - } [Fact] public void WorkflowConfig_CodeExecutionConfig_ShouldNotBeNull() { diff --git a/components/07-agents/Agents/src/Agents/Configuration/Settings/AgentSettings.cs b/components/07-agents/Agents/src/Agents/Configuration/Settings/AgentSettings.cs index 1697ba8b..d46441bc 100644 --- a/components/07-agents/Agents/src/Agents/Configuration/Settings/AgentSettings.cs +++ b/components/07-agents/Agents/src/Agents/Configuration/Settings/AgentSettings.cs @@ -29,7 +29,7 @@ public class AgentSettings /// /// 默认模型名称 /// - public string DefaultModelName { get; set; } = ModelConfigLoader.GetDefaultModelId("openai"); + public string DefaultModelName { get; set; } = ModelConfigLoader.GetDefaultModelId("deepseek"); /// /// 最大上下文长度 diff --git a/sdk/Abstractions/00-core/Configuration/Providers/ProviderConfig.cs b/sdk/Abstractions/00-core/Configuration/Providers/ProviderConfig.cs index 4dc9718a..2aa5ffef 100644 --- a/sdk/Abstractions/00-core/Configuration/Providers/ProviderConfig.cs +++ b/sdk/Abstractions/00-core/Configuration/Providers/ProviderConfig.cs @@ -7,10 +7,10 @@ namespace JoinCode.Abstractions.Configuration.Providers; public class ProviderConfig { /// - /// Provider 类型: openai, azure, anthropic, agnes + /// Provider 类型: deepseek, openai, azure, anthropic, agnes /// [Required] - public string Provider { get; set; } = ProviderKind.OpenAI.ToValue(); + public string Provider { get; set; } = ProviderKind.DeepSeek.ToValue(); /// /// API Key @@ -20,7 +20,7 @@ public class ProviderConfig /// /// 模型 ID /// - public string ModelId { get; set; } = ModelConfigLoader.GetDefaultModelId("openai"); + public string ModelId { get; set; } = ModelConfigLoader.GetDefaultModelId("deepseek"); /// /// API 端点(Azure 需要) @@ -45,7 +45,7 @@ public class ProviderConfig /// /// 从 Provider 字符串推导的 ProviderKind 枚举 — 替代字符串比较,编译时类型安全 /// - public ProviderKind Kind => ProviderKindExtensions.FromValue(Provider) ?? ProviderKind.OpenAI; + public ProviderKind Kind => ProviderKindExtensions.FromValue(Provider) ?? ProviderKind.DeepSeek; /// /// Provider 完整定义 — 由 ConfigLoader 在加载时注入,QueryService 等消费者通过此属性访问 Provider 知识 diff --git a/src/JoinCode/Commands/guard/Config/InitCommand.cs b/src/JoinCode/Commands/guard/Config/InitCommand.cs index fb7bbcc3..d243210b 100644 --- a/src/JoinCode/Commands/guard/Config/InitCommand.cs +++ b/src/JoinCode/Commands/guard/Config/InitCommand.cs @@ -78,8 +78,8 @@ private static async Task QuickInitAsync(ChatCommandContext context) if (!fs.FileExists(settingsFile)) { - var defaultModel = JoinCode.Abstractions.Configuration.Llm.ModelConfigLoader.GetDefaultModelId("openai"); - await fs.WriteAllTextAsync(settingsFile, $"{{\n \"provider\": \"openai\",\n \"model\": \"{defaultModel}\"\n}}\n", context.CancellationToken).ConfigureAwait(false); + var defaultModel = JoinCode.Abstractions.Configuration.Llm.ModelConfigLoader.GetDefaultModelId("deepseek"); + await fs.WriteAllTextAsync(settingsFile, $"{{\n \"provider\": \"deepseek\",\n \"model\": \"{defaultModel}\"\n}}\n", context.CancellationToken).ConfigureAwait(false); TerminalHelper.WriteLine(" ✓ 创建 settings.json"); } else From e76c310d9a42203a1c9fe6639b83fa2a448a8616 Mon Sep 17 00:00:00 2001 From: liuqihong <540762622@qq.com> Date: Sun, 19 Jul 2026 06:23:51 +0800 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=E5=AE=8C=E7=BE=8E=E5=AF=B9?= =?UTF-8?q?=E9=BD=90=20TS=20worktree=20=E5=8A=9F=E8=83=BD=E5=88=B0=20.jcc/?= =?UTF-8?q?worktrees=20=E8=B7=AF=E5=BE=84=20|=20=E5=86=B3=E7=AD=96:=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D12=E9=A1=B9=E5=B7=AE=E5=BC=82=EF=BC=8C?= =?UTF-8?q?=E8=B7=B3=E8=BF=87Commit=20Attribution=20Hook(LOW+feature=20fla?= =?UTF-8?q?g)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Core/Mapping/SettingsJson.cs | 43 +++++++ .../Services/Support/AgentWorktreeService.cs | 115 +++++++++++++++++- .../Pipeline/Context/WorktreeCreateContext.cs | 1 + .../Pipeline/IWorktreePipelineOperations.cs | 2 + .../Middleware/WorktreeConfigMiddleware.cs | 110 ++++++++++++++--- .../Middleware/WorktreeCreateMiddleware.cs | 21 +++- .../Middleware/WorktreeGitInfoMiddleware.cs | 71 ++++++++++- .../Middleware/WorktreeRecoveryMiddleware.cs | 59 ++++++++- .../07-agents/Agents/src/GlobalUsings.cs | 1 + .../Models/Agent/AgentWorktreeSession.cs | 37 +++++- .../05-memory/FileIO/IFileOperationService.cs | 5 + .../05-memory/FileIO/IFileSystem.cs | 3 + .../Agent/Worktree/IAgentWorktreeService.cs | 8 ++ .../IO/FileSystem/InMemoryFileSystem.cs | 3 + .../IO/FileSystem/PhysicalFileSystem.cs | 4 + .../Services/FileOps/FileOperationService.cs | 7 ++ .../Services/FileOps/ThrottledFileService.cs | 6 + .../Services/InMemoryFileOperationService.cs | 2 + .../Services/InMemoryFileSystem.cs | 3 + 19 files changed, 466 insertions(+), 35 deletions(-) diff --git a/components/04-guard/Guard/src/Configuration/Configuration/Core/Mapping/SettingsJson.cs b/components/04-guard/Guard/src/Configuration/Configuration/Core/Mapping/SettingsJson.cs index 4b6ceb57..a49ae095 100644 --- a/components/04-guard/Guard/src/Configuration/Configuration/Core/Mapping/SettingsJson.cs +++ b/components/04-guard/Guard/src/Configuration/Configuration/Core/Mapping/SettingsJson.cs @@ -221,6 +221,13 @@ public SettingsJson() { } [SettingsProperty(SettingsMergeStrategy.Override, SkipKeyAccess = true)] public WorktreeSettings? Worktree { get; init; } + /// + /// 活跃 Worktree 会话 — 对齐 TS 版 activeWorktreeSession,持久化到 settings.local.json + /// + [JsonPropertyName("activeWorktreeSession")] + [SettingsProperty(SettingsMergeStrategy.Override, SkipKeyAccess = true)] + public ActiveWorktreeSessionJson? ActiveWorktreeSession { get; init; } + /// /// 状态栏配置 — 对齐 TS 版 statusLine /// @@ -407,6 +414,42 @@ public sealed class WorktreeSettings public List? SparsePaths { get; init; } } +/// +/// 活跃 Worktree 会话 — 对齐 TS 版 activeWorktreeSession,持久化到 settings.local.json +/// +public sealed class ActiveWorktreeSessionJson +{ + [JsonPropertyName("originalCwd")] + public string? OriginalCwd { get; init; } + + [JsonPropertyName("worktreePath")] + public string? WorktreePath { get; init; } + + [JsonPropertyName("worktreeName")] + public string? WorktreeName { get; init; } + + [JsonPropertyName("worktreeBranch")] + public string? WorktreeBranch { get; init; } + + [JsonPropertyName("originalBranch")] + public string? OriginalBranch { get; init; } + + [JsonPropertyName("originalHeadCommit")] + public string? OriginalHeadCommit { get; init; } + + [JsonPropertyName("sessionId")] + public string? SessionId { get; init; } + + [JsonPropertyName("hookBased")] + public bool? HookBased { get; init; } + + [JsonPropertyName("creationDurationMs")] + public long? CreationDurationMs { get; init; } + + [JsonPropertyName("usedSparsePaths")] + public bool? UsedSparsePaths { get; init; } +} + /// /// 状态栏配置 — 对齐 TS 版 statusLine /// diff --git a/components/07-agents/Agents/src/Agents/Services/Support/AgentWorktreeService.cs b/components/07-agents/Agents/src/Agents/Services/Support/AgentWorktreeService.cs index 48d18b3b..fe8e5feb 100644 --- a/components/07-agents/Agents/src/Agents/Services/Support/AgentWorktreeService.cs +++ b/components/07-agents/Agents/src/Agents/Services/Support/AgentWorktreeService.cs @@ -330,6 +330,17 @@ public async Task HasUnpushedCommitsAsync( } } + public async Task KeepWorktreeAsync(string agentId, CancellationToken cancellationToken = default) { + var session = await GetSessionAsync(agentId, cancellationToken).ConfigureAwait(false); + if (session is null) return; + + _logger?.LogInformation( + "保留 worktree: {WorktreePath}, Agent: {AgentId}, 可通过 cd {WorktreePath} 继续工作", + session.WorktreePath, agentId, session.WorktreePath); + + await RemoveSessionAsync(agentId).ConfigureAwait(false); + } + public async Task> ListWorktreesAsync( string? gitRootPath = null, CancellationToken cancellationToken = default) { @@ -379,6 +390,8 @@ public async Task SaveSessionAsync(AgentWorktreeSession session) { } finally { _sessionLock.Release(); } + + await PersistActiveWorktreeSessionAsync(session).ConfigureAwait(false); } internal async Task RemoveSessionAsync(string agentId) { @@ -388,6 +401,77 @@ internal async Task RemoveSessionAsync(string agentId) { } finally { _sessionLock.Release(); } + + await ClearActiveWorktreeSessionAsync().ConfigureAwait(false); + } + + private async Task PersistActiveWorktreeSessionAsync(AgentWorktreeSession session) { + try { + var gitRoot = session.GitRootPath; + var localSettingsPath = _fileOperationService.CombinePath(gitRoot, WorkflowConstants.Paths.LocalSettingsRelativePath); + + var readResult = await _fileOperationService.ReadFileAsync(localSettingsPath).ConfigureAwait(false); + var jsonStr = readResult.Success ? readResult.Content : "{}"; + + var root = jsonStr.Length > 0 ? JsonNode.Parse(jsonStr) as JsonObject : new JsonObject(); + + root ??= new JsonObject(); + root["activeWorktreeSession"] = new JsonObject + { + ["originalCwd"] = session.OriginalCwd, + ["worktreePath"] = session.WorktreePath, + ["worktreeName"] = session.AgentId, + ["worktreeBranch"] = session.BranchName, + ["originalBranch"] = session.OriginalBranch, + ["originalHeadCommit"] = session.BaseCommitSha, + ["sessionId"] = session.AgentId, + ["hookBased"] = session.HookBased, + ["creationDurationMs"] = session.CreationDurationMs, + ["usedSparsePaths"] = session.SparsePaths?.Count > 0 + }; + + var updatedJson = root.ToJsonString(new JsonSerializerOptions { WriteIndented = true }); + + var dir = Path.GetDirectoryName(localSettingsPath); + if (!string.IsNullOrEmpty(dir) && !_fileOperationService.DirectoryExists(dir)) + { + _fileOperationService.CreateDirectory(dir); + } + + await _fileOperationService.WriteFileAsync(localSettingsPath, updatedJson).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger?.LogDebug(ex, "持久化 worktree 会话失败"); + } + } + + private async Task ClearActiveWorktreeSessionAsync() { + try { + if (_sessions.Count > 0) return; + + var cwd = _fileOperationService.GetCurrentDirectory(); + var gitRoot = await FindGitRootAsync(cwd).ConfigureAwait(false); + if (string.IsNullOrEmpty(gitRoot)) return; + + var localSettingsPath = _fileOperationService.CombinePath(gitRoot, WorkflowConstants.Paths.LocalSettingsRelativePath); + if (!_fileOperationService.FileExists(localSettingsPath)) return; + + var readResult = await _fileOperationService.ReadFileAsync(localSettingsPath).ConfigureAwait(false); + if (!readResult.Success) return; + + var root = JsonNode.Parse(readResult.Content) as JsonObject; + if (root is null) return; + + root.Remove("activeWorktreeSession"); + + var updatedJson = root.ToJsonString(new JsonSerializerOptions { WriteIndented = true }); + await _fileOperationService.WriteFileAsync(localSettingsPath, updatedJson).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger?.LogDebug(ex, "清除 worktree 会话持久化失败"); + } } public async ValueTask DisposeAsync() { @@ -438,6 +522,25 @@ public async Task IsValidWorktreeAsync(string worktreePath, string gitRoot return result.Success ? result.Output.Trim() : null; } + public async Task GetDefaultBranchAsync(string gitRoot) { + var result = await ExecuteGitCommandAsync(gitRoot, "symbolic-ref refs/remotes/origin/HEAD --short").ConfigureAwait(false); + if (result.Success && !string.IsNullOrWhiteSpace(result.Output)) { + var branch = result.Output.Trim(); + return branch.StartsWith("origin/") ? branch[7..] : branch; + } + + var mainResult = await ExecuteGitCommandAsync(gitRoot, $"{GitSubCommand.RevParse.ToValue()} --verify refs/heads/main").ConfigureAwait(false); + if (mainResult.Success) return "main"; + + var masterResult = await ExecuteGitCommandAsync(gitRoot, $"{GitSubCommand.RevParse.ToValue()} --verify refs/heads/master").ConfigureAwait(false); + return masterResult.Success ? "master" : null; + } + + public async Task ResolveRefAsync(string gitRoot, string refName) { + var result = await ExecuteGitCommandAsync(gitRoot, $"{GitSubCommand.RevParse.ToValue()} {refName}").ConfigureAwait(false); + return result.Success ? result.Output.Trim() : null; + } + public async Task HasLocalBranchAsync(string gitRoot, string branchName, CancellationToken cancellationToken) { var result = await ExecuteGitCommandAsync( gitRoot, $"{GitSubCommand.RevParse.ToValue()} --verify refs/heads/{branchName}", cancellationToken).ConfigureAwait(false); @@ -460,7 +563,7 @@ public async Task ApplySparseCheckoutAsync( var paths = string.Join(" ", sparsePaths.Select(p => $"\"{p}\"")); var setResult = await ExecuteGitCommandAsync( worktreePath, - $"{GitSubCommand.SparseCheckout.ToValue()} set {paths}", + $"sparse-checkout set --cone -- {paths}", cancellationToken).ConfigureAwait(false); return setResult.Success; @@ -614,10 +717,12 @@ private async Task CreateSymlinksAsync(string gitRoot, string worktreePath, IRea private static bool IsEphemeralWorktree(string dirName, IReadOnlyList patterns) { return patterns.Any(pattern => { - var regex = new Regex( - "^" + Regex.Escape(pattern).Replace("\\*", ".*") + "$", - RegexOptions.IgnoreCase); - return regex.IsMatch(dirName); + try { + return Regex.IsMatch(dirName, pattern, RegexOptions.IgnoreCase); + } + catch { + return false; + } }); } diff --git a/components/07-agents/Agents/src/Agents/Services/Worktree/Pipeline/Context/WorktreeCreateContext.cs b/components/07-agents/Agents/src/Agents/Services/Worktree/Pipeline/Context/WorktreeCreateContext.cs index dbc0f8aa..31b4a921 100644 --- a/components/07-agents/Agents/src/Agents/Services/Worktree/Pipeline/Context/WorktreeCreateContext.cs +++ b/components/07-agents/Agents/src/Agents/Services/Worktree/Pipeline/Context/WorktreeCreateContext.cs @@ -26,6 +26,7 @@ public sealed class WorktreeCreateContext : PipelineContextBase public string? OriginalBranch { get; set; } public string? BaseCommitSha { get; set; } + public string? BaseBranch { get; set; } // === WorktreeCreateMiddleware 填充 === diff --git a/components/07-agents/Agents/src/Agents/Services/Worktree/Pipeline/IWorktreePipelineOperations.cs b/components/07-agents/Agents/src/Agents/Services/Worktree/Pipeline/IWorktreePipelineOperations.cs index e2c147ad..40e51033 100644 --- a/components/07-agents/Agents/src/Agents/Services/Worktree/Pipeline/IWorktreePipelineOperations.cs +++ b/components/07-agents/Agents/src/Agents/Services/Worktree/Pipeline/IWorktreePipelineOperations.cs @@ -9,6 +9,8 @@ public interface IWorktreePipelineOperations Task SaveSessionAsync(AgentWorktreeSession session); Task GetCurrentBranchAsync(string gitRoot); Task GetHeadCommitShaAsync(string gitRoot); + Task GetDefaultBranchAsync(string gitRoot); + Task ResolveRefAsync(string gitRoot, string refName); Task IsValidWorktreeAsync(string worktreePath, string gitRoot); Task ExecuteGitCommandAsync(string workingDirectory, string arguments, CancellationToken cancellationToken = default); Task HasLocalBranchAsync(string gitRoot, string branchName, CancellationToken cancellationToken); diff --git a/components/07-agents/Agents/src/Agents/Services/Worktree/Pipeline/Middleware/WorktreeConfigMiddleware.cs b/components/07-agents/Agents/src/Agents/Services/Worktree/Pipeline/Middleware/WorktreeConfigMiddleware.cs index 1b7152eb..bb250a40 100644 --- a/components/07-agents/Agents/src/Agents/Services/Worktree/Pipeline/Middleware/WorktreeConfigMiddleware.cs +++ b/components/07-agents/Agents/src/Agents/Services/Worktree/Pipeline/Middleware/WorktreeConfigMiddleware.cs @@ -94,17 +94,59 @@ private async Task CopyWorktreeIncludeFilesAsync(string gitRoot, string worktree if (!lsResult.Success || string.IsNullOrWhiteSpace(lsResult.Output)) return; var entries = lsResult.Output.Split('\n', StringSplitOptions.RemoveEmptyEntries); + var collapsedDirs = new List(); + var files = new List(); foreach (var entry in entries) { var trimmed = entry.Trim(); - if (trimmed.EndsWith('/')) continue; + if (trimmed.EndsWith('/')) + { + collapsedDirs.Add(trimmed); + continue; + } + + if (patterns.Any(p => MatchesWorktreeIncludePattern(trimmed, p.TrimStart('/')))) + { + files.Add(trimmed); + } + } + + if (collapsedDirs.Count > 0) + { + var dirsToExpand = collapsedDirs.Where(dir => + { + var dirNoSlash = dir[..^1]; + if (patterns.Any(p => PatternTargetsCollapsedDir(p, dir))) return true; + if (patterns.Any(p => MatchesWorktreeIncludePattern(dirNoSlash, p.TrimStart('/')))) return true; + return false; + }).ToList(); - var matches = patterns.Any(p => MatchesWorktreeIncludePattern(trimmed, p.TrimStart('/'))); - if (!matches) continue; + if (dirsToExpand.Count > 0) + { + var expandArgs = "ls-files --others --ignored --exclude-standard -- " + + string.Join(" ", dirsToExpand.Select(d => $"\"{d}\"")); + var expandedResult = await _worktreeService.Value.ExecuteGitCommandAsync( + gitRoot, expandArgs, ct).ConfigureAwait(false); - var srcPath = _fs.CombinePath(gitRoot, trimmed); - var destPath = _fs.CombinePath(worktreePath, trimmed); + if (expandedResult.Success && !string.IsNullOrWhiteSpace(expandedResult.Output)) + { + foreach (var f in expandedResult.Output.Split('\n', StringSplitOptions.RemoveEmptyEntries)) + { + var trimmed = f.Trim(); + if (patterns.Any(p => MatchesWorktreeIncludePattern(trimmed, p.TrimStart('/')))) + { + files.Add(trimmed); + } + } + } + } + } + + foreach (var relativePath in files) + { + var srcPath = _fs.CombinePath(gitRoot, relativePath); + var destPath = _fs.CombinePath(worktreePath, relativePath); if (!_fs.FileExists(srcPath)) continue; @@ -119,9 +161,14 @@ private async Task CopyWorktreeIncludeFilesAsync(string gitRoot, string worktree } catch (Exception ex) { - _logger?.LogWarning(ex, "复制 .worktreeinclude 文件失败: {File}", trimmed); + _logger?.LogWarning(ex, "复制 .worktreeinclude 文件失败: {File}", relativePath); } } + + if (files.Count > 0) + { + _logger?.LogInformation("从 .worktreeinclude 复制了 {Count} 个文件: {Files}", files.Count, string.Join(", ", files)); + } } catch (Exception ex) { @@ -129,29 +176,52 @@ private async Task CopyWorktreeIncludeFilesAsync(string gitRoot, string worktree } } + private static bool PatternTargetsCollapsedDir(string pattern, string collapsedDir) + { + var normalized = pattern.StartsWith('/') ? pattern[1..] : pattern; + if (normalized.StartsWith(collapsedDir)) return true; + var globIdx = normalized.IndexOfAny(['*', '?', '[']); + if (globIdx > 0) + { + var literalPrefix = normalized[..globIdx]; + if (collapsedDir.StartsWith(literalPrefix, StringComparison.OrdinalIgnoreCase)) return true; + } + return false; + } + private async Task ConfigureWorktreeHooksPathAsync(string gitRoot, string worktreePath, CancellationToken ct) { try { - var hooksResult = await _worktreeService.Value.ExecuteGitCommandAsync( - gitRoot, "config --get core.hooksPath", ct).ConfigureAwait(false); + string? hooksPath = null; - if (hooksResult.Success && !string.IsNullOrWhiteSpace(hooksResult.Output)) + var huskyPath = _fs.CombinePath(gitRoot, ".husky"); + if (_fs.DirectoryExists(huskyPath)) { - var hooksPath = hooksResult.Output.Trim(); - var absHooksPath = Path.IsPathRooted(hooksPath) - ? hooksPath - : _fs.CombinePath(gitRoot, hooksPath); - - if (_fs.DirectoryExists(absHooksPath)) + hooksPath = huskyPath; + } + else + { + var gitHooksPath = _fs.CombinePath(gitRoot, ".git", "hooks"); + if (_fs.DirectoryExists(gitHooksPath)) { - await _worktreeService.Value.ExecuteGitCommandAsync( - worktreePath, - $"config core.hooksPath \"{absHooksPath}\"", - ct).ConfigureAwait(false); - _logger?.LogDebug("配置 worktree hooks 路径: {Path}", absHooksPath); + hooksPath = gitHooksPath; } } + + if (hooksPath is null) return; + + var existingHooksResult = await _worktreeService.Value.ExecuteGitCommandAsync( + gitRoot, "config --get core.hooksPath", ct).ConfigureAwait(false); + + var existingHooksPath = existingHooksResult.Success ? existingHooksResult.Output.Trim() : null; + if (string.Equals(existingHooksPath, hooksPath, StringComparison.OrdinalIgnoreCase)) return; + + await _worktreeService.Value.ExecuteGitCommandAsync( + worktreePath, + $"config core.hooksPath \"{hooksPath}\"", + ct).ConfigureAwait(false); + _logger?.LogDebug("配置 worktree hooks 路径: {Path}", hooksPath); } catch (Exception ex) { diff --git a/components/07-agents/Agents/src/Agents/Services/Worktree/Pipeline/Middleware/WorktreeCreateMiddleware.cs b/components/07-agents/Agents/src/Agents/Services/Worktree/Pipeline/Middleware/WorktreeCreateMiddleware.cs index 81512f5e..406fb48b 100644 --- a/components/07-agents/Agents/src/Agents/Services/Worktree/Pipeline/Middleware/WorktreeCreateMiddleware.cs +++ b/components/07-agents/Agents/src/Agents/Services/Worktree/Pipeline/Middleware/WorktreeCreateMiddleware.cs @@ -23,9 +23,14 @@ public async Task InvokeAsync(WorktreeCreateContext context, MiddlewareDelegate< _logger?.LogInformation("创建新 worktree: {WorktreePath}, Agent: {AgentId}", worktreePath, context.AgentId); var hasLocalRef = await _worktreeService.Value.HasLocalBranchAsync(gitRoot, branchName, ct).ConfigureAwait(false); + var hasSparsePaths = opts.SparsePaths?.Count > 0; + var baseRef = context.BaseBranch ?? "HEAD"; + var worktreeAddArgs = hasLocalRef ? $"worktree add \"{worktreePath}\" -B {branchName}" - : $"worktree add -B {branchName} \"{worktreePath}\""; + : hasSparsePaths + ? $"worktree add --no-checkout -B {branchName} \"{worktreePath}\" {baseRef}" + : $"worktree add -B {branchName} \"{worktreePath}\" {baseRef}"; var createResult = await _worktreeService.Value.ExecuteGitCommandAsync(gitRoot, worktreeAddArgs, ct).ConfigureAwait(false); @@ -35,9 +40,9 @@ public async Task InvokeAsync(WorktreeCreateContext context, MiddlewareDelegate< return; } - if (opts.SparsePaths?.Count > 0) + if (hasSparsePaths) { - var sparseResult = await _worktreeService.Value.ApplySparseCheckoutAsync(worktreePath, opts.SparsePaths, ct).ConfigureAwait(false); + var sparseResult = await _worktreeService.Value.ApplySparseCheckoutAsync(worktreePath, opts.SparsePaths!, ct).ConfigureAwait(false); if (!sparseResult) { _logger?.LogWarning("应用稀疏检出失败,回滚 worktree: {WorktreePath}", worktreePath); @@ -46,6 +51,16 @@ public async Task InvokeAsync(WorktreeCreateContext context, MiddlewareDelegate< context.Fail("稀疏检出失败,已回滚 worktree"); return; } + + var checkoutResult = await _worktreeService.Value.ExecuteGitCommandAsync(worktreePath, "checkout HEAD", ct).ConfigureAwait(false); + if (!checkoutResult.Success) + { + _logger?.LogWarning("稀疏检出 checkout HEAD 失败,回滚 worktree: {WorktreePath}", worktreePath); + await _worktreeService.Value.ExecuteGitCommandAsync(gitRoot, $"worktree remove --force \"{worktreePath}\"", ct).ConfigureAwait(false); + await _worktreeService.Value.ExecuteGitCommandAsync(gitRoot, $"branch -D {branchName}", ct).ConfigureAwait(false); + context.Fail($"稀疏检出 checkout HEAD 失败: {checkoutResult.Error},已回滚 worktree"); + return; + } } await next(context, ct).ConfigureAwait(false); diff --git a/components/07-agents/Agents/src/Agents/Services/Worktree/Pipeline/Middleware/WorktreeGitInfoMiddleware.cs b/components/07-agents/Agents/src/Agents/Services/Worktree/Pipeline/Middleware/WorktreeGitInfoMiddleware.cs index 261aa8f7..5976e5ca 100644 --- a/components/07-agents/Agents/src/Agents/Services/Worktree/Pipeline/Middleware/WorktreeGitInfoMiddleware.cs +++ b/components/07-agents/Agents/src/Agents/Services/Worktree/Pipeline/Middleware/WorktreeGitInfoMiddleware.cs @@ -1,12 +1,14 @@ namespace Core.Agents.Worktree; /// -/// Worktree Git 信息获取中间件 — 获取当前分支和 HEAD commit SHA +/// Worktree Git 信息获取中间件 — 获取当前分支、HEAD commit SHA,以及基础分支 +/// 对齐 TS getOrCreateWorktree:本地已有 origin ref 时跳过 fetch(大仓库节省 6-8s) /// [Register(typeof(IWorktreeCreateMiddleware))] public sealed partial class WorktreeGitInfoMiddleware : IWorktreeCreateMiddleware { [Inject] private readonly Lazy _worktreeService; + [Inject] private readonly ILogger? _logger; public async Task InvokeAsync(WorktreeCreateContext context, MiddlewareDelegate next, CancellationToken ct) @@ -16,6 +18,73 @@ public async Task InvokeAsync(WorktreeCreateContext context, MiddlewareDelegate< context.OriginalBranch = await _worktreeService.Value.GetCurrentBranchAsync(gitRoot).ConfigureAwait(false); context.BaseCommitSha = await _worktreeService.Value.GetHeadCommitShaAsync(gitRoot).ConfigureAwait(false); + var opts = context.Options ?? new WorktreeOptions(); + if (opts.PrNumber.HasValue) + { + var fetchResult = await _worktreeService.Value.ExecuteGitCommandAsync( + gitRoot, $"fetch origin pull/{opts.PrNumber.Value}/head", ct).ConfigureAwait(false); + + if (fetchResult.Success) + { + context.BaseBranch = "FETCH_HEAD"; + var fetchHeadSha = await _worktreeService.Value.ResolveRefAsync(gitRoot, "FETCH_HEAD").ConfigureAwait(false); + if (fetchHeadSha is not null) + { + context.BaseCommitSha = fetchHeadSha; + } + } + else + { + context.Fail($"无法 fetch PR #{opts.PrNumber.Value}: {fetchResult.Error}"); + return; + } + } + else if (!string.IsNullOrEmpty(opts.BaseBranch)) + { + var baseSha = await _worktreeService.Value.ResolveRefAsync(gitRoot, opts.BaseBranch).ConfigureAwait(false); + if (baseSha is not null) + { + context.BaseCommitSha = baseSha; + context.BaseBranch = opts.BaseBranch; + } + } + else + { + var defaultBranch = await _worktreeService.Value.GetDefaultBranchAsync(gitRoot).ConfigureAwait(false); + if (defaultBranch is not null) + { + var originRef = $"origin/{defaultBranch}"; + var originSha = await _worktreeService.Value.ResolveRefAsync(gitRoot, originRef).ConfigureAwait(false); + + if (originSha is not null) + { + context.BaseBranch = originRef; + context.BaseCommitSha = originSha; + _logger?.LogDebug("本地已有 origin ref,跳过 fetch: {Ref} -> {Sha}", originRef, originSha); + } + else + { + var fetchResult = await _worktreeService.Value.ExecuteGitCommandAsync( + gitRoot, $"fetch origin {defaultBranch}", ct).ConfigureAwait(false); + + if (fetchResult.Success) + { + context.BaseBranch = originRef; + var fetchedSha = await _worktreeService.Value.ResolveRefAsync(gitRoot, originRef).ConfigureAwait(false); + if (fetchedSha is not null) + { + context.BaseCommitSha = fetchedSha; + } + } + else + { + context.BaseBranch = "HEAD"; + _logger?.LogDebug("fetch origin {Branch} 失败,回退到 HEAD: {Error}", defaultBranch, fetchResult.Error); + } + } + } + } + await next(context, ct).ConfigureAwait(false); } } diff --git a/components/07-agents/Agents/src/Agents/Services/Worktree/Pipeline/Middleware/WorktreeRecoveryMiddleware.cs b/components/07-agents/Agents/src/Agents/Services/Worktree/Pipeline/Middleware/WorktreeRecoveryMiddleware.cs index 02b3fe2d..2810784a 100644 --- a/components/07-agents/Agents/src/Agents/Services/Worktree/Pipeline/Middleware/WorktreeRecoveryMiddleware.cs +++ b/components/07-agents/Agents/src/Agents/Services/Worktree/Pipeline/Middleware/WorktreeRecoveryMiddleware.cs @@ -2,6 +2,7 @@ namespace Core.Agents.Worktree; /// /// Worktree 恢复中间件 — 检查现有 worktree 是否可恢复,命中时短路 +/// 对齐 TS readWorktreeHeadSha:直接读取 .git 指针文件获取 HEAD SHA(无子进程,~15ms 优化) /// [Register(typeof(IWorktreeCreateMiddleware))] public sealed partial class WorktreeRecoveryMiddleware : IWorktreeCreateMiddleware @@ -18,10 +19,20 @@ public async Task InvokeAsync(WorktreeCreateContext context, MiddlewareDelegate< var worktreePath = AgentWorktreeSession.GenerateWorktreePath(context.GitRoot, context.AgentId); var branchName = AgentWorktreeSession.GenerateBranchName(context.AgentId); - if (_fs.DirectoryExists(worktreePath) && await _worktreeService.Value.IsValidWorktreeAsync(worktreePath, context.GitRoot).ConfigureAwait(false)) + var existingHeadSha = ReadWorktreeHeadSha(worktreePath); + if (existingHeadSha is not null) { _logger?.LogInformation("恢复现有 worktree: {WorktreePath}, Agent: {AgentId}", worktreePath, context.AgentId); + try + { + _fs.SetDirectoryLastWriteTimeUtc(worktreePath, _clock.GetUtcNow()); + } + catch (Exception ex) + { + _logger?.LogDebug(ex, "更新 worktree mtime 失败: {Path}", worktreePath); + } + var existingSession = new AgentWorktreeSession { AgentId = context.AgentId, @@ -29,6 +40,7 @@ public async Task InvokeAsync(WorktreeCreateContext context, MiddlewareDelegate< WorktreePath = worktreePath, BranchName = branchName, GitRootPath = context.GitRoot, + BaseCommitSha = existingHeadSha, CreatedAt = _clock.GetUtcNow(), Existed = true }; @@ -39,6 +51,7 @@ public async Task InvokeAsync(WorktreeCreateContext context, MiddlewareDelegate< context.RecoveredSession = existingSession; context.WorktreePath = worktreePath; context.BranchName = branchName; + context.BaseCommitSha = existingHeadSha; context.Result = WorktreeCreateResult.SuccessResult(existingSession, true); return; } @@ -48,4 +61,48 @@ public async Task InvokeAsync(WorktreeCreateContext context, MiddlewareDelegate< await next(context, ct).ConfigureAwait(false); } + + private string? ReadWorktreeHeadSha(string worktreePath) + { + try + { + var gitFile = _fs.CombinePath(worktreePath, ".git"); + if (!_fs.FileExists(gitFile)) return null; + + var readResult = _fs.ReadFileAsync(gitFile).GetAwaiter().GetResult(); + if (!readResult.Success) return null; + + var content = readResult.Content.Trim(); + if (!content.StartsWith("gitdir: ", StringComparison.OrdinalIgnoreCase)) return null; + + var gitdirRelative = content["gitdir: ".Length..].Trim(); + var gitdirAbs = _fs.CombinePath(worktreePath, gitdirRelative); + var normalizedGitdir = Path.GetFullPath(gitdirAbs); + + var headFile = _fs.CombinePath(normalizedGitdir, "HEAD"); + if (!_fs.FileExists(headFile)) return null; + + var headReadResult = _fs.ReadFileAsync(headFile).GetAwaiter().GetResult(); + if (!headReadResult.Success) return null; + + var headContent = headReadResult.Content.Trim(); + + if (headContent.StartsWith("ref: ", StringComparison.OrdinalIgnoreCase)) + { + var refPath = headContent["ref: ".Length..].Trim(); + var refFile = _fs.CombinePath(normalizedGitdir, refPath); + if (!_fs.FileExists(refFile)) return null; + + var refReadResult = _fs.ReadFileAsync(refFile).GetAwaiter().GetResult(); + return refReadResult.Success ? refReadResult.Content.Trim() : null; + } + + return headContent.Length == 40 && headContent.All(c => char.IsAsciiHexDigit(c)) ? headContent : null; + } + catch (Exception ex) + { + _logger?.LogDebug(ex, "读取 worktree HEAD SHA 失败: {Path}", worktreePath); + return null; + } + } } diff --git a/components/07-agents/Agents/src/GlobalUsings.cs b/components/07-agents/Agents/src/GlobalUsings.cs index 2f2a76d4..4bb1d02f 100644 --- a/components/07-agents/Agents/src/GlobalUsings.cs +++ b/components/07-agents/Agents/src/GlobalUsings.cs @@ -82,6 +82,7 @@ global using System.Security.Cryptography; global using System.Text; global using System.Text.Json; +global using System.Text.Json.Nodes; global using System.Text.Json.Serialization; global using System.Text.RegularExpressions; global using System.Threading.Channels; diff --git a/sdk/Abstractions/00-core/Models/Agent/AgentWorktreeSession.cs b/sdk/Abstractions/00-core/Models/Agent/AgentWorktreeSession.cs index c11aac9a..f6d816c2 100644 --- a/sdk/Abstractions/00-core/Models/Agent/AgentWorktreeSession.cs +++ b/sdk/Abstractions/00-core/Models/Agent/AgentWorktreeSession.cs @@ -70,8 +70,7 @@ public sealed record AgentWorktreeSession { /// public static string GenerateBranchName(string agentId) { var safeId = agentId.Replace("/", "+").Replace("\\", "+"); - var shortId = safeId.Length > 8 ? safeId[..8] : safeId; - return $"worktree-agent-{shortId}"; + return $"worktree-{safeId}"; } /// @@ -81,6 +80,26 @@ public static string GenerateWorktreePath(string gitRoot, string agentId) { var safeId = agentId.Replace("/", "+").Replace("\\", "+"); return WorkflowConstants.Paths.GetProjectWorktreePath(gitRoot, agentId); } + + /// + /// 解析 PR 引用 — 对齐 TS parsePRReference + /// 支持 #N 格式和 GitHub PR URL(如 https://github.com/owner/repo/pull/123) + /// + public static int? ParsePRReference(string input) { + var urlMatch = Regex.Match(input, @"^https?://[^/]+/[^/]+/[^/]+/pull/(\d+)/?(?:[?#].*)?$", RegexOptions.IgnoreCase); + if (urlMatch.Success && int.TryParse(urlMatch.Groups[1].Value, out var urlPrNum)) + { + return urlPrNum; + } + + var hashMatch = Regex.Match(input, @"^#(\d+)$"); + if (hashMatch.Success && int.TryParse(hashMatch.Groups[1].Value, out var hashPrNum)) + { + return hashPrNum; + } + + return null; + } } /// @@ -92,6 +111,11 @@ public sealed record WorktreeOptions { /// public string? BaseBranch { get; init; } + /// + /// PR 编号(可选)— 对齐 TS parsePRReference,支持 #N 和 GitHub PR URL + /// + public int? PrNumber { get; init; } + /// /// 稀疏检出路径列表(可选) /// @@ -135,12 +159,15 @@ public int StaleTimeoutHours { /// /// 临时 worktree 名称模式(用于自动清理) + /// 对齐 TS EPHEMERAL_WORKTREE_PATTERNS:精确正则匹配,避免误删用户命名的 worktree /// public IReadOnlyList EphemeralPatterns { get; init; } = [ - "agent-*", - "wf-*", - "bridge-*" + "^agent-[a-z0-9]{1,8}$", + "^wf_[0-9a-f]{8}-[0-9a-f]{3}-\\d+$", + "^wf-\\d+$", + "^bridge-[A-Za-z0-9_]+(-[A-Za-z0-9_]+)*$", + "^job-[a-zA-Z0-9._-]{1,55}-[0-9a-f]{8}$" ]; } diff --git a/sdk/Abstractions/05-memory/FileIO/IFileOperationService.cs b/sdk/Abstractions/05-memory/FileIO/IFileOperationService.cs index b72153c4..60197c4b 100644 --- a/sdk/Abstractions/05-memory/FileIO/IFileOperationService.cs +++ b/sdk/Abstractions/05-memory/FileIO/IFileOperationService.cs @@ -99,6 +99,11 @@ Task ListDirectoryAsync( /// DateTime GetDirectoryLastWriteTimeUtc(string directoryPath); + /// + /// 设置目录最后写入时间(UTC)— 对齐 TS utimes,防止过期清理误删恢复的 worktree + /// + void SetDirectoryLastWriteTimeUtc(string directoryPath, DateTime utcTime); + /// /// 获取文件最后写入时间 /// diff --git a/sdk/Abstractions/05-memory/FileIO/IFileSystem.cs b/sdk/Abstractions/05-memory/FileIO/IFileSystem.cs index d886bdf4..a9703210 100644 --- a/sdk/Abstractions/05-memory/FileIO/IFileSystem.cs +++ b/sdk/Abstractions/05-memory/FileIO/IFileSystem.cs @@ -133,6 +133,9 @@ public interface IFileSystem /// 获取目录最后写入时间(UTC) — 对齐 Directory.GetLastWriteTimeUtc DateTime GetDirectoryLastWriteTimeUtc(string path); + /// 设置目录最后写入时间(UTC) — 对齐 Directory.SetLastWriteTimeUtc + void SetDirectoryLastWriteTimeUtc(string path, DateTime utcTime); + /// 获取父目录路径 — 对齐 DirectoryInfo.Parent.FullName,根目录返回 null string? GetParentPath(string path); diff --git a/sdk/Abstractions/07-agents/Agent/Worktree/IAgentWorktreeService.cs b/sdk/Abstractions/07-agents/Agent/Worktree/IAgentWorktreeService.cs index 3575a5f2..5a36bffe 100644 --- a/sdk/Abstractions/07-agents/Agent/Worktree/IAgentWorktreeService.cs +++ b/sdk/Abstractions/07-agents/Agent/Worktree/IAgentWorktreeService.cs @@ -105,4 +105,12 @@ Task HasUnpushedCommitsAsync( Task> ListWorktreesAsync( string? gitRootPath = null, CancellationToken cancellationToken = default); + + /// + /// 保留 Worktree 但清除会话 — 对齐 TS keepWorktree + /// Worktree 目录和分支保留,但会话从内存和持久化中移除 + /// + /// 智能体 ID + /// 取消令牌 + Task KeepWorktreeAsync(string agentId, CancellationToken cancellationToken = default); } diff --git a/src/Infrastructure/IO/FileSystem/InMemoryFileSystem.cs b/src/Infrastructure/IO/FileSystem/InMemoryFileSystem.cs index e0b25974..b4f06412 100644 --- a/src/Infrastructure/IO/FileSystem/InMemoryFileSystem.cs +++ b/src/Infrastructure/IO/FileSystem/InMemoryFileSystem.cs @@ -485,6 +485,9 @@ public void MoveDirectory(string sourceDir, string destDir) public DateTime GetDirectoryLastWriteTimeUtc(string path) => DateTime.UtcNow; + /// + public void SetDirectoryLastWriteTimeUtc(string path, DateTime utcTime) { } + /// public string? GetParentPath(string path) { diff --git a/src/Infrastructure/IO/FileSystem/PhysicalFileSystem.cs b/src/Infrastructure/IO/FileSystem/PhysicalFileSystem.cs index 0a1163ee..f1a5a786 100644 --- a/src/Infrastructure/IO/FileSystem/PhysicalFileSystem.cs +++ b/src/Infrastructure/IO/FileSystem/PhysicalFileSystem.cs @@ -186,6 +186,10 @@ public void MoveDirectory(string sourceDir, string destDir) public DateTime GetDirectoryLastWriteTimeUtc(string path) => Directory.GetLastWriteTimeUtc(path); + /// + public void SetDirectoryLastWriteTimeUtc(string path, DateTime utcTime) + => Directory.SetLastWriteTimeUtc(path, utcTime); + /// public string? GetParentPath(string path) { diff --git a/src/Infrastructure/IO/Services/FileOps/FileOperationService.cs b/src/Infrastructure/IO/Services/FileOps/FileOperationService.cs index 9ab21c16..5d220a91 100644 --- a/src/Infrastructure/IO/Services/FileOps/FileOperationService.cs +++ b/src/Infrastructure/IO/Services/FileOps/FileOperationService.cs @@ -168,6 +168,13 @@ public DateTime GetDirectoryLastWriteTimeUtc(string directoryPath) return _fs.GetDirectoryLastWriteTimeUtc(normalizedPath); } + /// + public void SetDirectoryLastWriteTimeUtc(string directoryPath, DateTime utcTime) + { + var normalizedPath = NormalizePath(directoryPath); + _fs.SetDirectoryLastWriteTimeUtc(normalizedPath, utcTime); + } + /// public DateTime GetFileLastWriteTime(string filePath) { diff --git a/src/Infrastructure/IO/Services/FileOps/ThrottledFileService.cs b/src/Infrastructure/IO/Services/FileOps/ThrottledFileService.cs index 5dfbe923..e9c91be6 100644 --- a/src/Infrastructure/IO/Services/FileOps/ThrottledFileService.cs +++ b/src/Infrastructure/IO/Services/FileOps/ThrottledFileService.cs @@ -485,6 +485,12 @@ public DateTime GetDirectoryLastWriteTimeUtc(string directoryPath) return _fs.GetDirectoryLastWriteTimeUtc(NormalizePath(directoryPath)); } + /// + public void SetDirectoryLastWriteTimeUtc(string directoryPath, DateTime utcTime) + { + _fs.SetDirectoryLastWriteTimeUtc(NormalizePath(directoryPath), utcTime); + } + /// public DateTime GetFileLastWriteTime(string filePath) { diff --git a/tests/Unit/Testing.Common/Services/InMemoryFileOperationService.cs b/tests/Unit/Testing.Common/Services/InMemoryFileOperationService.cs index 148c8042..1ba5ee1b 100644 --- a/tests/Unit/Testing.Common/Services/InMemoryFileOperationService.cs +++ b/tests/Unit/Testing.Common/Services/InMemoryFileOperationService.cs @@ -336,6 +336,8 @@ public DateTime GetDirectoryLastWriteTimeUtc(string directoryPath) return DateTime.UtcNow; } + public void SetDirectoryLastWriteTimeUtc(string directoryPath, DateTime utcTime) { } + public DateTime GetFileLastWriteTime(string filePath) { return _fileSystem.GetLastWriteTime(filePath); diff --git a/tests/Unit/Testing.Common/Services/InMemoryFileSystem.cs b/tests/Unit/Testing.Common/Services/InMemoryFileSystem.cs index 32059575..875a4a93 100644 --- a/tests/Unit/Testing.Common/Services/InMemoryFileSystem.cs +++ b/tests/Unit/Testing.Common/Services/InMemoryFileSystem.cs @@ -518,6 +518,9 @@ public void MoveDirectory(string sourceDir, string destDir) public DateTime GetDirectoryLastWriteTimeUtc(string path) => DateTime.UtcNow; + /// + public void SetDirectoryLastWriteTimeUtc(string path, DateTime utcTime) { } + /// public string? GetParentPath(string path) { From 58b4f33631e237ac3139b189435f1d997b3950bc Mon Sep 17 00:00:00 2001 From: liuqihong <540762622@qq.com> Date: Sun, 19 Jul 2026 06:37:28 +0800 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20=E7=BB=9F=E4=B8=80=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E7=9B=AE=E5=BD=95=E4=B8=BA.jcc=20+=20=E9=A6=96?= =?UTF-8?q?=E6=AC=A1=E5=90=AF=E5=8A=A8=E7=94=9F=E6=88=90=E5=B8=A6=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E9=85=8D=E7=BD=AE=E6=A8=A1=E6=9D=BF=20|=20=E5=86=B3?= =?UTF-8?q?=E7=AD=96:=20AppDataFolder=E9=BB=98=E8=AE=A4=E5=80=BC=E4=BB=8Ej?= =?UTF-8?q?cc=E6=94=B9=E4=B8=BA.jcc=E6=B6=88=E9=99=A4=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E4=B8=8D=E4=B8=80=E8=87=B4,11=E5=A4=84=E7=A1=AC=E7=BC=96?= =?UTF-8?q?=E7=A0=81=E7=BB=9F=E4=B8=80=E7=94=A8AppDataConstants,=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=A8=A1=E6=9D=BF=E7=94=A8StringBuilder=E9=81=BF?= =?UTF-8?q?=E5=85=8D=E5=B5=8C=E5=A5=97=E5=AD=97=E7=AC=A6=E4=B8=B2=E5=8F=AF?= =?UTF-8?q?=E8=AF=BB=E6=80=A7=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/ExtractMemoriesCallback.cs | 2 +- .../Services/SessionMemoryCompactService.cs | 2 +- .../SessionMemoryExtractionService.cs | 2 +- .../src/Shell/Providers/BashShellProvider.cs | 2 +- .../Core/Loading/ConfigJsonContext.cs | 4 +- .../Core/Loading/SettingsLoader.cs | 5 +- .../Unit/Configuration/SettingsLoaderTests.cs | 2 +- .../Bridge/src/Core/BridgePointer.cs | 2 +- .../src/Core/Runtime/ConcurrentSession.cs | 2 +- .../src/Messaging/BridgeInboundAttachments.cs | 2 +- .../src/Security/BridgeDeviceTokenService.cs | 4 +- .../src/Session/BridgeSubprocessManager.cs | 2 +- .../Configuration/AppData/AppDataPaths.cs | 2 +- .../Configuration/Llm/ModelConfigLoader.cs | 2 +- .../IO/Services/FileOps/McpOutputStorage.cs | 2 +- src/Infrastructure/Ssh/SshForwardedPort.cs | 2 +- src/Infrastructure/Ssh/SshSession.cs | 2 +- .../Commands/guard/Config/InitCommand.cs | 10 +++- .../Commands/system/System/CopyCommand.cs | 2 +- src/JoinCode/Entry/StartupWorkflow.cs | 58 ++++++++++++++++++- 20 files changed, 87 insertions(+), 24 deletions(-) diff --git a/components/02-brain/Brain/src/Context/Compact/Services/ExtractMemoriesCallback.cs b/components/02-brain/Brain/src/Context/Compact/Services/ExtractMemoriesCallback.cs index 36914cbb..a665adfc 100644 --- a/components/02-brain/Brain/src/Context/Compact/Services/ExtractMemoriesCallback.cs +++ b/components/02-brain/Brain/src/Context/Compact/Services/ExtractMemoriesCallback.cs @@ -93,7 +93,7 @@ public async Task OnPostSamplingAsync(PostSamplingContext context) private string GetMemoryDirectory() { var cwd = _fileSystem.GetCurrentDirectory(); - return Path.Combine(cwd, ".jcc", "memory"); + return Path.Combine(cwd, AppDataConstants.AppDataFolder, "memory"); } private string FormatMemoryManifest(string memoryDir) diff --git a/components/02-brain/Brain/src/Context/Compact/Services/SessionMemoryCompactService.cs b/components/02-brain/Brain/src/Context/Compact/Services/SessionMemoryCompactService.cs index 3f2a4b14..95fd3b49 100644 --- a/components/02-brain/Brain/src/Context/Compact/Services/SessionMemoryCompactService.cs +++ b/components/02-brain/Brain/src/Context/Compact/Services/SessionMemoryCompactService.cs @@ -4,7 +4,7 @@ namespace Core.Context.Compact; [Register] public sealed partial class SessionMemoryCompactService : ISessionMemoryCompactService { - private const string SessionMemorySubdir = ".jcc"; + private static readonly string SessionMemorySubdir = AppDataConstants.AppDataFolder; private const string SessionMemoryFileName = "session-memory.md"; private readonly SessionMemoryCompactConfig _config; diff --git a/components/02-brain/Brain/src/Context/Compact/Services/SessionMemoryExtractionService.cs b/components/02-brain/Brain/src/Context/Compact/Services/SessionMemoryExtractionService.cs index 114617d4..636884f1 100644 --- a/components/02-brain/Brain/src/Context/Compact/Services/SessionMemoryExtractionService.cs +++ b/components/02-brain/Brain/src/Context/Compact/Services/SessionMemoryExtractionService.cs @@ -75,7 +75,7 @@ public bool ShouldExtract(int currentTokenCount, int toolCallsSinceLastUpdate) public string GetMemoryFilePath() { var cwd = _fileSystem.GetCurrentDirectory(); - return Path.Combine(cwd, ".jcc", "session-memory.md"); + return Path.Combine(cwd, AppDataConstants.AppDataFolder, "session-memory.md"); } /// diff --git a/components/03-hands/Hands/src/Shell/Providers/BashShellProvider.cs b/components/03-hands/Hands/src/Shell/Providers/BashShellProvider.cs index 43de6adc..5d33cfc7 100644 --- a/components/03-hands/Hands/src/Shell/Providers/BashShellProvider.cs +++ b/components/03-hands/Hands/src/Shell/Providers/BashShellProvider.cs @@ -28,7 +28,7 @@ public sealed class BashShellProvider : ShellProviderBase /// private static readonly string SnapshotDir = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".jcc", "shell-snapshots"); + AppDataConstants.AppDataFolder, "shell-snapshots"); public BashShellProvider(IFileSystem fs, string? shellPath = null, ILogger? logger = null) : base(fs, shellPath, logger) diff --git a/components/04-guard/Guard/src/Configuration/Configuration/Core/Loading/ConfigJsonContext.cs b/components/04-guard/Guard/src/Configuration/Configuration/Core/Loading/ConfigJsonContext.cs index 7cc085eb..4b8873a5 100644 --- a/components/04-guard/Guard/src/Configuration/Configuration/Core/Loading/ConfigJsonContext.cs +++ b/components/04-guard/Guard/src/Configuration/Configuration/Core/Loading/ConfigJsonContext.cs @@ -1,7 +1,7 @@ namespace Core.Configuration; -[JsonSourceGenerationOptions(WriteIndented = false)] +[JsonSourceGenerationOptions(WriteIndented = false, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true)] [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(SettingsJson))] @@ -14,7 +14,7 @@ namespace Core.Configuration; [JsonSerializable(typeof(StatusLineSettings))] public partial class ConfigJsonContext : JsonSerializerContext; -[JsonSourceGenerationOptions(WriteIndented = true)] +[JsonSourceGenerationOptions(WriteIndented = true, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true)] [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(SettingsJson))] diff --git a/components/04-guard/Guard/src/Configuration/Configuration/Core/Loading/SettingsLoader.cs b/components/04-guard/Guard/src/Configuration/Configuration/Core/Loading/SettingsLoader.cs index 09a4c118..208ee6a8 100644 --- a/components/04-guard/Guard/src/Configuration/Configuration/Core/Loading/SettingsLoader.cs +++ b/components/04-guard/Guard/src/Configuration/Configuration/Core/Loading/SettingsLoader.cs @@ -155,11 +155,10 @@ public static string GetUserSettingsPath() /// /// 获取项目共享设置路径: {projectDir}/.jcc/settings.json - /// 项目设置始终使用 .jcc 相对目录名,不受 AppDataFolder 绝对路径覆盖影响 + /// 项目级目录始终使用相对目录名,当 AppDataFolder 为绝对路径(测试隔离)时回退到 .jcc /// public static string GetProjectSettingsPath(string projectDir) { - // 项目设置固定使用 .jcc 目录名,不跟随 AppDataFolder 覆盖 var folderName = Path.IsPathRooted(AppDataConstants.AppDataFolder) ? ".jcc" : AppDataConstants.AppDataFolder; return Path.Combine(projectDir, folderName, "settings.json"); } @@ -182,7 +181,7 @@ public static string GetManagedSettingsPath() { return Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), - "jcc", + AppDataConstants.AppDataFolder, "managed-settings.json"); } diff --git a/components/04-guard/Guard/tests/Unit/Configuration/SettingsLoaderTests.cs b/components/04-guard/Guard/tests/Unit/Configuration/SettingsLoaderTests.cs index 2aaa5a8d..4d6831e0 100644 --- a/components/04-guard/Guard/tests/Unit/Configuration/SettingsLoaderTests.cs +++ b/components/04-guard/Guard/tests/Unit/Configuration/SettingsLoaderTests.cs @@ -26,7 +26,7 @@ public SettingsLoaderTests() _fs.CreateDirectory(_tempDir); // 项目设置目录: _tempDir/.jcc/ - _projectAppDataDir = Path.Combine(_tempDir, ".jcc"); + _projectAppDataDir = Path.Combine(_tempDir, AppDataConstants.AppDataFolder); _fs.CreateDirectory(_projectAppDataDir); // 用户设置隔离目录(绝对路径) diff --git a/components/08-transport/Bridge/src/Core/BridgePointer.cs b/components/08-transport/Bridge/src/Core/BridgePointer.cs index f6c891f4..fdfdc638 100644 --- a/components/08-transport/Bridge/src/Core/BridgePointer.cs +++ b/components/08-transport/Bridge/src/Core/BridgePointer.cs @@ -72,7 +72,7 @@ public static string GetPointerPath(string dir) { ArgumentNullException.ThrowIfNull(dir); var appData = Environment.GetEnvironmentVariable("JCC_APP_DATA_FOLDER") - ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "jcc"); + ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), AppDataConstants.AppDataFolder); var safeDir = dir.Replace(Path.DirectorySeparatorChar, '_').Replace(Path.AltDirectorySeparatorChar, '_'); var projectsDir = Path.Combine(appData, "projects", safeDir); return Path.Combine(projectsDir, "bridge-pointer.json"); diff --git a/components/08-transport/Bridge/src/Core/Runtime/ConcurrentSession.cs b/components/08-transport/Bridge/src/Core/Runtime/ConcurrentSession.cs index 3d6da3c2..b3b7a70e 100644 --- a/components/08-transport/Bridge/src/Core/Runtime/ConcurrentSession.cs +++ b/components/08-transport/Bridge/src/Core/Runtime/ConcurrentSession.cs @@ -112,7 +112,7 @@ public ConcurrentSessionService(IFileSystem fs, ILogger? logger = null, IClockSe public static string GetSessionsDir() { var appData = Environment.GetEnvironmentVariable("JCC_APP_DATA_FOLDER") - ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "jcc"); + ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), AppDataConstants.AppDataFolder); return Path.Combine(appData, "sessions"); } diff --git a/components/08-transport/Bridge/src/Messaging/BridgeInboundAttachments.cs b/components/08-transport/Bridge/src/Messaging/BridgeInboundAttachments.cs index 9f1fd4a4..4b4de4d1 100644 --- a/components/08-transport/Bridge/src/Messaging/BridgeInboundAttachments.cs +++ b/components/08-transport/Bridge/src/Messaging/BridgeInboundAttachments.cs @@ -70,7 +70,7 @@ public static async Task ResolveInboundAttachmentsAsync( var uploadDir = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "jcc", "uploads", sessionId); + AppDataConstants.AppDataFolder, "uploads", sessionId); fs.CreateDirectory(uploadDir); diff --git a/components/08-transport/Bridge/src/Security/BridgeDeviceTokenService.cs b/components/08-transport/Bridge/src/Security/BridgeDeviceTokenService.cs index c729f13a..cbce234f 100644 --- a/components/08-transport/Bridge/src/Security/BridgeDeviceTokenService.cs +++ b/components/08-transport/Bridge/src/Security/BridgeDeviceTokenService.cs @@ -33,8 +33,8 @@ internal BridgeDeviceTokenService(HttpClient httpClient, IFileSystem fs, ILogger _logger = logger; _authFilePath = authFilePath ?? Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "jcc", "auth.json"); + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + AppDataConstants.AppDataFolder, "auth.json"); } /// diff --git a/components/08-transport/Bridge/src/Session/BridgeSubprocessManager.cs b/components/08-transport/Bridge/src/Session/BridgeSubprocessManager.cs index 238f2b13..0df3fa3a 100644 --- a/components/08-transport/Bridge/src/Session/BridgeSubprocessManager.cs +++ b/components/08-transport/Bridge/src/Session/BridgeSubprocessManager.cs @@ -550,7 +550,7 @@ public async Task SpawnAsync(BridgeSubprocessOptions opt else if (options.Verbose || IsAntBuild()) { var tempDir = Path.GetTempPath(); - debugFile = Path.Combine(tempDir, "jcc", $"bridge-session-{safeId}.log"); + debugFile = Path.Combine(tempDir, AppDataConstants.AppDataFolder, $"bridge-session-{safeId}.log"); } // 构建带 transcript 的路径 diff --git a/sdk/Abstractions/00-core/Configuration/AppData/AppDataPaths.cs b/sdk/Abstractions/00-core/Configuration/AppData/AppDataPaths.cs index c3d5bf87..26151960 100644 --- a/sdk/Abstractions/00-core/Configuration/AppData/AppDataPaths.cs +++ b/sdk/Abstractions/00-core/Configuration/AppData/AppDataPaths.cs @@ -39,7 +39,7 @@ public sealed record AppDataPaths( public static AppDataPaths FromEnvironment() { return new AppDataPaths( - AppDataFolder: ResolveEnv(JccEnvVar.AppDataFolder, "jcc"), + AppDataFolder: ResolveEnv(JccEnvVar.AppDataFolder, ".jcc"), CredentialsFileName: ResolveEnv(JccEnvVar.CredentialsFileName, "credentials.json"), AuthFileName: ResolveEnv(JccEnvVar.AuthFileName, "auth.json"), SettingsFileName: ResolveEnv(JccEnvVar.SettingsFileName, "settings.json"), diff --git a/sdk/Abstractions/00-core/Configuration/Llm/ModelConfigLoader.cs b/sdk/Abstractions/00-core/Configuration/Llm/ModelConfigLoader.cs index a6fa67c9..eb221744 100644 --- a/sdk/Abstractions/00-core/Configuration/Llm/ModelConfigLoader.cs +++ b/sdk/Abstractions/00-core/Configuration/Llm/ModelConfigLoader.cs @@ -189,7 +189,7 @@ private static void ApplyUserOverride(ModelConfigRoot config) { var userConfigPath = System.IO.Path.Combine( System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile), - ".jcc", "models.json"); + AppData.AppDataConstants.AppDataFolder, "models.json"); if (!System.IO.File.Exists(userConfigPath)) return; diff --git a/src/Infrastructure/IO/Services/FileOps/McpOutputStorage.cs b/src/Infrastructure/IO/Services/FileOps/McpOutputStorage.cs index af7c4cdb..138ef82e 100644 --- a/src/Infrastructure/IO/Services/FileOps/McpOutputStorage.cs +++ b/src/Infrastructure/IO/Services/FileOps/McpOutputStorage.cs @@ -19,7 +19,7 @@ public McpOutputStorage(IFileSystem fs, ILogger? logger = null _logger = logger; _baseDir = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "jcc", + AppDataConstants.AppDataFolder, "mcp-output"); } diff --git a/src/Infrastructure/Ssh/SshForwardedPort.cs b/src/Infrastructure/Ssh/SshForwardedPort.cs index d00fbd2c..587c67ae 100644 --- a/src/Infrastructure/Ssh/SshForwardedPort.cs +++ b/src/Infrastructure/Ssh/SshForwardedPort.cs @@ -58,7 +58,7 @@ public Task StartAsync(CancellationToken ct = default) { var keyFile = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "jcc", "ssh", $"key_{_sessionId}"); + AppDataConstants.AppDataFolder, "ssh", $"key_{_sessionId}"); args.Append($" -i \"{keyFile}\""); } diff --git a/src/Infrastructure/Ssh/SshSession.cs b/src/Infrastructure/Ssh/SshSession.cs index 56d7ba3e..254926b7 100644 --- a/src/Infrastructure/Ssh/SshSession.cs +++ b/src/Infrastructure/Ssh/SshSession.cs @@ -390,7 +390,7 @@ private void AddAuthArgs(ProcessStartInfo startInfo) case SshAuthMethod.PrivateKey when Config.PrivateKey != null: var keyFile = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "jcc", "ssh", $"key_{SessionId}"); + AppDataConstants.AppDataFolder, "ssh", $"key_{SessionId}"); _fs.CreateDirectory(Path.GetDirectoryName(keyFile)!); _fs.WriteAllText(keyFile, Config.PrivateKey); diff --git a/src/JoinCode/Commands/guard/Config/InitCommand.cs b/src/JoinCode/Commands/guard/Config/InitCommand.cs index d243210b..bc245efe 100644 --- a/src/JoinCode/Commands/guard/Config/InitCommand.cs +++ b/src/JoinCode/Commands/guard/Config/InitCommand.cs @@ -79,7 +79,15 @@ private static async Task QuickInitAsync(ChatCommandContext context) if (!fs.FileExists(settingsFile)) { var defaultModel = JoinCode.Abstractions.Configuration.Llm.ModelConfigLoader.GetDefaultModelId("deepseek"); - await fs.WriteAllTextAsync(settingsFile, $"{{\n \"provider\": \"deepseek\",\n \"model\": \"{defaultModel}\"\n}}\n", context.CancellationToken).ConfigureAwait(false); + var sb = new StringBuilder(); + sb.AppendLine("{"); + sb.AppendLine(" // 项目级 Provider(覆盖全局配置)"); + sb.AppendLine(" \"provider\": \"deepseek\","); + sb.AppendLine(); + sb.AppendLine(" // 项目级模型 ID"); + sb.AppendLine($" \"model\": \"{defaultModel}\""); + sb.Append('}'); + await fs.WriteAllTextAsync(settingsFile, sb.ToString(), context.CancellationToken).ConfigureAwait(false); TerminalHelper.WriteLine(" ✓ 创建 settings.json"); } else diff --git a/src/JoinCode/Commands/system/System/CopyCommand.cs b/src/JoinCode/Commands/system/System/CopyCommand.cs index 00519095..0b5902d4 100644 --- a/src/JoinCode/Commands/system/System/CopyCommand.cs +++ b/src/JoinCode/Commands/system/System/CopyCommand.cs @@ -82,7 +82,7 @@ private static async Task CopyWithFallbackAsync( // 对齐 TS: 同时写入临时文件作为回退(OSC52需要终端支持) try { - var tempDir = Path.Combine(Path.GetTempPath(), "jcc"); + var tempDir = Path.Combine(Path.GetTempPath(), AppDataConstants.AppDataFolder); DirectoryHelper.EnsureDirectoryExists(fs, tempDir); var filePath = Path.Combine(tempDir, filename); await fs.WriteAllTextAsync(filePath, text, ct).ConfigureAwait(false); diff --git a/src/JoinCode/Entry/StartupWorkflow.cs b/src/JoinCode/Entry/StartupWorkflow.cs index 61010ce2..36cc7419 100644 --- a/src/JoinCode/Entry/StartupWorkflow.cs +++ b/src/JoinCode/Entry/StartupWorkflow.cs @@ -87,6 +87,7 @@ internal static async Task RunOnboardingIfNeededAsync(IOnboardingService onboard var appDataPath = WorkflowConstants.Paths.JccDirectory; var settingsPath = Path.Combine(appDataPath, AppDataConstants.SettingsFileName); + var authPath = Path.Combine(appDataPath, AppDataConstants.AuthFileName); if (!fs.DirectoryExists(appDataPath)) { @@ -95,7 +96,12 @@ internal static async Task RunOnboardingIfNeededAsync(IOnboardingService onboard if (!fs.FileExists(settingsPath)) { - await fs.WriteAllTextAsync(settingsPath, "{}").ConfigureAwait(false); + await fs.WriteAllTextAsync(settingsPath, BuildDefaultSettingsTemplate()).ConfigureAwait(false); + } + + if (!fs.FileExists(authPath)) + { + await fs.WriteAllTextAsync(authPath, BuildDefaultAuthTemplate()).ConfigureAwait(false); } } @@ -244,4 +250,54 @@ internal static async Task CheckWorkspaceTrustAsync(CommandLineOptions opt return false; } + + private static string BuildDefaultSettingsTemplate() + { + var defaultModel = JoinCode.Abstractions.Configuration.Llm.ModelConfigLoader.GetDefaultModelId("deepseek"); + var sb = new StringBuilder(); + sb.AppendLine("{"); + sb.AppendLine(" // LLM Provider: deepseek | openai | anthropic | azure | agnes"); + sb.AppendLine(" \"provider\": \"deepseek\","); + sb.AppendLine(); + sb.AppendLine(" // 模型 ID,可用 /model 命令切换"); + sb.AppendLine($" \"model\": \"{defaultModel}\","); + sb.AppendLine(); + sb.AppendLine(" // API 端点(默认使用 Provider 内置地址,无需修改)"); + sb.AppendLine(" // \"endpoint\": \"https://api.deepseek.com\","); + sb.AppendLine(); + sb.AppendLine(" // 推理努力级别: low | medium | high"); + sb.AppendLine(" // \"effortLevel\": \"high\","); + sb.AppendLine(); + sb.AppendLine(" // 快速模式:使用轻量模型处理简单任务"); + sb.AppendLine(" // \"fastMode\": false,"); + sb.AppendLine(); + sb.AppendLine(" // 默认 Shell: powershell | bash | cmd"); + sb.AppendLine(" // \"defaultShell\": \"powershell\","); + sb.AppendLine(); + sb.AppendLine(" // 语言: zh-CN | en-US"); + sb.AppendLine(" // \"language\": \"zh-CN\","); + sb.AppendLine(); + sb.AppendLine(" // 自动记忆:自动将重要信息保存到记忆文件"); + sb.AppendLine(" // \"autoMemoryEnabled\": true"); + sb.Append('}'); + return sb.ToString(); + } + + private static string BuildDefaultAuthTemplate() + { + var sb = new StringBuilder(); + sb.AppendLine("{"); + sb.AppendLine(" // API Key 存储 — 键名为 Provider 名称,值为对应的 API Key"); + sb.AppendLine(" // 支持的键名: deepseek | openai | anthropic | azure | agnes"); + sb.AppendLine(" //"); + sb.AppendLine(" // 示例:"); + sb.AppendLine(" // \"deepseek\": \"sk-your-deepseek-api-key\","); + sb.AppendLine(" // \"openai\": \"sk-your-openai-api-key\","); + sb.AppendLine(" // \"anthropic\": \"sk-ant-your-anthropic-api-key\""); + sb.AppendLine(" //"); + sb.AppendLine(" // 也可通过环境变量设置(优先级更高):"); + sb.AppendLine(" // DEEPSEEK_API_KEY / OPENAI_API_KEY / ANTHROPIC_API_KEY / AZURE_OPENAI_API_KEY"); + sb.Append('}'); + return sb.ToString(); + } } From 03bdcf45cd39034bcd9ac4f592ea2e45dad086dc Mon Sep 17 00:00:00 2001 From: liuqihong <540762622@qq.com> Date: Sun, 19 Jul 2026 06:41:40 +0800 Subject: [PATCH 4/8] =?UTF-8?q?test:=20=E6=B7=BB=E5=8A=A0=20worktree=20?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=E6=B5=8B=E8=AF=95=20=E2=80=94=20PR=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E3=80=81=E5=88=86=E6=94=AF=E5=90=8D=E3=80=81=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E3=80=81Ephemeral=E6=A8=A1=E5=BC=8F=E3=80=81KeepWorkt?= =?UTF-8?q?ree?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Agents/Worktree/WorktreeAlignmentTests.cs | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 components/07-agents/Agents/tests/Unit/Agents/Worktree/WorktreeAlignmentTests.cs diff --git a/components/07-agents/Agents/tests/Unit/Agents/Worktree/WorktreeAlignmentTests.cs b/components/07-agents/Agents/tests/Unit/Agents/Worktree/WorktreeAlignmentTests.cs new file mode 100644 index 00000000..792dd76a --- /dev/null +++ b/components/07-agents/Agents/tests/Unit/Agents/Worktree/WorktreeAlignmentTests.cs @@ -0,0 +1,134 @@ +namespace JoinCode.Agents.Tests.Worktree; + +public class WorktreeAlignmentTests +{ + [Fact] + public void ParsePRReference_ShouldParseHashFormat() + { + var result = AgentWorktreeSession.ParsePRReference("#123"); + result.Should().Be(123); + } + + [Fact] + public void ParsePRReference_ShouldParseGitHubUrl() + { + var result = AgentWorktreeSession.ParsePRReference("https://github.com/owner/repo/pull/456"); + result.Should().Be(456); + } + + [Fact] + public void ParsePRReference_ShouldParseGitHubUrlWithTrailingSlash() + { + var result = AgentWorktreeSession.ParsePRReference("https://github.com/owner/repo/pull/789/"); + result.Should().Be(789); + } + + [Fact] + public void ParsePRReference_ShouldReturnNullForNonPR() + { + AgentWorktreeSession.ParsePRReference("feature-branch").Should().BeNull(); + AgentWorktreeSession.ParsePRReference("").Should().BeNull(); + AgentWorktreeSession.ParsePRReference("123").Should().BeNull(); + } + + [Fact] + public void GenerateBranchName_ShouldFlattenSlashes() + { + var result = AgentWorktreeSession.GenerateBranchName("user/feature"); + result.Should().Be("worktree-user+feature"); + } + + [Fact] + public void GenerateBranchName_ShouldNotTruncate() + { + var longId = new string('a', 64); + var result = AgentWorktreeSession.GenerateBranchName(longId); + result.Should().Be($"worktree-{longId}"); + } + + [Fact] + public void GenerateBranchName_ShouldFlattenBackslashes() + { + var result = AgentWorktreeSession.GenerateBranchName("user\\feature"); + result.Should().Be("worktree-user+feature"); + } + + [Fact] + public void GenerateWorktreePath_ShouldUseJccWorktreesDir() + { + var result = AgentWorktreeSession.GenerateWorktreePath("/home/user/project", "my-agent"); + var normalized = result.Replace('\\', '/'); + normalized.Should().Be("/home/user/project/.jcc/worktrees/my-agent"); + } + + [Fact] + public void GenerateWorktreePath_ShouldFlattenSlashes() + { + var result = AgentWorktreeSession.GenerateWorktreePath("/home/user/project", "user/feature"); + var normalized = result.Replace('\\', '/'); + normalized.Should().Be("/home/user/project/.jcc/worktrees/user+feature"); + } + + [Theory] + [InlineData("agent-a1b2c3d", true)] + [InlineData("agent-a1b2c3d4", true)] + [InlineData("wf_a1b2c3d4-e1f-5", true)] + [InlineData("wf-1", true)] + [InlineData("bridge-session1-part2", true)] + [InlineData("job-mytemplate-a1b2c3d4", true)] + [InlineData("my-feature", false)] + [InlineData("work-main", false)] + [InlineData("wf-myfeature", false)] + public void EphemeralPatterns_ShouldMatchCorrectly(string dirName, bool expectedMatch) + { + var opts = new WorktreeOptions(); + var isEphemeral = opts.EphemeralPatterns.Any(pattern => + { + try { return System.Text.RegularExpressions.Regex.IsMatch(dirName, pattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase); } + catch { return false; } + }); + isEphemeral.Should().Be(expectedMatch); + } + + [Fact] + public void WorktreeOptions_PrNumber_ShouldDefaultToNull() + { + var opts = new WorktreeOptions(); + opts.PrNumber.Should().BeNull(); + } + + [Fact] + public void WorktreeOptions_PrNumber_ShouldBeSettable() + { + var opts = new WorktreeOptions { PrNumber = 42 }; + opts.PrNumber.Should().Be(42); + } + + [Fact] + public async Task KeepWorktreeAsync_ShouldRemoveSessionButKeepDirectory() + { + var fs = new InMemoryFileOperationService(); + var processService = new Mock(); + var service = new AgentWorktreeService(fs, processService.Object); + + var session = new AgentWorktreeSession + { + AgentId = "test-agent", + OriginalCwd = "/home/user/project", + WorktreePath = "/home/user/project/.jcc/worktrees/test-agent", + BranchName = "worktree-test-agent", + GitRootPath = "/home/user/project", + CreatedAt = DateTime.UtcNow, + Existed = false + }; + + await service.SaveSessionAsync(session); + var savedSession = await service.GetSessionAsync("test-agent"); + savedSession.Should().NotBeNull(); + + await service.KeepWorktreeAsync("test-agent"); + + var afterKeep = await service.GetSessionAsync("test-agent"); + afterKeep.Should().BeNull(); + } +} From 3f58a014adab1a367a8dea15950ca1081f55423d Mon Sep 17 00:00:00 2001 From: liuqihong <540762622@qq.com> Date: Sun, 19 Jul 2026 06:44:23 +0800 Subject: [PATCH 5/8] 1 --- .../00-core/Configuration/AppData/AppDataPaths.cs | 4 ++-- .../00-core/Configuration/AppData/WorkflowConstants.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/Abstractions/00-core/Configuration/AppData/AppDataPaths.cs b/sdk/Abstractions/00-core/Configuration/AppData/AppDataPaths.cs index 26151960..239174fb 100644 --- a/sdk/Abstractions/00-core/Configuration/AppData/AppDataPaths.cs +++ b/sdk/Abstractions/00-core/Configuration/AppData/AppDataPaths.cs @@ -81,7 +81,7 @@ public static AppDataPaths CreateForTest( } /// - /// 获取 .jcc 目录的完整路径 + /// 获取 .jcc 目录的完整路径 — 统一使用 UserProfile(~/.jcc/) /// public string JccDirectory { @@ -91,7 +91,7 @@ public string JccDirectory return AppDataFolder; return Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataFolder); } } diff --git a/sdk/Abstractions/00-core/Configuration/AppData/WorkflowConstants.cs b/sdk/Abstractions/00-core/Configuration/AppData/WorkflowConstants.cs index 47e13e6d..624cf1df 100644 --- a/sdk/Abstractions/00-core/Configuration/AppData/WorkflowConstants.cs +++ b/sdk/Abstractions/00-core/Configuration/AppData/WorkflowConstants.cs @@ -152,7 +152,7 @@ public static class Paths public const int DefaultBridgePort = 3456; /// - /// 获取 .jcc 目录的完整路径 + /// 获取 .jcc 目录的完整路径 — 统一使用 UserProfile(~/.jcc/) /// 优先使用 JCC_APP_DATA_FOLDER 环境变量覆盖(测试隔离场景) /// 其次检查 AppDataConstants.AppDataFolder 是否为绝对路径(backing field 覆盖场景) /// @@ -169,7 +169,7 @@ public static string JccDirectory return appDataFolder; return Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), appDataFolder); } } From 7a11c682020038f8957c97c721bd4fc2f9ead2e9 Mon Sep 17 00:00:00 2001 From: liuqihong <540762622@qq.com> Date: Sun, 19 Jul 2026 06:49:48 +0800 Subject: [PATCH 6/8] =?UTF-8?q?feat:=20=E5=85=A8=E5=B1=80=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E7=9B=AE=E5=BD=95=E7=BB=9F=E4=B8=80=E5=88=B0UserProfi?= =?UTF-8?q?le/.jcc=20|=20=E5=86=B3=E7=AD=96:=20=E4=BB=8EAppData/Roaming?= =?UTF-8?q?=E6=94=B9=E4=B8=BAUserProfile,=E4=B8=8ELinux/macOS=E9=A3=8E?= =?UTF-8?q?=E6=A0=BC=E4=B8=80=E8=87=B4(~/.jcc/),26=E5=A4=84SpecialFolder.A?= =?UTF-8?q?pplicationData=E7=BB=9F=E4=B8=80=E6=9B=BF=E6=8D=A2=E4=B8=BAUser?= =?UTF-8?q?Profile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Skills/Discovery/SkillDiscoveryOptions.cs | 2 +- .../src/Skills/Services/Skill/SkillService.cs | 4 ++-- .../Configuration/Core/Loading/ConfigLoader.cs | 14 +++++++------- .../Configuration/Core/Loading/SettingsLoader.cs | 2 +- .../Configuration/Rules/CustomCommandLoader.cs | 2 +- .../Configuration/Rules/ExternalRulesLoader.cs | 2 +- .../Configuration/Rules/ProjectRulesLoader.cs | 6 +++--- .../Configuration/Services/ConfigurationService.cs | 6 +++--- .../src/DependencyInjection/ServiceRegistration.cs | 2 +- .../Vault/src/Memdir/Memdir/Core/MemoryPaths.cs | 6 +++--- .../tests/Unit/State/DatabasePathResolverTests.cs | 2 +- .../Coordinator/Team/TeammateMailboxService.cs | 4 ++-- .../Services/Support/AgentDefinitionProvider.cs | 2 +- .../Agents/Services/Support/AgentMemoryService.cs | 4 ++-- .../08-transport/Bridge/src/Core/BridgePointer.cs | 4 ++-- .../Bridge/src/Core/Runtime/ConcurrentSession.cs | 4 ++-- .../src/Messaging/BridgeInboundAttachments.cs | 2 +- .../src/Security/BridgeDeviceTokenService.cs | 2 +- .../IO/Services/FileOps/FileHistoryService.cs | 2 +- .../IO/Services/FileOps/McpOutputStorage.cs | 2 +- src/Infrastructure/Ssh/SshForwardedPort.cs | 2 +- src/Infrastructure/Ssh/SshSession.cs | 2 +- .../Cli/Onboarding/OnboardingStatePersistence.cs | 2 +- src/JoinCode/Cli/TrustFolderManager.cs | 4 ++-- .../Commands/guard/Config/ResetConfigCommand.cs | 4 ++-- .../Commands/system/Social/ShareCommand.cs | 4 ++-- 26 files changed, 46 insertions(+), 46 deletions(-) diff --git a/components/03-hands/Hands/src/Skills/Discovery/SkillDiscoveryOptions.cs b/components/03-hands/Hands/src/Skills/Discovery/SkillDiscoveryOptions.cs index cbf36b6e..2ddb3026 100644 --- a/components/03-hands/Hands/src/Skills/Discovery/SkillDiscoveryOptions.cs +++ b/components/03-hands/Hands/src/Skills/Discovery/SkillDiscoveryOptions.cs @@ -6,7 +6,7 @@ namespace Core.Skills.Discovery; public sealed partial class SkillDiscoveryOptions { public string SkillsDirectory { get; init; } = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataConstants.AppDataFolder, "skills"); diff --git a/components/03-hands/Hands/src/Skills/Services/Skill/SkillService.cs b/components/03-hands/Hands/src/Skills/Services/Skill/SkillService.cs index ad6a0fa4..377d9615 100644 --- a/components/03-hands/Hands/src/Skills/Services/Skill/SkillService.cs +++ b/components/03-hands/Hands/src/Skills/Services/Skill/SkillService.cs @@ -5,7 +5,7 @@ namespace Core.Skills; [Register] public sealed record SkillOptions { - public string SkillsDirectory { get; init; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), AppDataConstants.AppDataFolder, "skills"); + public string SkillsDirectory { get; init; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataConstants.AppDataFolder, "skills"); public TimeSpan CacheExpiration { get; init; } = TimeSpan.FromMinutes(5); public SkillOptions() { } @@ -14,7 +14,7 @@ public SkillOptions(WorkflowConfig? config) { SkillsDirectory = config is not null && !string.IsNullOrEmpty(config.SkillsDirectory) ? config.SkillsDirectory - : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), AppDataConstants.AppDataFolder, "skills"); + : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataConstants.AppDataFolder, "skills"); } public static SkillOptions FromConfig(WorkflowConfig? config) => new(config); diff --git a/components/04-guard/Guard/src/Configuration/Configuration/Core/Loading/ConfigLoader.cs b/components/04-guard/Guard/src/Configuration/Configuration/Core/Loading/ConfigLoader.cs index 5f878a7e..54e80863 100644 --- a/components/04-guard/Guard/src/Configuration/Configuration/Core/Loading/ConfigLoader.cs +++ b/components/04-guard/Guard/src/Configuration/Configuration/Core/Loading/ConfigLoader.cs @@ -113,7 +113,7 @@ public async Task LoadConfigAsync(IFileSystem fs, CancellationTo public static async Task LoadSettingsJsonAsync(IFileSystem fs, CancellationToken cancellationToken = default) { var settingsPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataConstants.AppDataFolder, AppDataConstants.SettingsFileName); @@ -137,7 +137,7 @@ public async Task LoadConfigAsync(IFileSystem fs, CancellationTo public static async Task SaveSettingsJsonAsync(SettingsJson settings, IFileSystem fs, CancellationToken cancellationToken = default) { var settingsPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataConstants.AppDataFolder, AppDataConstants.SettingsFileName); @@ -248,7 +248,7 @@ public static async Task SaveApiKeyToJccAsync(string provider, string apiKey, IF public static async Task LoadSettingFromSettingsJsonAsync(string key, IFileSystem fs, CancellationToken cancellationToken = default) { var settingsPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataConstants.AppDataFolder, AppDataConstants.SettingsFileName); @@ -276,7 +276,7 @@ public static async Task SaveApiKeyToJccAsync(string provider, string apiKey, IF public static string? LoadSettingFromSettingsJson(string key, IFileSystem fs) { var settingsPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataConstants.AppDataFolder, AppDataConstants.SettingsFileName); @@ -325,7 +325,7 @@ public static async Task SaveApiKeyToJccAsync(string provider, string apiKey, IF public static async Task SaveSettingToSettingsJsonAsync(string key, string? value, IFileSystem fs, CancellationToken cancellationToken = default) { var settingsPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataConstants.AppDataFolder, AppDataConstants.SettingsFileName); @@ -379,7 +379,7 @@ public static async Task SaveSettingToSettingsJsonAsync(string key, string? valu public static async Task LoadSettingFromGlobalConfigAsync(string key, IFileSystem fs, CancellationToken cancellationToken = default) { var globalPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataConstants.AppDataFolder, AppDataConstants.GlobalConfigFileName); @@ -417,7 +417,7 @@ public static async Task SaveSettingToSettingsJsonAsync(string key, string? valu public static async Task SaveSettingToGlobalConfigAsync(string key, string? value, IFileSystem fs, CancellationToken cancellationToken = default) { var globalPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataConstants.AppDataFolder, AppDataConstants.GlobalConfigFileName); diff --git a/components/04-guard/Guard/src/Configuration/Configuration/Core/Loading/SettingsLoader.cs b/components/04-guard/Guard/src/Configuration/Configuration/Core/Loading/SettingsLoader.cs index 208ee6a8..1688e707 100644 --- a/components/04-guard/Guard/src/Configuration/Configuration/Core/Loading/SettingsLoader.cs +++ b/components/04-guard/Guard/src/Configuration/Configuration/Core/Loading/SettingsLoader.cs @@ -148,7 +148,7 @@ public static string GetUserSettingsPath() return Path.Combine(appDataFolder, settingsFileName); return Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), appDataFolder, settingsFileName); } diff --git a/components/04-guard/Guard/src/Configuration/Configuration/Rules/CustomCommandLoader.cs b/components/04-guard/Guard/src/Configuration/Configuration/Rules/CustomCommandLoader.cs index 69555938..400fba73 100644 --- a/components/04-guard/Guard/src/Configuration/Configuration/Rules/CustomCommandLoader.cs +++ b/components/04-guard/Guard/src/Configuration/Configuration/Rules/CustomCommandLoader.cs @@ -48,7 +48,7 @@ public async Task> LoadProjectCommandsAsync(string workingDi public async Task> LoadUserCommandsAsync(CancellationToken cancellationToken = default) { - var appDataRoot = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + var appDataRoot = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); // 并行扫描所有用户命令目录 diff --git a/components/04-guard/Guard/src/Configuration/Configuration/Rules/ExternalRulesLoader.cs b/components/04-guard/Guard/src/Configuration/Configuration/Rules/ExternalRulesLoader.cs index b3e0e5f0..cde145f3 100644 --- a/components/04-guard/Guard/src/Configuration/Configuration/Rules/ExternalRulesLoader.cs +++ b/components/04-guard/Guard/src/Configuration/Configuration/Rules/ExternalRulesLoader.cs @@ -48,7 +48,7 @@ public async Task> LoadProjectRulesAsync(string workingDirectory, currentDirPath = _fs.GetParentPath(currentDirPath); } - var appDataRoot = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + var appDataRoot = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); // 并行扫描所有用户规则目录 var userDirTasks = new List>>(); foreach (var rulesDir in UserRulesDirs) diff --git a/components/04-guard/Guard/src/Configuration/Configuration/Rules/ProjectRulesLoader.cs b/components/04-guard/Guard/src/Configuration/Configuration/Rules/ProjectRulesLoader.cs index ea40f026..e391afc2 100644 --- a/components/04-guard/Guard/src/Configuration/Configuration/Rules/ProjectRulesLoader.cs +++ b/components/04-guard/Guard/src/Configuration/Configuration/Rules/ProjectRulesLoader.cs @@ -91,7 +91,7 @@ public ProjectRulesLoader( currentDirPath = _fs.GetParentPath(currentDirPath); } - var appDataRoot = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + var appDataRoot = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); // 并行读取用户规则文件 var userReadTasks = new List>(); @@ -227,7 +227,7 @@ public bool HasRulesFile(string? workingDirectory = null) { currentDirPath = _fs.GetParentPath(currentDirPath); } - var appDataRoot = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + var appDataRoot = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); foreach (var relativePath in UserRulesFilePaths) { var fullPath = Path.Combine(appDataRoot, relativePath); if (_fs.FileExists(fullPath)) { @@ -270,7 +270,7 @@ public bool HasRulesFile(string? workingDirectory = null) { currentDirPath = _fs.GetParentPath(currentDirPath); } - var appDataRoot = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + var appDataRoot = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); foreach (var relativePath in UserRulesFilePaths) { var fullPath = Path.Combine(appDataRoot, relativePath); if (_fs.FileExists(fullPath)) { diff --git a/components/04-guard/Guard/src/Configuration/Services/ConfigurationService.cs b/components/04-guard/Guard/src/Configuration/Services/ConfigurationService.cs index c9bdb5ef..ab47da46 100644 --- a/components/04-guard/Guard/src/Configuration/Services/ConfigurationService.cs +++ b/components/04-guard/Guard/src/Configuration/Services/ConfigurationService.cs @@ -87,8 +87,8 @@ public async Task SetAsync(string key, string value, SettingSource source, { // 对齐 TS markInternalWrite — 写入前标记内部写,防止 FileSystemWatcher 回声 var targetPath = source == SettingSource.GlobalConfig - ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), AppDataConstants.AppDataFolder, AppDataConstants.GlobalConfigFileName) - : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), AppDataConstants.AppDataFolder, AppDataConstants.SettingsFileName); + ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataConstants.AppDataFolder, AppDataConstants.GlobalConfigFileName) + : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataConstants.AppDataFolder, AppDataConstants.SettingsFileName); _configChangeNotifier?.MarkInternalWrite(targetPath); if (source == SettingSource.GlobalConfig) @@ -120,7 +120,7 @@ public async Task RemoveAsync(string key, CancellationToken cancellationTo try { // 对齐 TS markInternalWrite — 写入前标记内部写 - var targetPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), AppDataConstants.AppDataFolder, AppDataConstants.SettingsFileName); + var targetPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataConstants.AppDataFolder, AppDataConstants.SettingsFileName); _configChangeNotifier?.MarkInternalWrite(targetPath); await ConfigLoader.SaveSettingToSettingsJsonAsync(key, null, _fs, cancellationToken).ConfigureAwait(false); diff --git a/components/04-guard/Guard/src/DependencyInjection/ServiceRegistration.cs b/components/04-guard/Guard/src/DependencyInjection/ServiceRegistration.cs index ea8eb472..bbc946dd 100644 --- a/components/04-guard/Guard/src/DependencyInjection/ServiceRegistration.cs +++ b/components/04-guard/Guard/src/DependencyInjection/ServiceRegistration.cs @@ -59,7 +59,7 @@ public static IServiceCollection AddHookSystem(this IServiceCollection services) var manager = new HookConfigurationManager(fs, sp.GetService>()); var logger = sp.GetService>(); - var appDataRoot = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + var appDataRoot = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); var userSettingsPath = Path.Combine(appDataRoot, AppDataConstants.AppDataFolder, AppDataConstants.SettingsFileName); manager.RegisterProvider(HookSource.UserSettings, new JsonFileHookConfigurationProvider(userSettingsPath, HookSource.UserSettings, fs, logger)); diff --git a/components/05-memory/Vault/src/Memdir/Memdir/Core/MemoryPaths.cs b/components/05-memory/Vault/src/Memdir/Memdir/Core/MemoryPaths.cs index b14d98d1..ff7b64c3 100644 --- a/components/05-memory/Vault/src/Memdir/Memdir/Core/MemoryPaths.cs +++ b/components/05-memory/Vault/src/Memdir/Memdir/Core/MemoryPaths.cs @@ -1,4 +1,4 @@ - + namespace Core.Memdir; /// @@ -101,7 +101,7 @@ public string GetMemoryFilePath(string memoryId, MemoryType type, string? contex private static string GetDefaultBaseDirectory() { return Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataConstants.AppDataFolder, "memories"); } @@ -165,7 +165,7 @@ public string GetTeamMemberDirectory(string teamId, string userId) private static string GetDefaultTeamBaseDirectory() { return Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataConstants.AppDataFolder, "team-memories"); } diff --git a/components/05-memory/Vault/tests/Unit/State/DatabasePathResolverTests.cs b/components/05-memory/Vault/tests/Unit/State/DatabasePathResolverTests.cs index dc574d04..6576f695 100644 --- a/components/05-memory/Vault/tests/Unit/State/DatabasePathResolverTests.cs +++ b/components/05-memory/Vault/tests/Unit/State/DatabasePathResolverTests.cs @@ -121,7 +121,7 @@ public void Resolve_RelativeAppDataFolder_RootsToApplicationData() // Assert — 应该在 %APPDATA%/TestJcc 下 var expectedBase = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "TestJcc"); Assert.Equal(Path.Combine(expectedBase, "workflow_state.db"), result); Assert.DoesNotContain(AppContext.BaseDirectory, result); diff --git a/components/07-agents/Agents/src/Agents/Agents/Coordinator/Team/TeammateMailboxService.cs b/components/07-agents/Agents/src/Agents/Agents/Coordinator/Team/TeammateMailboxService.cs index cd6ee4c7..7b7c27ff 100644 --- a/components/07-agents/Agents/src/Agents/Agents/Coordinator/Team/TeammateMailboxService.cs +++ b/components/07-agents/Agents/src/Agents/Agents/Coordinator/Team/TeammateMailboxService.cs @@ -1,4 +1,4 @@ -namespace Core.Agents.Coordinator; +namespace Core.Agents.Coordinator; [Register] public sealed partial class TeammateMailboxService : ITeammateMailboxService, IDisposable @@ -21,7 +21,7 @@ public TeammateMailboxService( _fs = fs ?? throw new ArgumentNullException(nameof(fs)); _mailboxRoot = mailboxRoot ?? Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataConstants.AppDataFolder, AppDataConstants.MailboxFolderName); _logger = logger; diff --git a/components/07-agents/Agents/src/Agents/Services/Support/AgentDefinitionProvider.cs b/components/07-agents/Agents/src/Agents/Services/Support/AgentDefinitionProvider.cs index 0d85122f..0e1625c2 100644 --- a/components/07-agents/Agents/src/Agents/Services/Support/AgentDefinitionProvider.cs +++ b/components/07-agents/Agents/src/Agents/Services/Support/AgentDefinitionProvider.cs @@ -140,7 +140,7 @@ public void ClearCache() CancellationToken cancellationToken) { var definitions = new List(); - var appDataRoot = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + var appDataRoot = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); var agentsPath = Path.Combine(appDataRoot, AppDataConstants.AppDataFolder, AppDataConstants.AgentsFolderName); if (_fs.DirectoryExists(agentsPath)) diff --git a/components/07-agents/Agents/src/Agents/Services/Support/AgentMemoryService.cs b/components/07-agents/Agents/src/Agents/Services/Support/AgentMemoryService.cs index 49aa9cda..2e4f882b 100644 --- a/components/07-agents/Agents/src/Agents/Services/Support/AgentMemoryService.cs +++ b/components/07-agents/Agents/src/Agents/Services/Support/AgentMemoryService.cs @@ -1,4 +1,4 @@ - + namespace Core.Agents; /// @@ -35,7 +35,7 @@ public AgentMemoryService(ILogger logger, IFileSystem fs, st _logger = logger; _fs = fs ?? throw new ArgumentNullException(nameof(fs)); _memoryBase = memoryBase ?? Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataConstants.AppDataFolder); _cwd = cwd ?? fs.GetCurrentDirectory(); } diff --git a/components/08-transport/Bridge/src/Core/BridgePointer.cs b/components/08-transport/Bridge/src/Core/BridgePointer.cs index fdfdc638..fb9b775c 100644 --- a/components/08-transport/Bridge/src/Core/BridgePointer.cs +++ b/components/08-transport/Bridge/src/Core/BridgePointer.cs @@ -1,4 +1,4 @@ - + namespace Core.Bridge; #region BridgePointerSource 枚举 @@ -72,7 +72,7 @@ public static string GetPointerPath(string dir) { ArgumentNullException.ThrowIfNull(dir); var appData = Environment.GetEnvironmentVariable("JCC_APP_DATA_FOLDER") - ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), AppDataConstants.AppDataFolder); + ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataConstants.AppDataFolder); var safeDir = dir.Replace(Path.DirectorySeparatorChar, '_').Replace(Path.AltDirectorySeparatorChar, '_'); var projectsDir = Path.Combine(appData, "projects", safeDir); return Path.Combine(projectsDir, "bridge-pointer.json"); diff --git a/components/08-transport/Bridge/src/Core/Runtime/ConcurrentSession.cs b/components/08-transport/Bridge/src/Core/Runtime/ConcurrentSession.cs index b3b7a70e..091218d7 100644 --- a/components/08-transport/Bridge/src/Core/Runtime/ConcurrentSession.cs +++ b/components/08-transport/Bridge/src/Core/Runtime/ConcurrentSession.cs @@ -1,4 +1,4 @@ - + namespace Core.Bridge; #region SessionKind 枚举 @@ -112,7 +112,7 @@ public ConcurrentSessionService(IFileSystem fs, ILogger? logger = null, IClockSe public static string GetSessionsDir() { var appData = Environment.GetEnvironmentVariable("JCC_APP_DATA_FOLDER") - ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), AppDataConstants.AppDataFolder); + ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataConstants.AppDataFolder); return Path.Combine(appData, "sessions"); } diff --git a/components/08-transport/Bridge/src/Messaging/BridgeInboundAttachments.cs b/components/08-transport/Bridge/src/Messaging/BridgeInboundAttachments.cs index 4b4de4d1..a0197d7c 100644 --- a/components/08-transport/Bridge/src/Messaging/BridgeInboundAttachments.cs +++ b/components/08-transport/Bridge/src/Messaging/BridgeInboundAttachments.cs @@ -69,7 +69,7 @@ public static async Task ResolveInboundAttachmentsAsync( if (attachments.Count == 0) return string.Empty; var uploadDir = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataConstants.AppDataFolder, "uploads", sessionId); fs.CreateDirectory(uploadDir); diff --git a/components/08-transport/Bridge/src/Security/BridgeDeviceTokenService.cs b/components/08-transport/Bridge/src/Security/BridgeDeviceTokenService.cs index cbce234f..85983fee 100644 --- a/components/08-transport/Bridge/src/Security/BridgeDeviceTokenService.cs +++ b/components/08-transport/Bridge/src/Security/BridgeDeviceTokenService.cs @@ -33,7 +33,7 @@ internal BridgeDeviceTokenService(HttpClient httpClient, IFileSystem fs, ILogger _logger = logger; _authFilePath = authFilePath ?? Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataConstants.AppDataFolder, "auth.json"); } diff --git a/src/Infrastructure/IO/Services/FileOps/FileHistoryService.cs b/src/Infrastructure/IO/Services/FileOps/FileHistoryService.cs index e54aa5f7..2c56b376 100644 --- a/src/Infrastructure/IO/Services/FileOps/FileHistoryService.cs +++ b/src/Infrastructure/IO/Services/FileOps/FileHistoryService.cs @@ -22,7 +22,7 @@ public FileHistoryService(IFileSystem fs, ILogger? logger = _logger = logger; _sessionId = Environment.ProcessId.ToString(); - var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); _baseDir = Path.Combine(homeDir, AppDataConstants.AppDataFolder, AppDataConstants.FileHistoryFolderName); } diff --git a/src/Infrastructure/IO/Services/FileOps/McpOutputStorage.cs b/src/Infrastructure/IO/Services/FileOps/McpOutputStorage.cs index 138ef82e..1f06e0ad 100644 --- a/src/Infrastructure/IO/Services/FileOps/McpOutputStorage.cs +++ b/src/Infrastructure/IO/Services/FileOps/McpOutputStorage.cs @@ -18,7 +18,7 @@ public McpOutputStorage(IFileSystem fs, ILogger? logger = null _fs = fs; _logger = logger; _baseDir = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataConstants.AppDataFolder, "mcp-output"); } diff --git a/src/Infrastructure/Ssh/SshForwardedPort.cs b/src/Infrastructure/Ssh/SshForwardedPort.cs index 587c67ae..5c57f0e8 100644 --- a/src/Infrastructure/Ssh/SshForwardedPort.cs +++ b/src/Infrastructure/Ssh/SshForwardedPort.cs @@ -57,7 +57,7 @@ public Task StartAsync(CancellationToken ct = default) if (_config.AuthMethod == SshAuthMethod.PrivateKey && _config.PrivateKey != null) { var keyFile = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataConstants.AppDataFolder, "ssh", $"key_{_sessionId}"); args.Append($" -i \"{keyFile}\""); } diff --git a/src/Infrastructure/Ssh/SshSession.cs b/src/Infrastructure/Ssh/SshSession.cs index 254926b7..14bbc966 100644 --- a/src/Infrastructure/Ssh/SshSession.cs +++ b/src/Infrastructure/Ssh/SshSession.cs @@ -389,7 +389,7 @@ private void AddAuthArgs(ProcessStartInfo startInfo) { case SshAuthMethod.PrivateKey when Config.PrivateKey != null: var keyFile = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataConstants.AppDataFolder, "ssh", $"key_{SessionId}"); _fs.CreateDirectory(Path.GetDirectoryName(keyFile)!); diff --git a/src/JoinCode/Cli/Onboarding/OnboardingStatePersistence.cs b/src/JoinCode/Cli/Onboarding/OnboardingStatePersistence.cs index 744d604c..92f8f730 100644 --- a/src/JoinCode/Cli/Onboarding/OnboardingStatePersistence.cs +++ b/src/JoinCode/Cli/Onboarding/OnboardingStatePersistence.cs @@ -15,7 +15,7 @@ public OnboardingStatePersistence(IFileSystem fs, IClockService? clock = null) _fs = fs; _clock = clock ?? SystemClockService.Instance; var appDataPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataConstants.AppDataFolder); _filePath = Path.Combine(appDataPath, "onboarding_complete.json"); } diff --git a/src/JoinCode/Cli/TrustFolderManager.cs b/src/JoinCode/Cli/TrustFolderManager.cs index 6db11486..fa4d95ad 100644 --- a/src/JoinCode/Cli/TrustFolderManager.cs +++ b/src/JoinCode/Cli/TrustFolderManager.cs @@ -1,4 +1,4 @@ -namespace JoinCode.Cli; +namespace JoinCode.Cli; /// /// 信任目录管理器 — CLI 简化版,实现 ITrustFolderManager @@ -13,7 +13,7 @@ public TrustFolderManager(IFileSystem fs) { _fs = fs; var appDataPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataConstants.AppDataFolder); _trustedFoldersPath = Path.Combine(appDataPath, AppDataConstants.TrustedFoldersFileName); } diff --git a/src/JoinCode/Commands/guard/Config/ResetConfigCommand.cs b/src/JoinCode/Commands/guard/Config/ResetConfigCommand.cs index ee0a2cb6..4fd7b111 100644 --- a/src/JoinCode/Commands/guard/Config/ResetConfigCommand.cs +++ b/src/JoinCode/Commands/guard/Config/ResetConfigCommand.cs @@ -1,4 +1,4 @@ - + namespace JoinCode.ChatCommands; [ChatCommand(Name = ChatCommandNameConstants.ResetConfig, Description = "重置配置文件到默认状态", Usage = "/reset-config [all|auth|settings|trust|onboarding]", Category = ChatCommandCategory.Config, ArgumentHint = "[all|auth|settings|trust|onboarding]")] @@ -8,7 +8,7 @@ public async override Task ExecuteAsync(ChatCommandContext co { var args = ChatCommandBase.GetNormalizedArgs(context).ToLowerInvariant(); var fs = context.Services.FileSystem; - var appDataRoot = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + var appDataRoot = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); var jccDir = Path.Combine(appDataRoot, AppDataConstants.AppDataFolder); if (args == "all" || string.IsNullOrEmpty(args)) diff --git a/src/JoinCode/Commands/system/Social/ShareCommand.cs b/src/JoinCode/Commands/system/Social/ShareCommand.cs index 50b836a4..4ed7140d 100644 --- a/src/JoinCode/Commands/system/Social/ShareCommand.cs +++ b/src/JoinCode/Commands/system/Social/ShareCommand.cs @@ -1,4 +1,4 @@ -namespace JoinCode.ChatCommands; +namespace JoinCode.ChatCommands; /// /// /share 命令 — 对齐 TS share/ @@ -53,7 +53,7 @@ public async override Task ExecuteAsync(ChatCommandContext co try { var sharePath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppDataConstants.AppDataFolder, "shares", $"share-{DateTime.Now:yyyyMMdd-HHmmss}.md"); From 7c6705ef0297c5ef2c8a9fa8000a80ff1be628fe Mon Sep 17 00:00:00 2001 From: liuqihong <540762622@qq.com> Date: Sun, 19 Jul 2026 06:59:51 +0800 Subject: [PATCH 7/8] =?UTF-8?q?feat:=20=E9=A6=96=E6=AC=A1=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E8=87=AA=E5=8A=A8=E5=88=9B=E5=BB=BA=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=96=87=E4=BB=B6(=E4=B8=8D=E4=BE=9D=E8=B5=96Onboarding)=20|?= =?UTF-8?q?=20=E5=86=B3=E7=AD=96:=20EnsureConfigFilesExistAsync=E5=9C=A8Lo?= =?UTF-8?q?adConfigAsync=E4=B9=8B=E5=89=8D=E8=B0=83=E7=94=A8,=E7=A1=AE?= =?UTF-8?q?=E4=BF=9D=E9=9D=9E=E4=BA=A4=E4=BA=92=E6=A8=A1=E5=BC=8F=E4=B9=9F?= =?UTF-8?q?=E8=83=BD=E7=94=9F=E6=88=90=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/JoinCode/Entry/StartupWorkflow.cs | 45 ++++++++++++++++----------- src/JoinCode/Program.cs | 4 +++ 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/JoinCode/Entry/StartupWorkflow.cs b/src/JoinCode/Entry/StartupWorkflow.cs index 36cc7419..9b1a67a7 100644 --- a/src/JoinCode/Entry/StartupWorkflow.cs +++ b/src/JoinCode/Entry/StartupWorkflow.cs @@ -2,6 +2,32 @@ namespace JoinCode.Entry; internal static class StartupWorkflow { + /// + /// 确保全局配置文件存在 — 首次启动时自动创建带注释的模板,不覆盖已有文件 + /// 必须在 LoadConfigAsync 之前调用,确保配置文件可供加载 + /// + internal static async Task EnsureConfigFilesExistAsync(IFileSystem fs) + { + var appDataPath = WorkflowConstants.Paths.JccDirectory; + var settingsPath = Path.Combine(appDataPath, AppDataConstants.SettingsFileName); + var authPath = Path.Combine(appDataPath, AppDataConstants.AuthFileName); + + if (!fs.DirectoryExists(appDataPath)) + { + DirectoryHelper.EnsureDirectoryExists(fs, appDataPath); + } + + if (!fs.FileExists(settingsPath)) + { + await fs.WriteAllTextAsync(settingsPath, BuildDefaultSettingsTemplate()).ConfigureAwait(false); + } + + if (!fs.FileExists(authPath)) + { + await fs.WriteAllTextAsync(authPath, BuildDefaultAuthTemplate()).ConfigureAwait(false); + } + } + internal static async Task RunOnboardingIfNeededAsync(IOnboardingService onboardingService, CommandLineOptions options, IFileSystem fs, bool hasApiKey, IProviderDefinitionRegistry registry, WorkflowConfig? config = null) { await onboardingService.InitializeAsync(); @@ -84,25 +110,6 @@ internal static async Task RunOnboardingIfNeededAsync(IOnboardingService onboard } } } - - var appDataPath = WorkflowConstants.Paths.JccDirectory; - var settingsPath = Path.Combine(appDataPath, AppDataConstants.SettingsFileName); - var authPath = Path.Combine(appDataPath, AppDataConstants.AuthFileName); - - if (!fs.DirectoryExists(appDataPath)) - { - DirectoryHelper.EnsureDirectoryExists(fs, appDataPath); - } - - if (!fs.FileExists(settingsPath)) - { - await fs.WriteAllTextAsync(settingsPath, BuildDefaultSettingsTemplate()).ConfigureAwait(false); - } - - if (!fs.FileExists(authPath)) - { - await fs.WriteAllTextAsync(authPath, BuildDefaultAuthTemplate()).ConfigureAwait(false); - } } /// diff --git a/src/JoinCode/Program.cs b/src/JoinCode/Program.cs index f974b4cd..0f883152 100644 --- a/src/JoinCode/Program.cs +++ b/src/JoinCode/Program.cs @@ -57,6 +57,10 @@ static async Task Main(string[] args) using var awaitTimer = StartAwaitTimer(options); var fs = IO.FileSystem.FileSystemFactory.Create(); + + // 首次启动时确保全局配置文件存在(带注释模板) + await Entry.StartupWorkflow.EnsureConfigFilesExistAsync(fs); + var config = await App.Builder.ApplicationBuilder.LoadConfigAsync(options, fs); var builder = new App.Builder.ApplicationBuilder() From 0ffd4e19ce87481a6b67a7910e398e8b14296019 Mon Sep 17 00:00:00 2001 From: liuqihong <540762622@qq.com> Date: Sun, 19 Jul 2026 21:20:38 +0800 Subject: [PATCH 8/8] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DCI=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E6=96=AD=E8=A8=80=E9=80=82=E9=85=8D=E9=BB=98=E8=AE=A4?= =?UTF-8?q?Provider=E6=94=B9=E4=B8=BADeepSeek=20|=20InitCommandTests?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=E8=BF=87=E6=97=B6=E7=9A=84legacyJccDir?= =?UTF-8?q?=E6=96=AD=E8=A8=80,ServiceRegistrationIntegrationTests=E4=BD=BF?= =?UTF-8?q?=E7=94=A8DeepSeek=E9=BB=98=E8=AE=A4=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ServiceRegistrationIntegrationTests.cs | 4 ++-- .../Host.Tests/ChatCommands/InitCommandTests.cs | 17 +++-------------- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/tests/MockServers/Sync.Integration.Tests/ServiceRegistrationIntegrationTests.cs b/tests/MockServers/Sync.Integration.Tests/ServiceRegistrationIntegrationTests.cs index 5408deb8..312896bd 100644 --- a/tests/MockServers/Sync.Integration.Tests/ServiceRegistrationIntegrationTests.cs +++ b/tests/MockServers/Sync.Integration.Tests/ServiceRegistrationIntegrationTests.cs @@ -3,7 +3,7 @@ namespace Tests; [Trait("Category", "Integration")] public class ServiceRegistrationIntegrationTests { - private static readonly string DefaultOpenAiModelId = ModelConfigLoader.GetDefaultModelId("openai"); + private static readonly string DefaultModelId = ModelConfigLoader.GetDefaultModelId("deepseek"); [Fact] public void AddWorkflowServices_ShouldRegisterITranscriptService() @@ -42,7 +42,7 @@ public void FastModeService_ShouldUsePrimaryModelIdFromConfig() var sp = services.BuildServiceProvider(); var fastModeService = sp.GetRequiredService(); - Assert.Equal(DefaultOpenAiModelId, fastModeService.PrimaryModelId); + Assert.Equal(DefaultModelId, fastModeService.PrimaryModelId); } [Fact] diff --git a/tests/Unit/Host.Tests/ChatCommands/InitCommandTests.cs b/tests/Unit/Host.Tests/ChatCommands/InitCommandTests.cs index 5294a738..cf8121a3 100644 --- a/tests/Unit/Host.Tests/ChatCommands/InitCommandTests.cs +++ b/tests/Unit/Host.Tests/ChatCommands/InitCommandTests.cs @@ -4,9 +4,7 @@ namespace Host.Tests.ChatCommands; /// /// InitCommand 单元测试 — 覆盖 /init quick 路径 -/// 验证目标:bug 修复 — 使用 AppDataConstants.AppDataFolder 而非硬编码 ".jcc" -/// 修复前:QuickInitAsync 硬编码 Path.Combine(cwd, ".jcc"),与 EnsureJccDirectory 使用 AppDataConstants.AppDataFolder (默认 "jcc") 路径不一致 -/// 修复后:统一使用 AppDataConstants.AppDataFolder,确保目录创建与文件写入路径一致 +/// 验证目标:统一使用 AppDataConstants.AppDataFolder 创建目录和写入文件 /// public sealed class InitCommandTests { @@ -41,14 +39,11 @@ public void IsHidden_Should_Be_False() [Fact] public async Task Execute_WithQuick_Should_Create_JccDirectory_Using_AppDataConstants() { - // Arrange — bug 复现:修复前硬编码 ".jcc",导致目录创建在 "jcc" 但文件写入 ".jcc" - // 修复后应统一使用 AppDataConstants.AppDataFolder (默认 "jcc") var fs = new InMemoryFileSystem(); var cwd = "/test/init-unit"; fs.SetCurrentDirectory(cwd); var expectedJccDir = Path.Combine(cwd, AppDataConstants.AppDataFolder); - var legacyJccDir = Path.Combine(cwd, ".jcc"); var cmd = new InitCommand(); var context = BuildContext("quick", fs); @@ -60,15 +55,12 @@ public async Task Execute_WithQuick_Should_Create_JccDirectory_Using_AppDataCons result.ShouldContinue.Should().BeTrue(); result.IsHandled.Should().BeTrue(); fs.DirectoryExists(expectedJccDir).Should().BeTrue( - $"应使用 AppDataConstants.AppDataFolder ('{AppDataConstants.AppDataFolder}') 创建目录,而非硬编码 '.jcc'"); - fs.DirectoryExists(legacyJccDir).Should().BeFalse( - "不应创建硬编码的 '.jcc' 目录 (bug 修复前会同时存在两个目录)"); + $"应使用 AppDataConstants.AppDataFolder ('{AppDataConstants.AppDataFolder}') 创建目录"); } [Fact] public async Task Execute_WithQuick_Should_Write_Files_To_AppDataConstants_Path() { - // Arrange — 验证文件写入路径与目录创建路径一致 var fs = new InMemoryFileSystem(); var cwd = "/test/init-files"; fs.SetCurrentDirectory(cwd); @@ -76,7 +68,6 @@ public async Task Execute_WithQuick_Should_Write_Files_To_AppDataConstants_Path( var expectedJccDir = Path.Combine(cwd, AppDataConstants.AppDataFolder); var expectedRulesFile = Path.Combine(expectedJccDir, "project_rules.md"); var expectedSettingsFile = Path.Combine(expectedJccDir, "settings.json"); - var legacyRulesFile = Path.Combine(cwd, ".jcc", "project_rules.md"); var cmd = new InitCommand(); var context = BuildContext("quick", fs); @@ -89,14 +80,12 @@ public async Task Execute_WithQuick_Should_Write_Files_To_AppDataConstants_Path( "project_rules.md 应写入 AppDataConstants.AppDataFolder 路径"); fs.FileExists(expectedSettingsFile).Should().BeTrue( "settings.json 应写入 AppDataConstants.AppDataFolder 路径"); - fs.FileExists(legacyRulesFile).Should().BeFalse( - "不应写入硬编码 '.jcc' 路径 (bug 修复前文件写入失败,因为目录创建在 'jcc' 而非 '.jcc')"); // 验证文件内容非空 var rulesContent = fs.ReadAllText(expectedRulesFile); rulesContent.Should().Contain("项目规则"); var settingsContent = fs.ReadAllText(expectedSettingsFile); - settingsContent.Should().Contain("openai"); + settingsContent.Should().Contain("deepseek"); } [Fact]