feat(database): message.stats and related message type definitions

- Changed migration command from `yarn run migrations:generate` to `yarn run db:migrations:generate` for consistency across the project.
- Updated related documentation in `CLAUDE.md`, `migrations/README.md`, and `src/main/data/README.md` to reflect the new command.
- Added a notice in `migrations/README.md` regarding potential database structure changes before the alpha release.
This commit is contained in:
fullex 2025-12-25 21:52:07 +08:00
parent 6633082335
commit 8292958c0d
11 changed files with 874 additions and 22 deletions

View File

@ -120,7 +120,7 @@ UI Library: `@packages/ui`
- **JSON Fields**: For JSON support, add `{ mode: 'json' }`, refer to `preference.ts` table definition
- **JSON Serialization**: For JSON fields, no need to manually serialize/deserialize when reading/writing to database, Drizzle handles this automatically
- **Timestamps**: Use existing `crudTimestamps` utility
- **Migrations**: Generate via `yarn run migrations:generate`
- **Migrations**: Generate via `yarn run db:migrations:generate`
## Data Access Patterns

View File

@ -1,6 +1,10 @@
**THIS DIRECTORY IS NOT FOR RUNTIME USE**
**v2 Data Refactoring Notice**
Before the official release of the alpha version, the database structure may change at any time. To maintain simplicity, the database migration files will be periodically reinitialized, which may cause the application to fail. If this occurs, please delete the `cherrystudio.sqlite` file located in the user data directory.
- Using `libsql` as the `sqlite3` driver, and `drizzle` as the ORM and database migration tool
- Table schemas are defined in `src\main\data\db\schemas`
- `migrations/sqlite-drizzle` contains auto-generated migration data. Please **DO NOT** modify it.
- If table structure changes, we should run migrations.
- To generate migrations, use the command `yarn run migrations:generate`
- To generate migrations, use the command `yarn run db:migrations:generate`

View File

@ -0,0 +1,3 @@
ALTER TABLE `message` ADD `stats` text;--> statement-breakpoint
ALTER TABLE `message` DROP COLUMN `usage`;--> statement-breakpoint
ALTER TABLE `message` DROP COLUMN `metrics`;

View File

