diff --git a/RuoYi-Vue/docs/superpowers/plans/2026-05-24-ai-generation-task-center.md b/RuoYi-Vue/docs/superpowers/plans/2026-05-24-ai-generation-task-center.md new file mode 100644 index 0000000..6663a40 --- /dev/null +++ b/RuoYi-Vue/docs/superpowers/plans/2026-05-24-ai-generation-task-center.md @@ -0,0 +1,197 @@ +# AI Generation Task Center Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Convert long-running DeepSeek generation requests into trackable asynchronous tasks with generation records, basic quota accounting, cost usage, and retry support. + +**Architecture:** Keep the existing synchronous `IAiGenerateService` methods as the actual generation engine and add a DB-backed task layer in front of them. A task service creates task rows, reserves quota, and a worker claims pending tasks with a short database lock before executing generation outside the claim transaction. The Vue workbench starts tasks, polls status, and applies the generated result after success. + +**Tech Stack:** Spring Boot, MyBatis XML mappers, MySQL tables in `sql/front_workbench.sql` and `sql/db.sql`, existing RuoYi thread pool conventions, Vue 3 + Element Plus. + +--- + +### File Structure + +- Create `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/AiGenerationTask.java`: task queue domain object. +- Create `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/AiUsageLedger.java`: usage/cost ledger domain object. +- Create `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/AiQuotaBucket.java`: per-user quota bucket domain object. +- Create DTOs under `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto`: `AiGenerationTaskCreateRequest`, `AiGenerationTaskStatusResponse`, `AiQuotaResponse`. +- Create mapper interfaces and XML files under `ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/front` and `ruoyi-generator/src/main/resources/mapper/front`. +- Create services under `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front`: `IAiGenerationTaskService`, `AiGenerationTaskServiceImpl`, `AiGenerationTaskWorker`, `AiQuotaService`, `AiCostCalculator`, `AiTaskRetryPolicy`. +- Modify `ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontProjectController.java`: add task create/status/retry/cancel/quota/history endpoints. +- Modify `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java`: expose execution helpers or keep using the public sync methods from worker. +- Modify `easycode-web/src/api/project.js`: add task APIs. +- Modify `easycode-web/src/views/GenerateView.vue`: replace blocking generation calls with task start + polling. +- Modify SQL scripts: add task, quota, and ledger tables. +- Add focused tests in `ruoyi-generator/src/test/java/com/ruoyi/generator/service/front`. + +### Task 1: Task Status and Retry Policy + +**Files:** +- Create: `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiTaskRetryPolicy.java` +- Test: `ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiTaskRetryPolicyTest.java` + +- [ ] **Step 1: Write failing tests** + +Test retryable messages: timeout, 429, 5xx, and DeepSeek unavailable retry. Test validation/permission/quota messages do not retry. Test backoff sequence returns 30 seconds, 2 minutes, 5 minutes. + +- [ ] **Step 2: Run test to verify it fails** + +Run: `mvn -pl ruoyi-generator -Dtest=AiTaskRetryPolicyTest test` +Expected: compile failure because `AiTaskRetryPolicy` does not exist. + +- [ ] **Step 3: Implement minimal retry policy** + +Create constants for max attempts and retry delays. Add `boolean canRetry(String errorMessage, int attempts, int maxAttempts)` and `Date nextRetryTime(int attempts, Date now)`. + +- [ ] **Step 4: Run test to verify it passes** + +Run: `mvn -pl ruoyi-generator -Dtest=AiTaskRetryPolicyTest test` +Expected: PASS. + +### Task 2: Cost and Quota Services + +**Files:** +- Create: `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiCostCalculator.java` +- Create: `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiQuotaService.java` +- Create: `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/AiQuotaBucket.java` +- Create: `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/AiUsageLedger.java` +- Test: `ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiCostCalculatorTest.java` +- Test: `ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiQuotaServiceTest.java` + +- [ ] **Step 1: Write failing tests** + +Test default quota allows a first task, blocks when daily count reaches 20, blocks when monthly cost reaches 5000 cents, reserves one running task, and releases running count on finish. + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `mvn -pl ruoyi-generator -Dtest=AiCostCalculatorTest,AiQuotaServiceTest test` +Expected: compile failure for missing services/domain types. + +- [ ] **Step 3: Implement minimal services** + +Use mapper methods for load/create/update quota rows. Cost calculator returns 0 when tokens are missing and uses configurable defaults for DeepSeek estimates. + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `mvn -pl ruoyi-generator -Dtest=AiCostCalculatorTest,AiQuotaServiceTest test` +Expected: PASS. + +### Task 3: Task Persistence + +**Files:** +- Create: `AiGenerationTask` domain and mapper XML/interface. +- Create: `AiUsageLedgerMapper` and `AiQuotaBucketMapper`. +- Modify: `sql/front_workbench.sql` +- Modify: `sql/db.sql` + +- [ ] **Step 1: Write mapper/service tests** + +Add tests for creating task rows, selecting task by user/project/task, selecting claimable tasks, updating lock/status, and inserting usage ledger. + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `mvn -pl ruoyi-generator -Dtest=AiGenerationTaskServiceImplTest test` +Expected: compile failure for missing domain/mapper/service methods. + +- [ ] **Step 3: Implement MyBatis domain and XML** + +Use status values `QUEUED`, `RUNNING`, `RETRY_WAITING`, `SUCCEEDED`, `FAILED`, `CANCELED`. Add indexes for `(status, next_retry_time)`, `(user_id, project_id)`, and `generation_id`. + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `mvn -pl ruoyi-generator -Dtest=AiGenerationTaskServiceImplTest test` +Expected: PASS. + +### Task 4: Task Service and Worker + +**Files:** +- Create: `IAiGenerationTaskService` +- Create: `AiGenerationTaskServiceImpl` +- Create: `AiGenerationTaskWorker` +- Modify: `FrontProjectController` +- Test: `AiGenerationTaskServiceImplTest` + +- [ ] **Step 1: Write failing service tests** + +Test task creation validates project ownership, reserves quota, creates a `front_project_generation` row, creates a queued task, returns status DTO, and deduplicates a still-running same request hash. + +- [ ] **Step 2: Run test to verify it fails** + +Run: `mvn -pl ruoyi-generator -Dtest=AiGenerationTaskServiceImplTest test` +Expected: compile failure or assertion failure before implementation. + +- [ ] **Step 3: Implement task creation and status queries** + +Generate request hash from `generateType + normalized request JSON`. Store request payload as JSON. Return `taskId`, `status`, `currentStep`, `progress`, `generationId`. + +- [ ] **Step 4: Write failing worker tests** + +Test worker executes app blueprint, database, and business blueprint tasks by delegating to existing sync service methods, marks success, records elapsed time, inserts ledger, and schedules retry for retryable failures. + +- [ ] **Step 5: Implement worker** + +Claim tasks using `locked_by` and `locked_until`, run outside the claim transaction, then update status. Keep max attempts at 3. + +- [ ] **Step 6: Run tests to verify they pass** + +Run: `mvn -pl ruoyi-generator -Dtest=AiGenerationTaskServiceImplTest,AiGenerationTaskWorkerTest test` +Expected: PASS. + +### Task 5: REST API + +**Files:** +- Modify: `ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontProjectController.java` +- Test: service tests remain the main safety net. + +- [ ] **Step 1: Add controller endpoints** + +Add `POST /front/project/{projectId}/ai-tasks`, `GET /front/project/{projectId}/ai-tasks/{taskId}`, `POST /front/project/{projectId}/ai-tasks/{taskId}/retry`, `POST /front/project/{projectId}/ai-tasks/{taskId}/cancel`, `GET /front/project/{projectId}/generations`, and `GET /front/project/ai-quota`. + +- [ ] **Step 2: Run compile tests** + +Run: `mvn -pl ruoyi-admin -DskipTests compile` +Expected: SUCCESS. + +### Task 6: Frontend Polling Experience + +**Files:** +- Modify: `easycode-web/src/api/project.js` +- Modify: `easycode-web/src/views/GenerateView.vue` + +- [ ] **Step 1: Add task API helpers** + +Add `createAiTask`, `getAiTask`, `retryAiTask`, `cancelAiTask`, `getAiQuota`, and `listGenerations`. + +- [ ] **Step 2: Update generation handlers** + +Replace direct calls to `generateAppBlueprint`, `generateDatabase`, and `generateBusinessBlueprint` with `createAiTask` plus polling. Keep the old direct API helpers for compatibility. + +- [ ] **Step 3: Show task state** + +Display status, progress, error message, retry button, and quota summary in the existing generate page. + +- [ ] **Step 4: Run frontend tests/build** + +Run direct `.mjs` tests with Node 20 and run Vite build. +Expected: tests and build pass. + +### Task 7: Final Verification + +**Files:** +- No new files. + +- [ ] **Step 1: Run backend tests** + +Run: `mvn test` +Expected: SUCCESS. + +- [ ] **Step 2: Run frontend tests/build** + +Run easycode-web direct `.mjs` tests and Vite build. +Expected: SUCCESS. + +- [ ] **Step 3: Inspect git diff** + +Run: `git diff --stat` and `git status --short`. +Expected: only intended AI task center files changed. diff --git a/RuoYi-Vue/easycode-web/src/api/project.js b/RuoYi-Vue/easycode-web/src/api/project.js index b6adb89..2e5dd69 100644 --- a/RuoYi-Vue/easycode-web/src/api/project.js +++ b/RuoYi-Vue/easycode-web/src/api/project.js @@ -76,6 +76,49 @@ export function generateBusinessBlueprint(projectId, data) { }).then(unwrap) } +export function createAiTask(projectId, data) { + return request({ + url: `/front/project/${projectId}/ai-tasks`, + method: 'post', + data + }).then(unwrap) +} + +export function getAiTask(projectId, taskId) { + return request({ + url: `/front/project/${projectId}/ai-tasks/${taskId}`, + method: 'get' + }).then(unwrap) +} + +export function retryAiTask(projectId, taskId) { + return request({ + url: `/front/project/${projectId}/ai-tasks/${taskId}/retry`, + method: 'post' + }).then(unwrap) +} + +export function cancelAiTask(projectId, taskId) { + return request({ + url: `/front/project/${projectId}/ai-tasks/${taskId}/cancel`, + method: 'post' + }).then(unwrap) +} + +export function getAiQuota() { + return request({ + url: '/front/project/ai-quota', + method: 'get' + }).then(unwrap) +} + +export function listGenerations(projectId) { + return request({ + url: `/front/project/${projectId}/generations`, + method: 'get' + }).then(unwrap) +} + export function getDatabase(projectId) { return request({ url: `/front/project/${projectId}/database`, diff --git a/RuoYi-Vue/easycode-web/src/views/GenerateView.vue b/RuoYi-Vue/easycode-web/src/views/GenerateView.vue index f6540cf..ca2506f 100644 --- a/RuoYi-Vue/easycode-web/src/views/GenerateView.vue +++ b/RuoYi-Vue/easycode-web/src/views/GenerateView.vue @@ -105,6 +105,31 @@ 预览项目 + +
+
+
+ {{ taskTypeLabel(activeAiTask.generateType) }} + {{ taskStatusLabel(activeAiTask.status) }} +
+ +

{{ activeAiTask.currentStep || '-' }}

+

{{ activeAiTask.errorMessage }}

+ + 重试 + +
+
+ 今日 {{ aiQuota.dailyTaskUsed || 0 }}/{{ aiQuota.dailyTaskLimit || 0 }} + 运行中 {{ aiQuota.runningCount || 0 }}/{{ aiQuota.runningLimit || 0 }} + 本月成本 {{ aiQuota.monthlyCostUsedCents || 0 }}/{{ aiQuota.monthlyCostLimitCents || 0 }} +
+
@@ -220,14 +245,14 @@