cc-haha/PLAN.md
程序员阿江(Relakkes) da2be1f81b Merge branch 'worktree-feature-scheduled-tasks' into docs/ui-clone-requirements
Resolve PLAN.md conflict by keeping both plans: Provider management
system and Scheduled Tasks enhancement.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 01:48:03 +08:00

20 KiB
Raw Blame History

模型配置重构计划 — Provider 管理系统

背景

当前项目的模型配置存在以下问题:

  • 模型列表硬编码在 src/server/api/models.tsAVAILABLE_MODELS 数组中
  • 不支持自定义 Provider供应商
  • 不支持自定义 Base URL 和 API Key
  • 无法测试模型连通性
  • 无法管理多个 Provider 并在它们之间切换

设计原则

  1. 非侵入性 — 不修改 Claude Code 原生 settings.json 的 schema通过 env 字段注入环境变量
  2. 简洁 — 不过度设计只实现核心功能Provider CRUD、激活切换、连通性测试
  3. 兼容 — 借鉴 cc-switch 的激活机制,通过写入 settings.jsonenv 字段实现 Provider 切换

核心机制

Claude Code 的 settings.json 支持 env 字段,会在启动时注入到 process.env

{
  "env": {
    "ANTHROPIC_BASE_URL": "https://api.example.com",
    "ANTHROPIC_AUTH_TOKEN": "sk-xxx"
  },
  "model": "claude-opus-4-6"
}

激活 Provider = 将其 baseUrl/apiKey/model 写入 settings.json 的 env 字段


数据模型

Provider 类型定义

// src/server/types/provider.ts

interface ProviderModel {
  id: string           // 模型 ID如 "claude-opus-4-6"
  name: string         // 显示名称,如 "Opus 4.6"
  description?: string // 简短描述
  context?: string     // 上下文窗口,如 "200k"
}

interface Provider {
  id: string           // UUID
  name: string         // 显示名称,如 "Anthropic 官方"、"OpenRouter"
  baseUrl: string      // API Base URL
  apiKey: string       // API Key
  models: ProviderModel[]  // 该 Provider 支持的模型列表
  isActive: boolean    // 是否为当前激活的 Provider
  createdAt: number    // 创建时间戳
  updatedAt: number    // 更新时间戳
  notes?: string       // 备注
}

存储格式

文件路径:~/.claude/providers.json

{
  "providers": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "Anthropic 官方",
      "baseUrl": "https://api.anthropic.com",
      "apiKey": "sk-ant-xxx",
      "models": [
        { "id": "claude-opus-4-6", "name": "Opus 4.6", "description": "Most capable", "context": "200k" },
        { "id": "claude-sonnet-4-6", "name": "Sonnet 4.6", "description": "Most efficient", "context": "200k" },
        { "id": "claude-haiku-4-5", "name": "Haiku 4.5", "description": "Fastest", "context": "200k" }
      ],
      "isActive": true,
      "createdAt": 1712476800000,
      "updatedAt": 1712476800000
    },
    {
      "id": "660e8400-e29b-41d4-a716-446655440001",
      "name": "OpenRouter",
      "baseUrl": "https://openrouter.ai/api/v1",
      "apiKey": "sk-or-xxx",
      "models": [
        { "id": "anthropic/claude-opus-4-6", "name": "Claude Opus 4.6", "context": "200k" }
      ],
      "isActive": false,
      "createdAt": 1712476800000,
      "updatedAt": 1712476800000
    }
  ],
  "activeModel": "claude-opus-4-6",
  "version": 1
}

实现步骤

Step 1: Provider 服务层 (src/server/services/providerService.ts)

创建 ProviderService 类,负责:

  • listProviders() — 读取 ~/.claude/providers.json 并返回 provider 列表
  • getProvider(id) — 获取单个 provider
  • getActiveProvider() — 获取当前激活的 provider
  • addProvider(data) — 添加新 provider自动生成 UUID
  • updateProvider(id, data) — 更新 provider 信息
  • deleteProvider(id) — 删除 provider不允许删除激活中的 provider
  • activateProvider(id, modelId) — 激活 provider 并选择模型
    • 将旧 provider 设为 isActive: false
    • 将新 provider 设为 isActive: true
    • 写入 ~/.claude/settings.jsonenv 字段:
      {
        "env": {
          "ANTHROPIC_BASE_URL": "<provider.baseUrl>",
          "ANTHROPIC_AUTH_TOKEN": "<provider.apiKey>"
        },
        "model": "<modelId>"
      }
      
  • testProvider(id) / testProviderConfig(baseUrl, apiKey, modelId) — 连通性测试
    • baseUrl/v1/messages 发送一个最小请求max_tokens=1, "Hi"
    • 返回 { success, latencyMs, error?, modelUsed? }

