feat: 智能复核添加快捷确认功能
This commit is contained in:
235
AGENTS.md
235
AGENTS.md
@@ -1,114 +1,63 @@
|
|||||||
# AGENTS.md - AI Coding Agent Guidelines
|
# AGENTS.md - AI Coding Agent Guidelines
|
||||||
|
|
||||||
This document provides guidelines for AI coding agents working on the BillAI project.
|
Guidelines for AI coding agents working on BillAI - a microservices bill analysis system.
|
||||||
|
|
||||||
## Project Overview
|
## Architecture
|
||||||
|
- `web/` - SvelteKit 5 + TailwindCSS 4 + TypeScript
|
||||||
|
- `server/` - Go 1.21 + Gin + MongoDB
|
||||||
|
- `analyzer/` - Python 3.12 + FastAPI
|
||||||
|
|
||||||
BillAI is a microservices-based personal bill analysis system supporting WeChat and Alipay bill parsing, intelligent categorization, and visualization.
|
## Build/Lint/Test Commands
|
||||||
|
|
||||||
**Architecture:**
|
|
||||||
- `web/` - Frontend (SvelteKit 5 + TailwindCSS 4.x + TypeScript)
|
|
||||||
- `server/` - Backend API (Go 1.21 + Gin + MongoDB)
|
|
||||||
- `analyzer/` - Python analysis service (Python 3.12 + FastAPI)
|
|
||||||
|
|
||||||
## Build, Lint, and Test Commands
|
|
||||||
|
|
||||||
### Frontend (web/)
|
### Frontend (web/)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Development
|
npm run dev # Start dev server
|
||||||
npm run dev # Start dev server (Vite)
|
npm run build # Production build
|
||||||
|
npm run check # TypeScript check
|
||||||
# Build
|
npm run lint # Prettier + ESLint
|
||||||
npm run build # Production build
|
npm run format # Format code
|
||||||
npm run preview # Preview production build
|
npm run test # Run all tests
|
||||||
|
npx vitest run src/routes/+page.spec.ts # Single test file
|
||||||
# Type checking
|
npx vitest run -t "test name" # Test by name
|
||||||
npm run check # svelte-check with TypeScript
|
|
||||||
npm run check:watch # Watch mode
|
|
||||||
|
|
||||||
# Linting and formatting
|
|
||||||
npm run lint # Prettier check + ESLint
|
|
||||||
npm run format # Format with Prettier
|
|
||||||
|
|
||||||
# Testing
|
|
||||||
npm run test # Run all tests once
|
|
||||||
npm run test:unit # Run tests in watch mode
|
|
||||||
npx vitest run src/demo.spec.ts # Run single test file
|
|
||||||
npx vitest run -t "sum test" # Run tests matching name
|
|
||||||
npx vitest run src/routes/page.svelte.spec.ts # Run component test
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Backend (server/)
|
### Backend (server/)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run
|
go run . # Start server
|
||||||
go run . # Start development server
|
go build . # Build binary
|
||||||
|
go mod tidy # Clean dependencies
|
||||||
# Build
|
go test ./... # All tests
|
||||||
go build . # Build binary
|
go test ./handler/... # Package tests
|
||||||
|
go test -run TestName # Single test
|
||||||
# Dependencies
|
|
||||||
go mod download # Install dependencies
|
|
||||||
go mod tidy # Clean up dependencies
|
|
||||||
|
|
||||||
# Testing (if tests exist)
|
|
||||||
go test ./... # Run all tests
|
|
||||||
go test ./handler/... # Run tests in specific package
|
|
||||||
go test -run TestName # Run single test by name
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Analyzer (analyzer/)
|
### Analyzer (analyzer/)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Setup
|
python server.py # Start FastAPI server
|
||||||
python -m venv venv
|
pytest # All tests
|
||||||
pip install -r requirements.txt
|
pytest test_file.py # Single file
|
||||||
|
pytest -k "test_name" # Test by pattern
|
||||||
# Run
|
|
||||||
python server.py # Start FastAPI server
|
|
||||||
|
|
||||||
# Testing (if tests exist)
|
|
||||||
pytest # Run all tests
|
|
||||||
pytest test_file.py # Run single test file
|
|
||||||
pytest -k "test_name" # Run tests matching name
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d --build # Start all services
|
docker-compose up -d --build # Start/rebuild all
|
||||||
docker-compose ps # Check service status
|
docker-compose logs -f server # Follow service logs
|
||||||
docker-compose down # Stop all services
|
|
||||||
docker-compose logs -f web # Follow logs for specific service
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Code Style Guidelines
|
## Code Style
|
||||||
|
|
||||||
### TypeScript/Svelte (Frontend)
|
### TypeScript/Svelte
|
||||||
|
**Prettier config:** Tabs, single quotes, no trailing commas, width 100
|
||||||
**Formatting (Prettier):**
|
|
||||||
- Use tabs for indentation
|
|
||||||
- Single quotes for strings
|
|
||||||
- No trailing commas
|
|
||||||
- Print width: 100 characters
|
|
||||||
|
|
||||||
**Imports:**
|
**Imports:**
|
||||||
- Use `$lib/` alias for imports from `src/lib/`
|
|
||||||
- Use `$app/` for SvelteKit internals
|
|
||||||
- Group imports: external packages, then internal modules
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment'; // SvelteKit
|
||||||
import { auth } from '$lib/stores/auth';
|
import { auth } from '$lib/stores/auth'; // Internal
|
||||||
import type { UIBill } from '$lib/models/bill';
|
import type { UIBill } from '$lib/models/bill';
|
||||||
```
|
```
|
||||||
|
|
||||||
**Types:**
|
**Types:**
|
||||||
- Define interfaces for API responses and requests
|
|
||||||
- Use `type` for unions and simple type aliases
|
|
||||||
- Export types from dedicated files in `$lib/types/` or alongside models
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export interface UploadResponse {
|
export interface UploadResponse {
|
||||||
result: boolean;
|
result: boolean;
|
||||||
@@ -117,55 +66,20 @@ export interface UploadResponse {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Naming Conventions:**
|
**Naming:** PascalCase (types, components), camelCase (functions, variables)
|
||||||
- PascalCase: Components, interfaces, types
|
|
||||||
- camelCase: Functions, variables, properties
|
|
||||||
- Use descriptive names: `fetchBills`, `UIBill`, `checkHealth`
|
|
||||||
|
|
||||||
**Error Handling:**
|
|
||||||
- Wrap API calls in try/catch
|
|
||||||
- Throw `Error` with HTTP status for API failures
|
|
||||||
- Handle 401 responses with logout redirect
|
|
||||||
|
|
||||||
|
**Error handling:**
|
||||||
```typescript
|
```typescript
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP ${response.status}`);
|
throw new Error(`HTTP ${response.status}`);
|
||||||
}
|
}
|
||||||
|
// Handle 401 -> logout redirect
|
||||||
```
|
```
|
||||||
|
|
||||||
### Go (Backend)
|
### Go Backend
|
||||||
|
**Structure:** `handler/` → `service/` → `repository/` → MongoDB
|
||||||
**Project Structure:**
|
|
||||||
- `handler/` - HTTP request handlers
|
|
||||||
- `service/` - Business logic
|
|
||||||
- `repository/` - Data access layer
|
|
||||||
- `model/` - Data structures
|
|
||||||
- `adapter/` - External service integrations
|
|
||||||
- `config/` - Configuration management
|
|
||||||
- `middleware/` - Auth and other middleware
|
|
||||||
|
|
||||||
**Naming Conventions:**
|
|
||||||
- PascalCase: Exported types, functions, constants
|
|
||||||
- camelCase: Unexported functions, variables
|
|
||||||
- Use descriptive names: `UpdateBillRequest`, `parseBillTime`
|
|
||||||
|
|
||||||
**Error Handling:**
|
|
||||||
- Define sentinel errors in `repository/errors.go`
|
|
||||||
- Return errors up the call stack
|
|
||||||
- Use structured JSON responses for HTTP errors
|
|
||||||
|
|
||||||
```go
|
|
||||||
if err == repository.ErrNotFound {
|
|
||||||
c.JSON(http.StatusNotFound, Response{Result: false, Message: "not found"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**JSON Tags:**
|
|
||||||
- Use snake_case for JSON field names
|
|
||||||
- Use `omitempty` for optional fields
|
|
||||||
- Match frontend API expectations
|
|
||||||
|
|
||||||
|
**JSON tags:** snake_case, omitempty for optional fields
|
||||||
```go
|
```go
|
||||||
type UpdateBillRequest struct {
|
type UpdateBillRequest struct {
|
||||||
Category *string `json:"category,omitempty"`
|
Category *string `json:"category,omitempty"`
|
||||||
@@ -173,9 +87,7 @@ type UpdateBillRequest struct {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response Format:**
|
**Response format:**
|
||||||
- All API responses use consistent structure:
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type Response struct {
|
type Response struct {
|
||||||
Result bool `json:"result"`
|
Result bool `json:"result"`
|
||||||
@@ -184,12 +96,16 @@ type Response struct {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Python (Analyzer)
|
**Error handling:**
|
||||||
|
```go
|
||||||
|
if err == repository.ErrNotFound {
|
||||||
|
c.JSON(http.StatusNotFound, Response{Result: false, Message: "not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
**Style:**
|
### Python Analyzer
|
||||||
- Follow PEP 8
|
**Style:** PEP 8, type hints, Pydantic models
|
||||||
- Use type hints for function signatures
|
|
||||||
- Use Pydantic models for request/response validation
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def do_clean(
|
def do_clean(
|
||||||
@@ -199,56 +115,27 @@ def do_clean(
|
|||||||
) -> tuple[bool, str, str]:
|
) -> tuple[bool, str, str]:
|
||||||
```
|
```
|
||||||
|
|
||||||
**Error Handling:**
|
**Error handling:**
|
||||||
- Raise `HTTPException` for API errors
|
|
||||||
- Use try/except for file operations
|
|
||||||
- Return structured responses
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(status_code=400, detail=message)
|
raise HTTPException(status_code=400, detail=message)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing Guidelines
|
## Key Patterns
|
||||||
|
|
||||||
**Frontend Tests:**
|
**API Flow:** Frontend (SvelteKit proxy) → Go API → MongoDB + Python analyzer
|
||||||
- Use Vitest with Playwright for browser testing
|
|
||||||
- Component tests: `*.svelte.spec.ts`
|
|
||||||
- Unit tests: `*.spec.ts`
|
|
||||||
- Tests require assertions: `expect.assertions()` or explicit expects
|
|
||||||
|
|
||||||
```typescript
|
**Auth:** JWT tokens, Bearer header, 401 → logout redirect
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { render } from 'vitest-browser-svelte';
|
|
||||||
|
|
||||||
describe('/+page.svelte', () => {
|
**File Processing:** ZIP → extract → convert (GBK→UTF-8, xlsx→csv) → clean → import
|
||||||
it('should render h1', async () => {
|
|
||||||
render(Page);
|
|
||||||
await expect.element(page.getByRole('heading')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Important Patterns
|
**Testing:** Vitest + Playwright for frontend, Go test for backend
|
||||||
|
|
||||||
**API Communication:**
|
## Important Files
|
||||||
- Frontend proxies API calls through SvelteKit to avoid CORS
|
- `web/src/lib/api.ts` - API client
|
||||||
- Backend uses Gin framework with JSON responses
|
- `web/src/lib/models/` - UI data models
|
||||||
- Analyzer communicates via HTTP (preferred) or subprocess
|
- `server/handler/` - HTTP handlers
|
||||||
|
- `server/service/` - Business logic
|
||||||
**Data Flow:**
|
- `server/model/` - Go data structures
|
||||||
- Frontend (SvelteKit) -> Backend (Go/Gin) -> MongoDB
|
- `analyzer/cleaners/` - Bill processing
|
||||||
- Backend -> Analyzer (Python/FastAPI) for bill parsing
|
- `mock_data/*.zip` - Test data (password: 123456)
|
||||||
|
|
||||||
**Authentication:**
|
|
||||||
- JWT tokens stored in frontend auth store
|
|
||||||
- Bearer token sent in Authorization header
|
|
||||||
- 401 responses trigger logout and redirect
|
|
||||||
|
|
||||||
## File Locations
|
|
||||||
|
|
||||||
- API types: `web/src/lib/api.ts`
|
|
||||||
- UI models: `web/src/lib/models/`
|
|
||||||
- Go handlers: `server/handler/`
|
|
||||||
- Go models: `server/model/`
|
|
||||||
- Python API: `analyzer/server.py`
|
|
||||||
|
|||||||
16
CHANGELOG.md
16
CHANGELOG.md
@@ -19,6 +19,22 @@
|
|||||||
- 新增 `DELETE /api/bills/:id` 接口
|
- 新增 `DELETE /api/bills/:id` 接口
|
||||||
- `BillDetailDrawer` 组件新增 `allowDelete` 和 `onDelete` props
|
- `BillDetailDrawer` 组件新增 `allowDelete` 和 `onDelete` props
|
||||||
|
|
||||||
|
## [1.2.1] - 2026-01-23
|
||||||
|
|
||||||
|
### 优化
|
||||||
|
- **智能复核快捷确认** - 在复核列表每行添加快捷确认按钮
|
||||||
|
- 无需打开详情页面即可确认分类正确
|
||||||
|
- 点击确认按钮立即清除复核标记并从列表移除
|
||||||
|
- 自动更新统计数据(总数、高优先级、低优先级计数)
|
||||||
|
- 按钮支持加载状态显示,防止重复操作
|
||||||
|
- 提升复核效率,支持快速批量确认
|
||||||
|
|
||||||
|
### 文档
|
||||||
|
- **AGENTS.md 更新** - 精简为 150 行,专为 AI 编程助手设计
|
||||||
|
- 核心构建/测试/lint 命令说明
|
||||||
|
- TypeScript、Go、Python 代码风格指南
|
||||||
|
- 关键架构模式和文件位置
|
||||||
|
|
||||||
## [1.1.0] - 2026-01-23
|
## [1.1.0] - 2026-01-23
|
||||||
|
|
||||||
### 新增
|
### 新增
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.2.0",
|
"version": "1.2.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
|||||||
@@ -7,12 +7,14 @@
|
|||||||
import * as Table from '$lib/components/ui/table';
|
import * as Table from '$lib/components/ui/table';
|
||||||
import BillDetailDrawer from '$lib/components/analysis/BillDetailDrawer.svelte';
|
import BillDetailDrawer from '$lib/components/analysis/BillDetailDrawer.svelte';
|
||||||
import { cleanedBillToUIBill, type UIBill } from '$lib/models/bill';
|
import { cleanedBillToUIBill, type UIBill } from '$lib/models/bill';
|
||||||
|
import { updateBill } from '$lib/api';
|
||||||
import Loader2 from '@lucide/svelte/icons/loader-2';
|
import Loader2 from '@lucide/svelte/icons/loader-2';
|
||||||
import AlertCircle from '@lucide/svelte/icons/alert-circle';
|
import AlertCircle from '@lucide/svelte/icons/alert-circle';
|
||||||
import AlertTriangle from '@lucide/svelte/icons/alert-triangle';
|
import AlertTriangle from '@lucide/svelte/icons/alert-triangle';
|
||||||
import Clock from '@lucide/svelte/icons/clock';
|
import Clock from '@lucide/svelte/icons/clock';
|
||||||
import PartyPopper from '@lucide/svelte/icons/party-popper';
|
import PartyPopper from '@lucide/svelte/icons/party-popper';
|
||||||
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
|
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
|
||||||
|
import Check from '@lucide/svelte/icons/check';
|
||||||
|
|
||||||
let isLoading = $state(true);
|
let isLoading = $state(true);
|
||||||
let errorMessage = $state('');
|
let errorMessage = $state('');
|
||||||
@@ -20,6 +22,9 @@
|
|||||||
let allBills = $state<CleanedBill[]>([]);
|
let allBills = $state<CleanedBill[]>([]);
|
||||||
let filterLevel = $state<'all' | 'HIGH' | 'LOW'>('all');
|
let filterLevel = $state<'all' | 'HIGH' | 'LOW'>('all');
|
||||||
|
|
||||||
|
// 快捷确认按钮的加载状态 (记录ID -> 是否在加载)
|
||||||
|
let confirmingBills = $state<Map<string, boolean>>(new Map());
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
loadReviewData();
|
loadReviewData();
|
||||||
});
|
});
|
||||||
@@ -88,6 +93,48 @@
|
|||||||
drawerOpen = true;
|
drawerOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 快捷确认(仅清除 review_level,不修改其他字段)
|
||||||
|
async function quickConfirm(record: CleanedBill, event: Event) {
|
||||||
|
// 阻止事件冒泡,避免触发行点击
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (confirmingBills.get(record.id)) return;
|
||||||
|
|
||||||
|
// 设置加载状态
|
||||||
|
confirmingBills.set(record.id, true);
|
||||||
|
confirmingBills = new Map(confirmingBills);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await updateBill(record.id, { review_level: '' });
|
||||||
|
|
||||||
|
if (resp.result) {
|
||||||
|
// 从列表中移除该记录
|
||||||
|
const index = allBills.findIndex(r => r.id === record.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
allBills.splice(index, 1);
|
||||||
|
allBills = [...allBills];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新统计数据
|
||||||
|
if (reviewStats) {
|
||||||
|
reviewStats = {
|
||||||
|
...reviewStats,
|
||||||
|
total: Math.max(0, reviewStats.total - 1),
|
||||||
|
high: record.review_level === 'HIGH' ? Math.max(0, reviewStats.high - 1) : reviewStats.high,
|
||||||
|
low: record.review_level === 'LOW' ? Math.max(0, reviewStats.low - 1) : reviewStats.low
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('快捷确认失败:', err);
|
||||||
|
// 这里可以添加错误提示
|
||||||
|
} finally {
|
||||||
|
// 清除加载状态
|
||||||
|
confirmingBills.delete(record.id);
|
||||||
|
confirmingBills = new Map(confirmingBills);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 复核完成后从列表中移除该记录
|
// 复核完成后从列表中移除该记录
|
||||||
function handleBillUpdate(updated: UIBill, original: UIBill) {
|
function handleBillUpdate(updated: UIBill, original: UIBill) {
|
||||||
// 更新后 review_level 已被清除,从列表中移除
|
// 更新后 review_level 已被清除,从列表中移除
|
||||||
@@ -254,6 +301,7 @@
|
|||||||
<Table.Head>收/支</Table.Head>
|
<Table.Head>收/支</Table.Head>
|
||||||
<Table.Head class="text-right">金额</Table.Head>
|
<Table.Head class="text-right">金额</Table.Head>
|
||||||
<Table.Head class="w-[80px]">优先级</Table.Head>
|
<Table.Head class="w-[80px]">优先级</Table.Head>
|
||||||
|
<Table.Head class="w-[100px] text-center">操作</Table.Head>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
</Table.Header>
|
</Table.Header>
|
||||||
<Table.Body>
|
<Table.Body>
|
||||||
@@ -287,6 +335,23 @@
|
|||||||
{record.review_level}
|
{record.review_level}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
|
<Table.Cell class="text-center">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
class="h-7 px-2 text-xs"
|
||||||
|
onclick={(e) => quickConfirm(record, e)}
|
||||||
|
disabled={confirmingBills.get(record.id) || false}
|
||||||
|
title="确认分类正确"
|
||||||
|
>
|
||||||
|
{#if confirmingBills.get(record.id)}
|
||||||
|
<Loader2 class="h-3 w-3 animate-spin" />
|
||||||
|
{:else}
|
||||||
|
<Check class="h-3 w-3 mr-1" />
|
||||||
|
确认
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
</Table.Cell>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
{/each}
|
{/each}
|
||||||
</Table.Body>
|
</Table.Body>
|
||||||
|
|||||||
Reference in New Issue
Block a user