@ -0,0 +1,655 @@
{
"version": "6",
"dialect": "sqlite",
"id": "ae53858a-1786-4059-9ff7-9e87267911b6",
"prevId": "62a198e0-bfc2-4db1-af58-7e479fedd7b9",
"tables": {
"app_state": {
"name": "app_state",
"columns": {
"key": {
"name": "key",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"entity_tag": {
"name": "entity_tag",
"columns": {
"entity_type": {
"name": "entity_type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"entity_id": {
"name": "entity_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"tag_id": {
"name": "tag_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"entity_tag_tag_id_idx": {
"name": "entity_tag_tag_id_idx",
"columns": [
"tag_id"
],
"isUnique": false
}
},
"foreignKeys": {
"entity_tag_tag_id_tag_id_fk": {
"name": "entity_tag_tag_id_tag_id_fk",
"tableFrom": "entity_tag",
"tableTo": "tag",
"columnsFrom": [
"tag_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"entity_tag_entity_type_entity_id_tag_id_pk": {
"columns": [
"entity_type",
"entity_id",
"tag_id"
],
"name": "entity_tag_entity_type_entity_id_tag_id_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"group": {
"name": "group",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"entity_type": {
"name": "entity_type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"sort_order": {
"name": "sort_order",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"group_entity_sort_idx": {
"name": "group_entity_sort_idx",
"columns": [
"entity_type",
"sort_order"
],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"message": {
"name": "message",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"topic_id": {
"name": "topic_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"parent_id": {
"name": "parent_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"response_group_id": {
"name": "response_group_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"assistant_id": {
"name": "assistant_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"assistant_meta": {
"name": "assistant_meta",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"model_id": {
"name": "model_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"model_meta": {
"name": "model_meta",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"data": {
"name": "data",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"stats": {
"name": "stats",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"trace_id": {
"name": "trace_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"searchable_text": {
"name": "searchable_text",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"deleted_at": {
"name": "deleted_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"message_parent_id_idx": {
"name": "message_parent_id_idx",
"columns": [
"parent_id"
],
"isUnique": false
},
"message_topic_created_idx": {
"name": "message_topic_created_idx",
"columns": [
"topic_id",
"created_at"
],
"isUnique": false
},
"message_trace_id_idx": {
"name": "message_trace_id_idx",
"columns": [
"trace_id"
],
"isUnique": false
}
},
"foreignKeys": {
"message_topic_id_topic_id_fk": {
"name": "message_topic_id_topic_id_fk",
"tableFrom": "message",
"tableTo": "topic",
"columnsFrom": [
"topic_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"message_parent_id_message_id_fk": {
"name": "message_parent_id_message_id_fk",
"tableFrom": "message",
"tableTo": "message",
"columnsFrom": [
"parent_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {
"message_role_check": {
"name": "message_role_check",
"value": "\"message\".\"role\" IN ('user', 'assistant', 'system')"
},
"message_status_check": {
"name": "message_status_check",
"value": "\"message\".\"status\" IN ('success', 'error', 'paused')"
}
}
},
"preference": {
"name": "preference",
"columns": {
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'default'"
},
"key": {
"name": "key",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"preference_scope_key_pk": {
"columns": [
"scope",
"key"
],
"name": "preference_scope_key_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"tag": {
"name": "tag",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"color": {
"name": "color",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"tag_name_unique": {
"name": "tag_name_unique",
"columns": [
"name"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"topic": {
"name": "topic",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"assistant_id": {
"name": "assistant_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"assistant_meta": {
"name": "assistant_meta",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"prompt": {
"name": "prompt",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"group_id": {
"name": "group_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"is_pinned": {
"name": "is_pinned",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": false
},
"pinned_order": {
"name": "pinned_order",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
},
"sort_order": {
"name": "sort_order",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
},
"is_name_manually_edited": {
"name": "is_name_manually_edited",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"deleted_at": {
"name": "deleted_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"topic_group_updated_idx": {
"name": "topic_group_updated_idx",
"columns": [
"group_id",
"updated_at"
],
"isUnique": false
},
"topic_group_sort_idx": {
"name": "topic_group_sort_idx",
"columns": [
"group_id",
"sort_order"
],
"isUnique": false
},
"topic_updated_at_idx": {
"name": "topic_updated_at_idx",
"columns": [
"updated_at"
],
"isUnique": false
},
"topic_is_pinned_idx": {
"name": "topic_is_pinned_idx",
"columns": [
"is_pinned",
"pinned_order"
],
"isUnique": false
},
"topic_assistant_id_idx": {
"name": "topic_assistant_id_idx",
"columns": [
"assistant_id"
],
"isUnique": false
}
},
"foreignKeys": {
"topic_group_id_group_id_fk": {
"name": "topic_group_id_group_id_fk",
"tableFrom": "topic",
"tableTo": "group",
"columnsFrom": [
"group_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@ -8,6 +8,13 @@
"when": 1766588456958,
"tag": "0000_init",
"breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1766670360754,
"tag": "0001_faulty_ogun",
"breakpoints": true
}
]
}
}

View File

@ -75,7 +75,7 @@
"format:check": "biome format && biome lint",
"prepare": "git config blame.ignoreRevsFile .git-blame-ignore-revs && husky",
"claude": "dotenv -e .env -- claude",
"migrations:generate": "drizzle-kit generate --config ./migrations/sqlite-drizzle.config.ts",
"db:migrations:generate": "drizzle-kit generate --config ./migrations/sqlite-drizzle.config.ts",
"release:aicore:alpha": "yarn workspace @cherrystudio/ai-core version prerelease --preid alpha --immediate && yarn workspace @cherrystudio/ai-core build && yarn workspace @cherrystudio/ai-core npm publish --tag alpha --access public",
"release:aicore:beta": "yarn workspace @cherrystudio/ai-core version prerelease --preid beta --immediate && yarn workspace @cherrystudio/ai-core build && yarn workspace @cherrystudio/ai-core npm publish --tag beta --access public",
"release:aicore": "yarn workspace @cherrystudio/ai-core version patch --immediate && yarn workspace @cherrystudio/ai-core build && yarn workspace @cherrystudio/ai-core npm publish --access public",

View File

@ -7,25 +7,31 @@ This directory contains shared type definitions and schemas for the Cherry Studi
```
packages/shared/data/
├── api/ # Data API type system
│ ├── index.ts # Barrel exports for clean imports
│ ├── apiSchemas.ts # API endpoint definitions and mappings
│ ├── apiTypes.ts # Core request/response infrastructure types
│ ├── apiModels.ts # Business entity types and DTOs
│ ├── apiPaths.ts # API path definitions and utilities
│ └── errorCodes.ts # Standardized error handling
│ ├── index.ts # Barrel exports for clean imports
│ ├── apiSchemas.ts # API endpoint definitions and mappings
│ ├── apiTypes.ts # Core request/response infrastructure types
│ ├── apiModels.ts # Business entity types and DTOs
│ ├── apiPaths.ts # API path definitions and utilities
│ └── errorCodes.ts # Standardized error handling
├── cache/ # Cache system type definitions
│ ├── cacheTypes.ts # Core cache infrastructure types
│ ├── cacheSchemas.ts # Cache key schemas and type mappings
│ └── cacheValueTypes.ts # Cache value type definitions
│ ├── cacheTypes.ts # Core cache infrastructure types
│ ├── cacheSchemas.ts # Cache key schemas and type mappings
│ └── cacheValueTypes.ts # Cache value type definitions
├── preference/ # Preference system type definitions
│ ├── preferenceTypes.ts # Core preference system types
│ ├── preferenceTypes.ts # Core preference system types
│ └── preferenceSchemas.ts # Preference schemas and default values
└── README.md # This file
├── types/ # Shared data types for Main/Renderer
└── README.md # This file
```
## 🏗️ System Overview
This directory provides type definitions for three main data management systems:
This directory provides type definitions for four main data management systems:
### Types System (`types/`)
- **Purpose**: Shared data types for cross-process (Main/Renderer) communication and database schemas
- **Features**: Database table field types, business entity definitions
- **Usage**: Used in Drizzle ORM schemas via `.$type<T>()` and runtime type checking
### API System (`api/`)
- **Purpose**: Type-safe IPC communication between Main and Renderer processes
@ -72,6 +78,11 @@ import type { PreferenceKeyType, PreferenceDefaultScopeType } from '@shared/data
## 🔧 Development Guidelines
### Adding Shared Types
1. Create or update type file in `types/` directory
2. Use camelCase for field names
3. Reference types in Drizzle schemas using `.$type<T>()`
### Adding Cache Types
1. Add cache key to `cache/cacheSchemas.ts`
2. Define value type in `cache/cacheValueTypes.ts`

View File

@ -0,0 +1,171 @@
/**
* Message Statistics - combines token usage and performance metrics
* Replaces the separate `usage` and `metrics` fields
*/
export interface MessageStats {
// Token consumption (from API response)
promptTokens?: number
completionTokens?: number
totalTokens?: number
thoughtsTokens?: number
// Cost (calculated at message completion time)
cost?: number
// Performance metrics (measured locally)
timeFirstTokenMs?: number
timeCompletionMs?: number
timeThinkingMs?: number
}
// ============================================================================
// Message Data
// ============================================================================
/**
* Message data field structure
* This is the type for the `data` column in the message table
*/
export interface MessageData {
blocks: MessageDataBlock[]
}
//FIXME [v2] 注意,以下类型只是占位,接口未稳定,随时会变
// ============================================================================
// Message Block
// ============================================================================
export enum BlockType {
UNKNOWN = 'unknown',
MAIN_TEXT = 'main_text',
THINKING = 'thinking',
TRANSLATION = 'translation',
IMAGE = 'image',
CODE = 'code',
TOOL = 'tool',
FILE = 'file',
ERROR = 'error',
CITATION = 'citation',
VIDEO = 'video',
COMPACT = 'compact'
}
/**
* Base message block data structure
*/
export interface BaseBlock {
type: BlockType
createdAt: number // timestamp
updatedAt?: number
modelId?: string
metadata?: Record<string, unknown>
error?: SerializedErrorData
}
/**
* Serialized error for storage
*/
export interface SerializedErrorData {
name?: string
message: string
code?: string
stack?: string
cause?: unknown
}
// Block type specific interfaces
export interface UnknownBlock extends BaseBlock {
type: BlockType.UNKNOWN
content?: string
}
export interface MainTextBlock extends BaseBlock {
type: BlockType.MAIN_TEXT
content: string
knowledgeBaseIds?: string[]
citationReferences?: {
citationBlockId?: string
citationBlockSource?: string
}[]
}
export interface ThinkingBlock extends BaseBlock {
type: BlockType.THINKING
content: string
thinkingMs: number
}
export interface TranslationBlock extends BaseBlock {
type: BlockType.TRANSLATION
content: string
sourceBlockId?: string
sourceLanguage?: string
targetLanguage: string
}
export interface CodeBlock extends BaseBlock {
type: BlockType.CODE
content: string
language: string
}
export interface ImageBlock extends BaseBlock {
type: BlockType.IMAGE
url?: string
fileId?: string
}
export interface ToolBlock extends BaseBlock {
type: BlockType.TOOL
toolId: string
toolName?: string
arguments?: Record<string, unknown>
content?: string | object
}
export interface CitationBlock extends BaseBlock {
type: BlockType.CITATION
responseData?: unknown
knowledgeData?: unknown
memoriesData?: unknown
}
export interface FileBlock extends BaseBlock {
type: BlockType.FILE
fileId: string
}
export interface VideoBlock extends BaseBlock {
type: BlockType.VIDEO
url?: string
filePath?: string
}
export interface ErrorBlock extends BaseBlock {
type: BlockType.ERROR
}
export interface CompactBlock extends BaseBlock {
type: BlockType.COMPACT
content: string
compactedContent: string
}
/**
* Union type of all message block data types
*/
export type MessageDataBlock =
| UnknownBlock
| MainTextBlock
| ThinkingBlock
| TranslationBlock
| CodeBlock
| ImageBlock
| ToolBlock
| CitationBlock
| FileBlock
| VideoBlock
| ErrorBlock
| CompactBlock

View File

@ -239,7 +239,7 @@ import { dataApiService } from '@/data/DataApiService'
### Adding Database Tables
1. Create schema in `db/schemas/{tableName}.ts`
2. Generate migration: `yarn run migrations:generate`
2. Generate migration: `yarn run db:migrations:generate`
3. Add seeding data in `db/seeding/` if needed
4. Decide: Repository pattern or direct Drizzle?
- Complex domain → Create repository in `repositories/`

View File

@ -1,3 +1,4 @@
import type { MessageData, MessageStats } from '@shared/data/types/message'
import { sql } from 'drizzle-orm'
import { check, index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
@ -37,11 +38,9 @@ export const messageTable = sqliteTable(
// Preserved model info (provider, name)
modelMeta: text({ mode: 'json' }),
// Main content - contains blocks[], mentions, etc.
data: text({ mode: 'json' }).notNull(),
// Token usage statistics
usage: text({ mode: 'json' }),
// Performance metrics
metrics: text({ mode: 'json' }),
data: text({ mode: 'json' }).$type<MessageData>().notNull(),
// Statistics: token usage, performance metrics, etc.
stats: text({ mode: 'json' }).$type<MessageStats>(),
// Trace ID for tracking
traceId: text(),
// Searchable text extracted from data.blocks (populated by trigger, used for FTS5)

View File

@ -1,3 +1,5 @@
//TODO [v2] 类型将转移至 packages/shared/data/types/message.ts。 转移后此文件将废弃(deprecated)
import type { CompletionUsage } from '@cherrystudio/openai/resources'
import type { ProviderMetadata } from 'ai'