Step 2: Provider REST API (src/server/api/providers.ts)

方法 路径 描述
GET /api/providers 获取 provider 列表
GET /api/providers/:id 获取单个 provider
POST /api/providers 添加 provider
PUT /api/providers/:id 更新 provider
DELETE /api/providers/:id 删除 provider
POST /api/providers/:id/activate 激活 provider 并选择模型
POST /api/providers/:id/test 测试已保存 provider 的连通性
POST /api/providers/test 测试未保存配置的连通性(用于添加时预检)

Step 3: 注册路由 (src/server/router.ts)

在 router 中添加 providers 路由:

case 'providers':
  return handleProvidersApi(req, url, segments)

Step 4: 重构 Models API (src/server/api/models.ts)

修改现有的 /api/models 端点:

  • GET /api/models — 不再返回硬编码列表,而是从当前激活的 Provider 读取模型列表
  • GET /api/models/current — 从 providers.json 的 activeModel 读取
  • PUT /api/models/current — 更新 activeModel 并同步到 settings.json

保留 Effort Level 相关 API 不变。

Step 5: Provider 类型定义 (src/server/types/provider.ts)

独立的类型文件,包含:

  • Provider 接口
  • ProviderModel 接口
  • ProviderTestResult 接口
  • ProvidersConfig 接口providers.json 的根类型)
  • Zod schema 验证

文件变更清单

操作 文件路径 说明
新建 src/server/types/provider.ts Provider 类型定义和 Zod schema
新建 src/server/services/providerService.ts Provider 服务层CRUD + 激活 + 测试)
新建 src/server/api/providers.ts Provider REST API 路由处理
修改 src/server/router.ts 注册 /api/providers 路由
修改 src/server/api/models.ts 从 Provider 动态读取模型列表

总计: 3 个新文件 + 2 个修改文件


激活流程图

用户选择 Provider "OpenRouter" + 模型 "claude-opus-4-6"
  │
  ├─ 1. 更新 providers.json
  │     - 旧 provider: isActive = false
  │     - 新 provider: isActive = true
  │     - activeModel = "claude-opus-4-6"
  │
  ├─ 2. 读取当前 ~/.claude/settings.json
  │
  ├─ 3. 合并写入 settings.json
  │     {
  │       ...existingSettings,
  │       "env": {
  │         ...existingEnv,
  │         "ANTHROPIC_BASE_URL": "https://openrouter.ai/api/v1",
  │         "ANTHROPIC_AUTH_TOKEN": "sk-or-xxx"
  │       },
  │       "model": "claude-opus-4-6"
  │     }
  │
  └─ 4. 返回成功响应

连通性测试流程

POST /api/providers/test
Body: { baseUrl, apiKey, modelId }
  │
  ├─ 1. 构造最小请求
  │     POST {baseUrl}/v1/messages
  │     Headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" }
  │     Body: { model: modelId, max_tokens: 1, messages: [{ role: "user", content: "Hi" }] }
  │
  ├─ 2. 记录开始时间
  │
  ├─ 3. 发送请求(超时 15 秒)
  │
  └─ 4. 返回结果
        成功: { success: true, latencyMs: 850, modelUsed: "claude-opus-4-6" }
        失败: { success: false, error: "401 Unauthorized", latencyMs: 200 }

不在本次范围内

  • 前端 UI 组件(后续单独实现)
  • Provider 图标管理
  • API Key 加密存储V2 考虑)
  • 多 API 格式支持OpenAI 兼容等V2 考虑)
  • Provider 导入/导出
  • 自动故障转移failover


Scheduled Tasks Enhancement Plan

Overview

