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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 75 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

/// <inheritdoc />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public sealed class BashShellProvider : ShellProviderBase
/// </summary>
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() { }
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

namespace Core.Configuration;

[JsonSourceGenerationOptions(WriteIndented = false)]
[JsonSourceGenerationOptions(WriteIndented = false, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true)]
[JsonSerializable(typeof(Dictionary<string, string>))]
[JsonSerializable(typeof(Dictionary<string, JsonElement>))]
[JsonSerializable(typeof(SettingsJson))]
Expand All @@ -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<string, string>))]
[JsonSerializable(typeof(Dictionary<string, JsonElement>))]
[JsonSerializable(typeof(SettingsJson))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public async Task<WorkflowConfig> LoadConfigAsync(IFileSystem fs, CancellationTo
public static async Task<SettingsJson?> LoadSettingsJsonAsync(IFileSystem fs, CancellationToken cancellationToken = default)
{
var settingsPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
AppDataConstants.AppDataFolder,
AppDataConstants.SettingsFileName);

Expand All @@ -137,7 +137,7 @@ public async Task<WorkflowConfig> 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);

Expand Down Expand Up @@ -248,7 +248,7 @@ public static async Task SaveApiKeyToJccAsync(string provider, string apiKey, IF
public static async Task<string?> 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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -379,7 +379,7 @@ public static async Task SaveSettingToSettingsJsonAsync(string key, string? valu
public static async Task<string?> 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);

Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,18 +148,17 @@ public static string GetUserSettingsPath()
return Path.Combine(appDataFolder, settingsFileName);

return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
appDataFolder,
settingsFileName);
}

/// <summary>
/// 获取项目共享设置路径: {projectDir}/.jcc/settings.json
/// 项目设置始终使用 .jcc 相对目录名,不受 AppDataFolder 绝对路径覆盖影响
/// 项目级目录始终使用相对目录名,当 AppDataFolder 为绝对路径(测试隔离)时回退到 .jcc
/// </summary>
public static string GetProjectSettingsPath(string projectDir)
{
// 项目设置固定使用 .jcc 目录名,不跟随 AppDataFolder 覆盖
var folderName = Path.IsPathRooted(AppDataConstants.AppDataFolder) ? ".jcc" : AppDataConstants.AppDataFolder;
return Path.Combine(projectDir, folderName, "settings.json");
}
Expand All @@ -182,7 +181,7 @@ public static string GetManagedSettingsPath()
{
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
"jcc",
AppDataConstants.AppDataFolder,
"managed-settings.json");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,13 @@ public SettingsJson() { }
[SettingsProperty(SettingsMergeStrategy.Override, SkipKeyAccess = true)]
public WorktreeSettings? Worktree { get; init; }

/// <summary>
/// 活跃 Worktree 会话 — 对齐 TS 版 activeWorktreeSession,持久化到 settings.local.json
/// </summary>
[JsonPropertyName("activeWorktreeSession")]
[SettingsProperty(SettingsMergeStrategy.Override, SkipKeyAccess = true)]
public ActiveWorktreeSessionJson? ActiveWorktreeSession { get; init; }

/// <summary>
/// 状态栏配置 — 对齐 TS 版 statusLine
/// </summary>
Expand Down Expand Up @@ -407,6 +414,42 @@ public sealed class WorktreeSettings
public List<string>? SparsePaths { get; init; }
}

/// <summary>
/// 活跃 Worktree 会话 — 对齐 TS 版 activeWorktreeSession,持久化到 settings.local.json
/// </summary>
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; }
}

/// <summary>
/// 状态栏配置 — 对齐 TS 版 statusLine
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public async Task<List<CustomCommand>> LoadProjectCommandsAsync(string workingDi

public async Task<List<CustomCommand>> 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);

// 并行扫描所有用户命令目录
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public async Task<List<RuleFile>> 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<Task<List<RuleFile>>>();
foreach (var rulesDir in UserRulesDirs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Task<(string Path, string? Content)?>>();
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ public async Task<bool> 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)
Expand Down Expand Up @@ -120,7 +120,7 @@ public async Task<bool> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public static IServiceCollection AddHookSystem(this IServiceCollection services)
var manager = new HookConfigurationManager(fs, sp.GetService<ILogger<HookConfigurationManager>>());
var logger = sp.GetService<ILogger<JsonFileHookConfigurationProvider>>();

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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

// 用户设置隔离目录(绝对路径)
Expand Down
Loading
Loading