改进定时任务功能:支持编辑、新增工作目录选择、复用 sessions 组件。

参考官方 Claude Code 桌面端 APP 的 "New scheduled task" 对话框实现。


Phase 1: Data Model Extension

1.1 扩展 CronTask 类型 (src/utils/cronTasks.ts)

在现有 CronTask 类型中新增以下字段:

type CronTask = {
  // ... existing fields ...
  
  /** Human-readable task name (e.g. "daily-code-review") */
  name?: string
  /** Task description (e.g. "Review yesterday's commits") */
  description?: string
  /** Working directory for the task execution */
  folder?: string
  /** Model to use (e.g. "claude-opus-4-6", "claude-sonnet-4-6") */
  model?: string
  /** Permission mode: "ask" | "auto-accept" | "plan" | "bypass" */
  permissionMode?: string
  /** Whether to use git worktree for execution */
  worktree?: boolean
  /** Schedule frequency for UI display: "manual" | "hourly" | "daily" | "weekdays" | "weekly" */
  frequency?: string
  /** Time string for scheduled execution (e.g. "09:00") - UI helper for daily/weekdays/weekly */
  scheduledTime?: string
}

向后兼容:所有新字段均为 optional旧数据无需迁移。

1.2 更新存储 read/write (src/utils/cronTasks.ts)

  • readCronTasks(): 在读取时保留新字段name, description, folder, model, permissionMode, worktree, frequency, scheduledTime
  • writeCronTasks(): 在写入时包含新字段(仍然 strip durableagentId
  • addCronTask(): 扩展函数签名接收新字段

1.3 新增 updateCronTask() 函数 (src/utils/cronTasks.ts)

export async function updateCronTask(
  id: string,
  updates: Partial<Omit<CronTask, 'id' | 'createdAt'>>,
  dir?: string,
): Promise<boolean>
  • 查找并更新内存中session store或磁盘上的任务
  • 如果 cron 表达式变化,重新验证
  • 返回是否找到并更新成功

Phase 2: CronUpdateTool (src/tools/ScheduleCronTool/CronUpdateTool.ts)

2.1 创建新工具文件

参照 CronCreateTool.tsCronDeleteTool.ts 的模式:

const inputSchema = z.strictObject({
  id: z.string().describe('Job ID returned by CronCreate.'),
  cron: z.string().optional().describe('New cron expression'),
  prompt: z.string().optional().describe('New prompt'),
  name: z.string().optional().describe('New name'),
  description: z.string().optional().describe('New description'),
  folder: z.string().optional().describe('New working directory'),
  model: z.string().optional().describe('New model'),
  permissionMode: z.string().optional().describe('New permission mode'),
  worktree: z.boolean().optional().describe('New worktree setting'),
  recurring: z.boolean().optional().describe('New recurring setting'),
  frequency: z.string().optional().describe('New frequency'),
  scheduledTime: z.string().optional().describe('New time'),
})

2.2 更新 prompt.ts

  • 新增 CRON_UPDATE_TOOL_NAME = 'CronUpdate'
  • 添加 description 和 prompt 构建函数

2.3 更新 UI.tsx

  • 新增 renderUpdateToolUseMessage()renderUpdateResultMessage()

Phase 3: UI Components (复用 sessions 组件)

核心原则:所有可复用的组件直接 import 使用不复制代码。sessions 那边改了,这边自动生效。

3.1 频率到 Cron 表达式映射工具 (src/utils/cronFrequency.ts)

新建工具函数,在 UI 友好的频率设置和 cron 表达式之间互转:

// Frequency → Cron
function frequencyToCron(frequency: string, time?: string): string
// "daily" + "09:00" → "0 9 * * *"
// "hourly" → "0 * * * *"
// "weekdays" + "09:00" → "0 9 * * 1-5"
// "weekly" + "09:00" → "0 9 * * 1"

// Cron → Frequency (best effort)
function cronToFrequency(cron: string): { frequency: string; time?: string }

3.2 定时任务向导 (src/components/scheduled-tasks/ScheduledTaskWizard.tsx)

使用现有 Wizard 框架 (src/components/wizard/),创建多步骤向导:

type ScheduledTaskWizardData = {
  name: string
  description: string
  prompt: string
  model?: string
  permissionMode?: string
  folder?: string
  worktree?: boolean
  frequency: string       // "manual" | "hourly" | "daily" | "weekdays" | "weekly"
  scheduledTime?: string  // "09:00"
  cron?: string           // 最终生成的 cron 表达式
}

// 支持 create 和 edit 两种模式
type Props = {
  mode: 'create' | 'edit'
  initialData?: Partial<ScheduledTaskWizardData>  // edit 模式下预填充
  taskId?: string                                  // edit 模式下的任务 ID
  onComplete: (data: ScheduledTaskWizardData) => void
  onCancel: () => void
}

向导步骤(每步复用 sessions 组件):

Step 组件 复用来源
1. NameStep TextInput src/components/TextInput.tsx
2. DescriptionStep TextInput + external editor src/components/agents/new-agent-creation/wizard-steps/DescriptionStep.tsx 的模式
3. PromptStep TextInput + external editor src/components/agents/new-agent-creation/wizard-steps/PromptStep.tsx 的模式
4. ModelStep ModelSelector 直接复用 src/components/agents/ModelSelector.tsx
5. PermissionStep Select 直接复用 src/components/CustomSelect/select.tsx
6. FolderStep Select / FuzzyPicker 复用 src/components/CustomSelect/select.tsx + sessionStorage.ts 的项目发现
7. ScheduleStep Select + TextInput 直接复用 src/components/CustomSelect/select.tsx
8. ConfirmStep Summary display 使用 Dialog + Text 显示摘要

3.3 各步骤详细设计

Step 1: NameStep

// 直接使用 TextInput参照 DescriptionStep 模式
function NameStep(): ReactNode {
  const { goNext, goBack, updateWizardData, wizardData } = useWizard<ScheduledTaskWizardData>()
  // TextInput with validation: name is required
}

Step 4: ModelStep复用 ModelSelector

import { ModelSelector } from '../../agents/ModelSelector.js'

function ModelStep(): ReactNode {
  const { goNext, updateWizardData, wizardData } = useWizard<ScheduledTaskWizardData>()
  return (
    <WizardDialogLayout subtitle="Select Model">
      <ModelSelector
        initialModel={wizardData.model}
        onComplete={(model) => {
          updateWizardData({ model })
          goNext()
        }}
        onCancel={goBack}
      />
    </WizardDialogLayout>
  )
}

Step 5: PermissionStep复用 Select

import { Select } from '../../CustomSelect/select.js'

const permissionOptions = [
  { label: 'Ask permissions', value: 'ask', description: 'Always ask before making changes' },
  { label: 'Auto accept edits', value: 'auto-accept', description: 'Automatically accept all file edits' },
  { label: 'Plan mode', value: 'plan', description: 'Create a plan before making changes' },
  { label: 'Bypass permissions', value: 'bypass', description: 'Accepts all permissions', disabled: false },
]

Step 6: FolderStep复用 sessionStorage 项目发现)

import { loadAllProjectsMessageLogs } from '../../utils/sessionStorage.js'
// 或者直接读取 GlobalConfig.projects 获取最近项目列表

function FolderStep(): ReactNode {
  // 1. 从 GlobalConfig.projects 获取已知项目路径
  // 2. 使用 Select 展示 "Recent" 项目列表
  // 3. 最后一项 "Choose a different folder" 允许手动输入路径
}

Step 7: ScheduleStep频率 + 时间)

const frequencyOptions = [
  { label: 'Manual', value: 'manual' },
  { label: 'Hourly', value: 'hourly' },
  { label: 'Daily', value: 'daily' },
  { label: 'Weekdays', value: 'weekdays' },
  { label: 'Weekly', value: 'weekly' },
]

function ScheduleStep(): ReactNode {
  // 1. 选择频率 (Select 组件)
  // 2. 如果是 daily/weekdays/weekly显示时间输入 (TextInput格式 HH:MM)
  // 3. 使用 frequencyToCron() 转换为 cron 表达式
}

Step 8: ConfirmStep

function ConfirmStep(): ReactNode {
  // 显示所有配置的摘要
  // Enter 确认创建/更新
  // Esc 返回上一步
}

Phase 4: Integration

4.1 新增 /schedule-local Skill (src/skills/bundled/scheduleLocal.ts)

或修改现有 /schedule skill添加本地定时任务的向导流程

// 用户输入 /schedule 时:
// 1. 如果没参数 → 显示 ScheduledTaskWizard (create 模式)
// 2. 如果参数是 task ID → 显示 ScheduledTaskWizard (edit 模式,预填充)
// 3. 如果参数是 "list" → 调用 CronList

4.2 注册 CronUpdateTool

在工具注册表中添加 CronUpdateTool

  • 更新 src/tools/ScheduleCronTool/ 导出
  • 确保工具在 isKairosCronEnabled() 条件下启用

4.3 更新 CronListTool 输出

在列表输出中包含新字段name, description, folder, model 等),方便用户识别任务。


Phase 5: Testing

5.1 单元测试

  • cronTasks.ts: 测试 updateCronTask() 的各种场景(内存任务、磁盘任务、不存在的 ID
  • cronFrequency.ts: 测试频率到 cron 的双向转换
  • CronUpdateTool.ts: 测试验证逻辑(无效 ID、权限检查

5.2 集成测试

  • 验证 wizard 创建的任务能被 scheduler 正确执行
  • 验证编辑后的任务能正确更新 nextFireAt

File Changes Summary

File Change Type Description
src/utils/cronTasks.ts Modified 扩展 CronTask 类型,新增 updateCronTask(),更新 read/write/add
src/utils/cronFrequency.ts New 频率 ↔ Cron 表达式转换工具
src/tools/ScheduleCronTool/CronUpdateTool.ts New 编辑定时任务工具
src/tools/ScheduleCronTool/prompt.ts Modified 新增 CronUpdate 的 name/description/prompt
src/tools/ScheduleCronTool/UI.tsx Modified 新增 update 的 render 函数
src/components/scheduled-tasks/ScheduledTaskWizard.tsx New 定时任务向导(复用 sessions 组件)
src/components/scheduled-tasks/steps/NameStep.tsx New 名称输入步骤
src/components/scheduled-tasks/steps/DescriptionStep.tsx New 描述输入步骤
src/components/scheduled-tasks/steps/PromptStep.tsx New Prompt 输入步骤
src/components/scheduled-tasks/steps/ModelStep.tsx New 模型选择步骤(复用 ModelSelector
src/components/scheduled-tasks/steps/PermissionStep.tsx New 权限模式步骤(复用 Select
src/components/scheduled-tasks/steps/FolderStep.tsx New 工作目录步骤(复用 Select + 项目发现)
src/components/scheduled-tasks/steps/ScheduleStep.tsx New 频率+时间步骤(复用 Select
src/components/scheduled-tasks/steps/ConfirmStep.tsx New 确认步骤
src/skills/bundled/scheduleLocal.ts New or Modified /schedule skill 集成向导
src/hooks/useScheduledTasks.ts Modified 支持 folder/model/permissionMode/worktree 在任务执行时生效

Key Reuse Points (复用列表)

确保以下组件是直接 import 复用,不是复制代码

  1. WizardProvider - src/components/wizard/WizardProvider.tsx
  2. WizardDialogLayout - src/components/wizard/WizardDialogLayout.tsx
  3. useWizard - src/components/wizard/useWizard.ts
  4. WizardNavigationFooter - src/components/wizard/WizardNavigationFooter.tsx
  5. ModelSelector - src/components/agents/ModelSelector.tsx
  6. Select - src/components/CustomSelect/select.tsx
  7. TextInput - src/components/TextInput.tsx
  8. Dialog - src/components/design-system/Dialog.tsx
  9. FuzzyPicker - src/components/design-system/FuzzyPicker.tsx(如 FolderStep 需要搜索)
  10. useKeybinding - src/hooks/useKeybinding.ts
  11. Session Project Discovery - src/utils/sessionStorage.ts (loadAllProjectsMessageLogs)
  12. GlobalConfig.projects - src/utils/config.ts(最近项目列表)

Implementation Order

  1. Phase 1 (Data Model) → 2. Phase 2 (CronUpdateTool) → 3. Phase 3 (UI) → 4. Phase 4 (Integration) → 5. Phase 5 (Testing)

每个 Phase 完成后做一次代码审查。