visuddhinanda 6 месяцев назад
Родитель
Сommit
9f3bc30a06

+ 115 - 0
api-v8/app/Http/Controllers/ChatController.php

@@ -0,0 +1,115 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use App\Http\Requests\StoreChatRequest;
+use App\Http\Requests\UpdateChatRequest;
+use App\Models\Chat;
+use App\Http\Resources\ChatResource;
+
+class ChatController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        $query = Chat::query();
+
+        if ($request->has('user_id')) {
+            $query->where('user_id', $request->user_id);
+        }
+
+        $total = $query->count();
+
+        $chats = $query->orderBy('updated_at', 'desc')
+            ->paginate($request->get('limit', 20));
+
+        return $this->ok([
+            'rows' => ChatResource::collection($chats),
+            'total' => $total
+        ]);
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \App\Http\Requests\StoreChatRequest  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(StoreChatRequest $request)
+    {
+        $chat = Chat::create($request->validated());
+
+        return $this->ok(new ChatResource($chat));
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\Chat  $chat
+     * @return \Illuminate\Http\Response
+     */
+    public function show(Chat $chat)
+    {
+        return $this->ok(new ChatResource($chat));
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \App\Http\Requests\UpdateChatRequest  $request
+     * @param  \App\Models\Chat  $chat
+     * @return \Illuminate\Http\Response
+     */
+    public function update(UpdateChatRequest $request, Chat $chat)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \App\Models\Chat  $chat
+     * @return \Illuminate\Http\Response
+     */
+    /**
+     * 单个软删除
+     */
+    public function destroy(Chat $chat)
+    {
+        $chat->delete(); // 软删除
+        return $this->ok('Chat deleted successfully.');
+    }
+
+    /**
+     * 批量软删除
+     */
+    public function batchDelete(Request $request)
+    {
+        $chatIds = $request->input('uids', []); // 前端传入数组
+        $count = Chat::batchSoftDelete($chatIds);
+
+        return $this->ok([
+            'message' => "Chats soft deleted successfully.",
+            'deleted_count' => $count
+        ]);
+    }
+
+    /**
+     * 批量恢复
+     */
+    public function batchRestore(Request $request)
+    {
+        $chatIds = $request->input('uids', []); // 前端传入数组
+        $count = Chat::batchRestore($chatIds);
+
+        return $this->ok([
+            'message' => "Chats restored successfully.",
+            'restored_count' => $count
+        ]);
+    }
+}

+ 106 - 0
api-v8/app/Http/Controllers/ChatMessageController.php

@@ -0,0 +1,106 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Http\Requests\StoreChatMessageRequest;
+use App\Http\Requests\UpdateChatMessageRequest;
+use App\Models\ChatMessage;
+use App\Http\Resources\ChatMessageResource;
+use App\Models\Chat;
+use Illuminate\Http\Request;
+use Illuminate\Support\Str;
+
+class ChatMessageController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index(Request $request)
+    {
+        //
+        $query = ChatMessage::where('chat_id', $request->get('chat'));
+
+        $total = $query->count();
+
+        $messages = $query->orderBy('id')->paginate($request->get('limit', 500));
+
+        return $this->ok([
+            'data' => ChatMessageResource::collection($messages),
+            'total' => $total
+        ]);
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \App\Http\Requests\StoreChatMessageRequest  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(StoreChatMessageRequest $request)
+    {
+
+        $messagesData = $request->validated()['messages'];
+        $chatId = $request->validated()['chat_id'];
+
+        $created = [];
+        foreach ($messagesData as $key => $data) {
+            $data['chat_id'] = $chatId;
+            $data['uid'] = (string) Str::uuid();
+
+            // 如果是新消息且没有指定session_id,创建新的session
+            if (empty($data['session_id']) && empty($data['parent_id'])) {
+                $data['session_id'] = (string) Str::uuid();
+            }
+            // 如果有parent_id但没有session_id,继承父消息的session_id
+            elseif (empty($data['session_id']) && !empty($data['parent_id'])) {
+                $parent = ChatMessage::where('uid', $data['parent_id'])->first();
+                if ($parent) {
+                    $data['session_id'] = $parent->session_id;
+                }
+            }
+
+            $created[] = ChatMessage::create($data);
+        }
+
+        return $this->ok([
+            'data' => ChatMessageResource::collection($created),
+            'total' => count($created),
+        ]);
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\Models\ChatMessage  $chatMessage
+     * @return \Illuminate\Http\Response
+     */
+    public function show(ChatMessage $chatMessage)
+    {
+        return $this->ok(new ChatMessageResource($chatMessage));
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \App\Http\Requests\UpdateChatMessageRequest  $request
+     * @param  \App\Models\ChatMessage  $chatMessage
+     * @return \Illuminate\Http\Response
+     */
+    public function update(UpdateChatMessageRequest $request, ChatMessage $chatMessage)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \App\Models\ChatMessage  $chatMessage
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(ChatMessage $chatMessage)
+    {
+        //
+    }
+}

+ 30 - 0
api-v8/app/Http/Requests/ChatMessageRequest.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class ChatMessageRequest extends FormRequest
+{
+    /**
+     * Determine if the user is authorized to make this request.
+     *
+     * @return bool
+     */
+    public function authorize()
+    {
+        return false;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     *
+     * @return array
+     */
+    public function rules()
+    {
+        return [
+            //
+        ];
+    }
+}

+ 30 - 0
api-v8/app/Http/Requests/ChatRequest.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class ChatRequest extends FormRequest
+{
+    /**
+     * Determine if the user is authorized to make this request.
+     *
+     * @return bool
+     */
+    public function authorize()
+    {
+        return false;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     *
+     * @return array
+     */
+    public function rules()
+    {
+        return [
+            //
+        ];
+    }
+}

+ 51 - 0
api-v8/app/Http/Requests/StoreChatMessageRequest.php

@@ -0,0 +1,51 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Validation\Rule;
+
+class StoreChatMessageRequest extends FormRequest
+{
+    /**
+     * Determine if the user is authorized to make this request.
+     *
+     * @return bool
+     */
+    public function authorize()
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     *
+     * @return array
+     */
+    public function rules()
+    {
+        return [
+            'chat_id' => 'required|string|size:36',
+            'messages' => 'required|array|min:1',
+            'messages.*.parent_id' => 'nullable|string|exists:chat_messages,uid',
+            'messages.*.session_id' => 'nullable|string|size:36',
+            'messages.*.role' => ['required', Rule::in(['user', 'assistant', 'tool'])],
+            'messages.*.content' => 'nullable|string',
+            'messages.*.model_name' => 'nullable|string|max:100',
+            'messages.*.tool_calls' => 'nullable|array',
+            'messages.*.tool_calls.*.id' => 'required_with:tool_calls|string',
+            'messages.*.tool_calls.*.function' => 'required_with:tool_calls|string',
+            'messages.*.tool_calls.*.arguments' => 'required_with:tool_calls',
+            'messages.*.tool_call_id' => 'nullable|string|max:100'
+        ];
+    }
+
+    public function messages()
+    {
+        return [
+            'messages.required' => '批量更新的消息列表不能为空',
+            'messages.*.parent_id.required' => '消息ID不能为空',
+            'messages.*.parent_id.exists' => '消息不存在'
+        ];
+    }
+}

+ 30 - 0
api-v8/app/Http/Requests/StoreChatRequest.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class StoreChatRequest extends FormRequest
+{
+    public function authorize()
+    {
+        return true;
+    }
+
+    public function rules()
+    {
+        return [
+            'title' => 'required|string|max:255',
+            'user_id' => 'required|string|exists:user_infos,userid'
+        ];
+    }
+
+    public function messages()
+    {
+        return [
+            'title.required' => '聊天标题不能为空',
+            'title.max' => '聊天标题不能超过255个字符',
+            'user_id.exists' => '用户不存在'
+        ];
+    }
+}

+ 32 - 0
api-v8/app/Http/Requests/UpdateChatMessageRequest.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class UpdateChatMessageRequest extends FormRequest
+{
+    /**
+     * Determine if the user is authorized to make this request.
+     *
+     * @return bool
+     */
+    public function authorize()
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     *
+     * @return array
+     */
+    public function rules()
+    {
+        return [
+            'content' => 'sometimes|string',
+            'is_active' => 'sometimes|boolean',
+            'model_name' => 'sometimes|string|max:100'
+        ];
+    }
+}

+ 30 - 0
api-v8/app/Http/Requests/UpdateChatRequest.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class UpdateChatRequest extends FormRequest
+{
+    /**
+     * Determine if the user is authorized to make this request.
+     *
+     * @return bool
+     */
+    public function authorize()
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     *
+     * @return array
+     */
+    public function rules()
+    {
+        return [
+            'title' => 'sometimes|string|max:255'
+        ];
+    }
+}

+ 32 - 0
api-v8/app/Http/Resources/ChatMessageResource.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace App\Http\Resources;
+
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class ChatMessageResource extends JsonResource
+{
+    /**
+     * Transform the resource into an array.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
+     */
+    public function toArray($request)
+    {
+        return [
+            'id' => $this->uid,
+            'chat_id' => $this->chat_id,
+            'parent_id' => $this->parent_id,
+            'session_id' => $this->session_id,
+            'role' => $this->role,
+            'content' => $this->content,
+            'model_name' => $this->model_name,
+            'tool_calls' => $this->tool_calls,
+            'tool_call_id' => $this->tool_call_id,
+            'is_active' => $this->is_active,
+            'created_at' => $this->created_at,
+            'updated_at' => $this->updated_at,
+        ];
+    }
+}

+ 28 - 0
api-v8/app/Http/Resources/ChatResource.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Http\Resources;
+
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class ChatResource extends JsonResource
+{
+    /**
+     * Transform the resource into an array.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
+     */
+    public function toArray($request)
+    {
+        return [
+            'id' => $this->uid,
+            'title' => $this->title,
+            'user_id' => $this->user_id,
+            'created_at' => $this->created_at,
+            'updated_at' => $this->updated_at,
+            'messages_count' => $this->when($this->relationLoaded('messages'), function () {
+                return $this->messages->count();
+            }),
+        ];
+    }
+}

+ 106 - 0
api-v8/app/Models/Chat.php

@@ -0,0 +1,106 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Str;
+
+class Chat extends Model
+{
+    use HasFactory;
+
+    protected $fillable = [
+        'uid',
+        'title',
+        'user_id'
+    ];
+
+    protected $casts = [
+        'id' => 'string',
+        'uid' => 'string',
+        'created_at' => 'datetime',
+        'updated_at' => 'datetime',
+        'deleted_at' => 'datetime',
+    ];
+    protected $primaryKey = 'uid';
+    protected static function boot()
+    {
+        parent::boot();
+
+        static::creating(function ($model) {
+            if (empty($model->uid)) {
+                $model->uid = (string) Str::uuid();
+            }
+        });
+
+        // 级联软删除:删除Chat时同时软删除所有相关消息
+        static::deleting(function ($chat) {
+            if ($chat->isForceDeleting()) {
+                // 强制删除时,物理删除所有消息
+                $chat->messages()->forceDelete();
+            } else {
+                // 软删除时,级联软删除所有消息
+                $chat->messages()->delete();
+            }
+        });
+
+        // 恢复Chat时同时恢复所有相关消息
+        static::restoring(function ($chat) {
+            $chat->messages()->withTrashed()->restore();
+        });
+    }
+    // 使用 uid 作为路由键
+    public function messages()
+    {
+        return $this->hasMany(ChatMessage::class);
+    }
+
+    public function getRouteKeyName()
+    {
+        return 'uid';
+    }
+
+    /**
+     * 获取根消息(没有parent_id的消息)
+     */
+    public function rootMessages()
+    {
+        return $this->hasMany(ChatMessage::class, 'chat_id', 'uid')
+            ->whereNull('parent_id')
+            ->where('is_active', true);
+    }
+
+    /**
+     * 获取token使用统计
+     */
+    public function getTokenStats()
+    {
+        return $this->messages()
+            ->where('role', 'assistant')
+            ->whereNotNull('metadata')
+            ->selectRaw('
+                       SUM(JSON_EXTRACT(metadata, "$.token_usage.total_tokens")) as total_tokens,
+                       SUM(JSON_EXTRACT(metadata, "$.token_usage.prompt_tokens")) as prompt_tokens,
+                       SUM(JSON_EXTRACT(metadata, "$.token_usage.completion_tokens")) as completion_tokens,
+                       AVG(JSON_EXTRACT(metadata, "$.performance.response_time_ms")) as avg_response_time
+                   ')
+            ->first();
+    }
+
+    /**
+     * 批量软删除多个聊天
+     */
+    public static function batchSoftDelete(array $chatIds): int
+    {
+        return static::whereIn('uid', $chatIds)->delete();
+    }
+
+    /**
+     * 批量恢复多个聊天
+     */
+    public static function batchRestore(array $chatIds): int
+    {
+        return static::withTrashed()->whereIn('uid', $chatIds)->restore();
+    }
+}

+ 186 - 0
api-v8/app/Models/ChatMessage.php

@@ -0,0 +1,186 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Str;
+
+class ChatMessage extends Model
+{
+    use HasFactory;
+
+    protected $fillable = [
+        'uid',
+        'chat_id',
+        'parent_id',
+        'session_id',
+        'role',
+        'content',
+        'model_name',
+        'tool_calls',
+        'tool_call_id',
+        'is_active'
+    ];
+    protected $dates = [
+        'deleted_at'
+    ];
+    protected $casts = [
+        'id' => 'string',
+        'tool_calls' => 'array',
+        'is_active' => 'boolean',
+        'created_at' => 'datetime',
+        'updated_at' => 'datetime',
+        'deleted_at' => 'datetime',
+    ];
+
+    protected static function boot()
+    {
+        parent::boot();
+
+        static::creating(function ($model) {
+            if (empty($model->uid)) {
+                $model->uid = (string) Str::uuid();
+            }
+            if (empty($model->session_id)) {
+                $model->session_id = (string) Str::uuid();
+            }
+        });
+    }
+    /**
+     * 关联聊天
+     */
+    public function chat()
+    {
+        return $this->belongsTo(Chat::class);
+    }
+    /**
+     * 关联父消息
+     */
+    public function parent()
+    {
+        return $this->belongsTo(ChatMessage::class, 'parent_id', 'uid');
+    }
+    /**
+     * 关联子消息
+     */
+    public function children()
+    {
+        return $this->hasMany(ChatMessage::class, 'parent_id', 'uid');
+    }
+    /**
+     * 关联激活状态的子消息
+     */
+    public function activeChildren()
+    {
+        return $this->hasMany(ChatMessage::class, 'parent_id', 'uid')
+            ->where('is_active', true);
+    }
+    /**
+     * Scope: 只查询激活状态的消息
+     */
+    public function scopeActive($query)
+    {
+        return $query->where('is_active', true);
+    }
+
+    /**
+     * Scope: 根据角色查询
+     */
+    public function scopeByRole($query, string $role)
+    {
+        return $query->where('role', $role);
+    }
+
+    /**
+     * Scope: 根据会话ID查询
+     */
+    public function scopeBySession($query, string $sessionId)
+    {
+        return $query->where('session_id', $sessionId);
+    }
+
+    /**
+     * Scope: 根据聊天ID查询
+     */
+    public function scopeByChat($query, string $chatId)
+    {
+        return $query->where('chat_id', $chatId);
+    }
+
+    /**
+     * 获取消息路径(从根到当前消息)
+     */
+    public function getPath(): array
+    {
+        $path = [];
+        $current = $this;
+
+        while ($current) {
+            array_unshift($path, $current);
+            $current = $current->parent;
+        }
+
+        return $path;
+    }
+
+    /**
+     * 获取消息树的所有叶子节点
+     */
+    public function getLeaves(): \Illuminate\Database\Eloquent\Collection
+    {
+        return ChatMessage::where('chat_id', $this->chat_id)
+            ->whereNotNull('parent_id')
+            ->whereDoesntHave('children')
+            ->get();
+    }
+
+    /**
+     * 获取Token使用情况
+     */
+    public function getTokenUsage(): ?array
+    {
+        return $this->metadata['token_usage'] ?? null;
+    }
+
+    /**
+     * 获取生成参数
+     */
+    public function getGenerationParams(): ?array
+    {
+        return $this->metadata['generation_params'] ?? null;
+    }
+
+    /**
+     * 设置Token使用情况
+     */
+    public function setTokenUsage(array $tokenUsage): void
+    {
+        $metadata = $this->metadata ?? [];
+        $metadata['token_usage'] = $tokenUsage;
+        $this->update(['metadata' => $metadata]);
+    }
+
+    /**
+     * 设置生成参数
+     */
+    public function setGenerationParams(array $params): void
+    {
+        $metadata = $this->metadata ?? [];
+        $metadata['generation_params'] = $params;
+        $this->update(['metadata' => $metadata]);
+    }
+
+    public function siblings()
+    {
+        return $this->where('parent_id', $this->parent_id)
+            ->where('role', $this->role)
+            ->where('id', '!=', $this->id);
+    }
+
+
+    public function getRouteKeyName()
+    {
+        return 'uid';
+    }
+}

+ 24 - 0
api-v8/database/factories/ChatFactory.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace Database\Factories;
+
+use App\Models\Chat;
+use Illuminate\Database\Eloquent\Factories\Factory;
+use Illuminate\Support\Str;
+
+class ChatFactory extends Factory
+{
+    /**
+     * Define the model's default state.
+     *
+     * @return array
+     */
+    public function definition()
+    {
+        return [
+            'uid' => (string) Str::uuid(),
+            'title' => $this->faker->sentence(3),
+            'user_id' => 'ba5463f3-72d1-4410-858e-eadd10884713', // 或者 User::factory() 如果有用户系统
+        ];
+    }
+}

+ 123 - 0
api-v8/database/factories/ChatMessageFactory.php

@@ -0,0 +1,123 @@
+<?php
+
+namespace Database\Factories;
+
+use App\Models\Chat;
+use App\Models\ChatMessage;
+use Illuminate\Database\Eloquent\Factories\Factory;
+use Illuminate\Support\Str;
+
+class ChatMessageFactory extends Factory
+{
+    /**
+     * Define the model's default state.
+     *
+     * @return array
+     */
+    public function definition()
+    {
+        return [
+            'uid' => (string) Str::uuid(),
+            'chat_id' => Chat::factory(),
+            'parent_id' => null,
+            'session_id' => (string) Str::uuid(),
+            'role' => $this->faker->randomElement(['user', 'assistant', 'tool']),
+            'content' => $this->faker->paragraph,
+            'model_id' => null,
+            'tool_calls' => null,
+            'tool_call_id' => null,
+            'is_active' => true,
+        ];
+    }
+
+    public function user()
+    {
+        return $this->state(function (array $attributes) {
+            return [
+                'role' => 'user',
+                'content' => $this->faker->sentence,
+                'model_name' => null,
+                'tool_calls' => null,
+                'tool_call_id' => null,
+            ];
+        });
+    }
+
+    public function assistant()
+    {
+        return $this->state(function (array $attributes) {
+            return [
+                'role' => 'assistant',
+                'content' => $this->faker->paragraph,
+                'model_name' => $this->faker->randomElement(['gpt-4', 'gpt-3.5-turbo', 'claude-3']),
+                'tool_calls' => null,
+                'tool_call_id' => null,
+            ];
+        });
+    }
+
+    public function tool()
+    {
+        return $this->state(function (array $attributes) {
+            return [
+                'role' => 'tool',
+                'content' => $this->faker->paragraph,
+                'model_name' => null,
+                'tool_calls' => null,
+                'tool_call_id' => 'call_' . $this->faker->randomNumber(8),
+            ];
+        });
+    }
+
+    public function withToolCalls()
+    {
+        return $this->state(function (array $attributes) {
+            return [
+                'role' => 'assistant',
+                'content' => null,
+                'tool_calls' => [
+                    [
+                        'id' => 'call_' . $this->faker->randomNumber(8),
+                        'function' => $this->faker->randomElement(['get_weather', 'search_web', 'calculate']),
+                        'arguments' => [
+                            'query' => $this->faker->sentence,
+                            'city' => $this->faker->city
+                        ]
+                    ]
+                ]
+            ];
+        });
+    }
+
+    public function inactive()
+    {
+        return $this->state(function (array $attributes) {
+            return [
+                'is_active' => false,
+            ];
+        });
+    }
+
+    public function withMetadata($metadata = null)
+    {
+        return $this->state(function (array $attributes) use ($metadata) {
+            return [
+                'metadata' => $metadata ?: [
+                    'temperature' => $this->faker->randomFloat(2, 0, 2),
+                    'tokens' => $this->faker->numberBetween(50, 500),
+                    'max_tokens' => $this->faker->numberBetween(500, 2000),
+                    'model_version' => $this->faker->randomElement(['1.0', '1.1', '2.0'])
+                ],
+            ];
+        });
+    }
+
+    public function withEditor($editorId = null)
+    {
+        return $this->state(function (array $attributes) use ($editorId) {
+            return [
+                'editor_id' => $editorId ?: (string) Str::uuid(),
+            ];
+        });
+    }
+}

+ 39 - 0
api-v8/database/migrations/2025_09_07_154047_create_chats_table.php

@@ -0,0 +1,39 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreateChatsTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('chats', function (Blueprint $table) {
+            $table->id();
+            $table->uuid('uid')->unique()->comment('UUID唯一标识');
+            $table->string('title')->comment('聊天标题');
+            $table->uuid('user_id')->comment('用户ID');
+            $table->timestamps();
+            $table->softDeletes(); // deleted_at
+
+            $table->index('uid');
+            $table->index('user_id');
+            $table->index('created_at');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('chats');
+    }
+}

+ 54 - 0
api-v8/database/migrations/2025_09_07_154243_create_chat_messages_table.php

@@ -0,0 +1,54 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreateChatMessagesTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('chat_messages', function (Blueprint $table) {
+            $table->id();
+            $table->uuid('uid')->unique()->comment('UUID唯一标识');
+            $table->uuid('chat_id')->comment('关联chats.uid');
+            $table->uuid('parent_id')->nullable()->comment('关联chat_messages.uid');
+            $table->uuid('session_id')->comment('会话段ID');
+
+            $table->enum('role', ['system', 'user', 'assistant', 'tool'])->comment('消息角色');
+            $table->text('content')->nullable()->comment('消息内容');
+            $table->uuid('model_id')->nullable()->comment('使用的模型UUID');
+            $table->json('tool_calls')->nullable()->comment('函数调用信息');
+            $table->string('tool_call_id', 100)->nullable()->comment('工具调用ID');
+
+            $table->json('metadata')->nullable()->comment('元数据信息(temperature, tokens等)');
+            $table->uuid('editor_id')->nullable()->comment('编辑者UUID');
+
+            $table->boolean('is_active')->default(true)->comment('是否为当前激活版本');
+            $table->timestamps();
+            $table->softDeletes(); // deleted_at
+
+
+            $table->index('uid');
+            $table->index('chat_id');
+            $table->index('parent_id');
+            $table->index('session_id');
+            $table->index(['chat_id', 'is_active']);
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('chat_messages');
+    }
+}

+ 89 - 0
api-v8/tests/Feature/ChatControllerTest.php

@@ -0,0 +1,89 @@
+<?php
+
+namespace Tests\Feature;
+
+use App\Models\Chat;
+use App\Models\ChatMessage;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Foundation\Testing\WithFaker;
+use Tests\TestCase;
+
+class ChatControllerTest extends TestCase
+{
+    use  WithFaker;
+
+    public function test_can_create_chat()
+    {
+        $data = [
+            'title' => $this->faker->sentence,
+            'user_id' => "ba5463f3-72d1-4410-858e-eadd10884713"
+        ];
+
+        $response = $this->postJson('/api/v2/chats', $data);
+
+        $response->assertStatus(200)
+            ->assertJsonStructure([
+                'data' => [
+                    'id',
+                    'title',
+                    'user_id',
+                    'created_at',
+                    'updated_at'
+                ]
+            ]);
+
+        $this->assertDatabaseHas('chats', [
+            'title' => $data['title']
+        ]);
+    }
+
+    public function test_can_list_chats()
+    {
+        Chat::factory()->count(1)->create();
+
+        $response = $this->getJson('/api/v2/chats?user_id=ba5463f3-72d1-4410-858e-eadd10884713');
+
+        $response->assertStatus(200)
+            ->assertJsonStructure([
+                'data' => [
+                    'rows' => [
+                        '*' => [
+                            'id',
+                            'title',
+                            'created_at',
+                            'updated_at'
+                        ]
+                    ]
+                ]
+            ]);
+    }
+
+    public function test_can_show_chat()
+    {
+        $chat = Chat::factory()->create();
+
+        $response = $this->getJson("/api/v2/chats/{$chat->uid}");
+
+        $response->assertStatus(200)
+            ->assertJson([
+                'data' => [
+                    'id' => $chat->uid,
+                    'title' => $chat->title
+                ]
+            ]);
+    }
+
+
+
+    public function test_can_delete_chat()
+    {
+        $chat = Chat::factory()->create();
+
+        $response = $this->deleteJson("/api/chats/{$chat->uid}");
+
+        $response->assertStatus(200);
+        $this->assertDatabaseMissing('chats', [
+            'id' => $chat->id
+        ]);
+    }
+}

+ 56 - 0
api-v8/tests/Feature/ChatMessageControllerTest.php

@@ -0,0 +1,56 @@
+<?php
+
+namespace Tests\Feature;
+
+use App\Models\Chat;
+use App\Models\ChatMessage;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Foundation\Testing\WithFaker;
+use Tests\TestCase;
+
+class ChatMessageControllerTest extends TestCase
+{
+    use  WithFaker;
+
+    protected $chat;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+        $this->chat = Chat::factory()->create();
+    }
+
+    public function test_can_create_chat_message()
+    {
+        $data = [
+            'chat_id' => $this->chat->uid,
+            'messages' => [
+                [
+                    'role' => 'user',
+                    'content' => $this->faker->sentence
+                ]
+            ]
+        ];
+
+        $response = $this->postJson("/api/v2/chat-messages", $data);
+
+        $response->assertStatus(200);
+
+        $this->assertDatabaseHas('chat_messages', [
+            'chat_id' => $this->chat->uid,
+            'role' => 'user',
+            'content' => $data['messages'][0]['content']
+        ]);
+    }
+
+    public function test_can_list_chat_messages()
+    {
+        ChatMessage::factory()->count(3)->create([
+            'chat_id' => $this->chat->id
+        ]);
+
+        $response = $this->getJson("/api/v2/chat-messages?chat={$this->chat->id}");
+
+        $response->assertStatus(200);
+    }
+}

+ 405 - 0
dashboard-v4/documents/development/chat/README.md

@@ -0,0 +1,405 @@
+# 聊天系统数据库设计文档
+
+## 1. 项目概述
+
+本项目旨在实现一个类似 Claude 的聊天系统,支持以下核心功能:
+
+- 多轮对话管理
+- Function Call 集成
+- 消息树结构(支持消息分支和版本控制)
+- 用户可修改问题并生成新的回答分支
+- 支持切换不同版本的回答
+- 支持更换模型重新回答同一问题
+- 消息生成参数跟踪(temperature、tokens 等)
+
+## 2. 核心设计理念
+
+### 2.1 消息组织结构
+
+- **Chat**: 每次点击"新建聊天"创建一个独立的消息树
+- **Session**: 将相关的消息组织成会话段,前端显示为一个消息组
+- **Message Tree**: 通过 parent_id 维护消息间的依赖关系
+- **Version Control**: 支持同一节点的多个版本,用户可切换查看
+
+### 2.2 Function Call 集成
+
+系统支持标准的 Function Call 流程:
+
+```text
+user → assistant(tool_calls) → tool(result) → assistant(final_response)
+```
+
+所有相关消息归属同一个 session,前端显示为一个完整的 AI 回复。
+
+### 2.3 软删除机制
+
+系统采用软删除策略,保护用户数据安全:
+
+- 所有删除操作仅标记 `deleted_at` 字段
+- 查询时默认过滤已删除数据
+- 支持数据恢复和审计需求
+
+## 3. 数据库表设计
+
+### 3.1 chats 表(聊天会话)
+
+存储每个独立的聊天会话。
+
+```sql
+CREATE TABLE chats (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    uid CHAR(36) NOT NULL UNIQUE COMMENT 'UUID唯一标识',
+    title VARCHAR(255) NOT NULL COMMENT '聊天标题',
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    deleted_at TIMESTAMP NULL COMMENT '软删除时间戳',
+    user_id CHAR(36) COMMENT '用户ID',
+
+    INDEX idx_chats_uid (uid),
+    INDEX idx_chats_user_id (user_id),
+    INDEX idx_chats_created_at (created_at),
+    INDEX idx_chats_deleted_at (deleted_at)
+) COMMENT '聊天会话表';
+```
+
+### 3.2 chat_messages 表(消息节点)
+
+存储所有消息,支持树形结构和版本控制。role=system 为消息树的唯一根节点。不可修改,不可删除。
+
+```sql
+CREATE TABLE chat_messages (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '自增主键,提供天然时间排序',
+    uid CHAR(36) NOT NULL UNIQUE COMMENT 'UUID唯一标识',
+    chat_id CHAR(36) NOT NULL COMMENT '关联chats.uid',
+    parent_id CHAR(36) COMMENT '关联chat_messages.uid,NULL表示根节点',
+    session_id CHAR(36) NOT NULL COMMENT '会话段ID,同一消息组共享',
+
+    role ENUM('system','user', 'assistant', 'tool') NOT NULL COMMENT '消息角色',
+    content TEXT COMMENT '消息内容',
+    model_id CHAR(36) COMMENT '使用的模型id(assistant消息)',
+    tool_calls JSON COMMENT '函数调用信息(assistant消息)',
+    tool_call_id VARCHAR(100) COMMENT '工具调用ID(tool消息)',
+    metadata JSON COMMENT '消息元数据:生成参数、token统计等',
+
+    is_active BOOLEAN DEFAULT TRUE COMMENT '是否为当前激活版本',
+    editor_id CHAR(36) COMMENT '最后编辑用户ID',
+    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    deleted_at TIMESTAMP NULL COMMENT '软删除时间戳',
+
+    INDEX idx_messages_uid (uid),
+    INDEX idx_messages_chat_id (chat_id),
+    INDEX idx_messages_parent_id (parent_id),
+    INDEX idx_messages_session_id (session_id),
+    INDEX idx_messages_active (chat_id, is_active),
+    INDEX idx_messages_deleted_at (deleted_at)
+) COMMENT '聊天消息表';
+```
+
+## 4. 数据结构说明
+
+### 4.1 字段详解
+
+#### chats 表字段
+
+- `id`: 自增主键,数据库内部使用
+- `uid`: UUID 标识,用于外部引用和 API
+- `title`: 聊天标题,可从首条消息自动生成
+- `user_id`: 用户标识(可选,如果需要多用户支持)
+- `deleted_at`: 软删除时间戳,NULL 表示未删除
+
+#### chat_messages 表字段
+
+- `id`: 自增主键,提供天然的消息时间顺序
+- `uid`: UUID 标识,用于 parent_id 引用,保证跨系统稳定性
+- `parent_id`: 指向父消息的 uid,构建技术依赖关系
+- `session_id`: 消息组标识,相同 session_id 的消息在前端显示为一组
+- `role`: 消息类型(user 用户/assistant 助手/tool 工具结果)
+- `tool_calls`: JSON 格式存储函数调用信息
+- `tool_call_id`: 关联 tool 消息到具体的函数调用
+- `metadata`: 消息生成相关参数和统计信息
+- `is_active`: 标记用户当前查看的版本
+- `deleted_at`: 软删除时间戳,NULL 表示未删除
+
+### 4.2 metadata 字段结构
+
+metadata 字段存储与消息生成相关的参数和统计信息:
+
+```json
+{
+  // AI生成参数(assistant消息)
+  "generation_params": {
+    "temperature": 0.7,
+    "max_tokens": 2048,
+    "top_p": 0.9,
+    "frequency_penalty": 0.0,
+    "presence_penalty": 0.0
+  },
+
+  // Token使用统计
+  "token_usage": {
+    "prompt_tokens": 150,
+    "completion_tokens": 320,
+    "total_tokens": 470
+  },
+
+  // 性能指标
+  "performance": {
+    "response_time_ms": 1250,
+    "first_token_time_ms": 450
+  },
+
+  // 工具调用统计(如适用)
+  "tool_stats": {
+    "total_calls": 2,
+    "successful_calls": 2,
+    "execution_time_ms": 340
+  },
+
+  // 其他扩展信息
+  "custom_data": {}
+}
+```
+
+### 4.3 消息关系说明
+
+#### parent_id 关系(技术依赖)
+
+维护 Function Call 的执行链:
+
+```text
+user(id=1)
+  ↓ parent_id=uuid1
+assistant(id=2, tool_calls=[...])
+  ↓ parent_id=uuid2
+tool(id=3, tool_call_id=xxx)
+  ↓ parent_id=uuid3
+assistant(id=4, final_response)
+```
+
+#### session_id 关系(显示分组)
+
+将相关消息组织成前端显示单元:
+
+```text
+Session 1: [user消息]
+Session 2: [assistant + tool + assistant] -> 显示为一个AI回复
+Session 3: [user消息]
+```
+
+## 5. 软删除实现
+
+### 5.1 删除操作
+
+```sql
+-- 软删除聊天(级联删除所有消息)
+UPDATE chats SET deleted_at = CURRENT_TIMESTAMP WHERE uid = ?;
+UPDATE chat_messages SET deleted_at = CURRENT_TIMESTAMP WHERE chat_id = ?;
+
+-- 软删除单个消息
+UPDATE chat_messages SET deleted_at = CURRENT_TIMESTAMP WHERE uid = ?;
+```
+
+### 5.2 查询过滤
+
+所有业务查询都需要过滤已删除数据:
+
+```sql
+-- 查询示例
+SELECT * FROM chats WHERE deleted_at IS NULL;
+SELECT * FROM chat_messages WHERE deleted_at IS NULL;
+```
+
+### 5.3 数据恢复
+
+```sql
+-- 恢复聊天
+UPDATE chats SET deleted_at = NULL WHERE uid = ?;
+
+-- 恢复消息
+UPDATE chat_messages SET deleted_at = NULL WHERE uid = ?;
+```
+
+## 6. 典型数据示例
+
+### 6.1 Function Call 对话示例
+
+```sql
+-- Chat记录
+INSERT INTO chats (id, uid, title) VALUES
+(1, 'chat-uuid-1', '天气查询对话');
+
+-- 消息记录
+INSERT INTO chat_messages (id, uid, chat_id, parent_id, session_id, role, content, metadata) VALUES
+-- Session 1: 用户提问
+(1, 'msg-uuid-1', 'chat-uuid-1', NULL, 'session-uuid-1', 'user', '查询今天天气', NULL),
+
+-- Session 2: AI处理流程
+(2, 'msg-uuid-2', 'chat-uuid-1', 'msg-uuid-1', 'session-uuid-2', 'assistant', NULL,
+ '{"generation_params":{"temperature":0.7,"max_tokens":2048},"token_usage":{"prompt_tokens":20,"completion_tokens":50,"total_tokens":70}}'),
+
+(3, 'msg-uuid-3', 'chat-uuid-1', 'msg-uuid-2', 'session-uuid-2', 'tool', '今天晴天,25°C',
+ '{"tool_stats":{"execution_time_ms":340}}'),
+
+(4, 'msg-uuid-4', 'chat-uuid-1', 'msg-uuid-3', 'session-uuid-2', 'assistant', '今天天气晴朗,气温25度...',
+ '{"generation_params":{"temperature":0.7},"token_usage":{"prompt_tokens":100,"completion_tokens":80,"total_tokens":180},"performance":{"response_time_ms":1250}}'),
+
+-- Session 3: 用户继续提问
+(5, 'msg-uuid-5', 'chat-uuid-1', 'msg-uuid-4', 'session-uuid-3', 'user', '那明天呢?', NULL);
+```
+
+### 6.2 tool_calls JSON 结构示例
+
+```json
+[
+  {
+    "id": "call_123",
+    "function": "get_weather",
+    "arguments": {
+      "city": "北京",
+      "date": "today"
+    }
+  }
+]
+```
+
+## 7. 核心业务场景
+
+### 7.1 用户修改问题
+
+1. 软删除原消息及其后续回答链
+2. 创建新版本消息
+3. 生成新的回答链,记录生成参数到 metadata
+
+### 7.2 重新生成回答
+
+1. 软删除当前 assistant 回答
+2. 使用相同或不同的参数重新生成
+3. 在 metadata 中记录新的生成参数和统计信息
+
+### 7.3 消息分支管理
+
+- 每个分支保持独立的 session_id
+- 通过 parent_id 维护分支关系
+- 软删除支持分支恢复
+
+## 8. 常用查询模式
+
+### 8.1 获取聊天的消息组列表
+
+```sql
+SELECT DISTINCT session_id, MIN(id) as first_message_id
+FROM chat_messages
+WHERE chat_id = ? AND is_active = true AND deleted_at IS NULL
+ORDER BY first_message_id;
+```
+
+### 8.2 获取某个 session 的所有消息
+
+```sql
+SELECT * FROM chat_messages
+WHERE session_id = ? AND is_active = true AND deleted_at IS NULL
+ORDER BY id;
+```
+
+### 8.3 获取完整对话历史(用于 AI Context)
+
+```sql
+-- 获取当前激活路径的所有消息
+WITH RECURSIVE active_path AS (
+  SELECT * FROM chat_messages
+  WHERE chat_id = ? AND parent_id IS NULL AND is_active = true AND deleted_at IS NULL
+
+  UNION ALL
+
+  SELECT m.* FROM chat_messages m
+  JOIN active_path p ON m.parent_id = p.uid
+  WHERE m.is_active = true AND m.deleted_at IS NULL
+)
+SELECT * FROM active_path ORDER BY id;
+```
+
+### 8.4 查询消息的所有版本(包含已删除)
+
+```sql
+SELECT *,
+       CASE WHEN deleted_at IS NULL THEN 'active' ELSE 'deleted' END as status
+FROM chat_messages
+WHERE parent_id = ? AND role = ?
+ORDER BY created_at;
+```
+
+### 8.5 Token 使用统计查询
+
+```sql
+-- 查询聊天的总token使用量
+SELECT
+    chat_id,
+    SUM(JSON_EXTRACT(metadata, '$.token_usage.total_tokens')) as total_tokens,
+    AVG(JSON_EXTRACT(metadata, '$.performance.response_time_ms')) as avg_response_time
+FROM chat_messages
+WHERE chat_id = ? AND role = 'assistant' AND deleted_at IS NULL
+GROUP BY chat_id;
+```
+
+## 9. 性能优化建议
+
+### 9.1 索引策略
+
+- `chat_id`: 快速查询某个聊天的所有消息
+- `session_id`: 快速获取消息组
+- `parent_id`: 支持树形查询和兄弟节点查询
+- `(chat_id, is_active)`: 快速获取激活消息
+- `deleted_at`: 快速过滤软删除数据
+
+### 9.2 查询优化
+
+- 使用递归 CTE 进行树形查询
+- 考虑 materialized path 优化深层树查询
+- 对于频繁的 session 查询,考虑适当的缓存策略
+- metadata JSON 字段可根据需要创建虚拟列索引
+
+### 9.3 软删除优化
+
+- 定期清理长期删除的数据(如 30 天后物理删除)
+- 考虑分区表优化查询性能
+- 为 deleted_at 字段建立部分索引(仅索引非 NULL 值)
+
+## 10. 扩展性考虑
+
+### 10.1 多模态支持
+
+- content 字段可存储 JSON,支持文本、图片、文件等多种内容类型
+- tool_calls 可扩展支持更多函数类型
+- metadata 可扩展记录多模态内容的处理参数
+
+### 10.2 权限控制
+
+- 通过 user_id 支持多用户隔离
+- 可扩展支持聊天分享、协作等功能
+- 软删除支持权限恢复场景
+
+### 10.3 审计和监控
+
+- created_at/updated_at 支持操作时间追踪
+- deleted_at 提供删除审计
+- metadata 记录详细的生成和性能数据
+- 可扩展添加操作日志表记录详细变更历史
+
+## 11. 注意事项
+
+1. **UUID 生成**: 确保 uid 字段使用标准 UUID v4 格式
+2. **JSON 字段**: 使用数据库原生 JSON 类型,支持索引和查询
+3. **外键约束**: 合理使用外键约束保证数据一致性
+4. **软删除一致性**: 确保级联删除的数据一致性
+5. **并发控制**: 在版本更新时注意并发冲突处理
+6. **数据清理**: 制定合理的物理删除策略,避免数据无限增长
+7. **查询习惯**: 所有业务查询都必须包含 `deleted_at IS NULL` 条件
+8. **JSON 索引**: 根据实际查询需求为 metadata 字段创建合适的索引
+
+---
+
+**文档版本**: 1.1  
+**创建时间**: 2025-09-07  
+**更新时间**: 2025-09-09  
+**修订说明**: 增加 metadata 字段和软删除机制

+ 379 - 0
dashboard-v4/documents/development/chat/api.md

@@ -0,0 +1,379 @@
+# Chat System API 使用指南
+
+## 安装和配置
+
+### 1. 运行数据库迁移
+
+```bash
+php artisan migrate
+```
+
+### 2. 发布配置文件(如需要)
+
+```bash
+php artisan vendor:publish --provider="App\Providers\ChatServiceProvider"
+```
+
+## API 端点和用法
+
+### 聊天管理 API
+
+#### 1. 创建新聊天
+
+```http
+POST /api/v2/chats
+Content-Type: application/json
+
+{
+    "title": "天气查询对话",
+    "user_id": "ba5463f3-72d1-4410-858e-eadd10884713"
+}
+```
+
+**响应示例:**
+
+```json
+{
+  "ok": true,
+  "message": "ok",
+  "data": {
+    "id": "550e8400-e29b-41d4-a716-446655440000",
+    "title": "天气查询对话",
+    "user_id": "ba5463f3-72d1-4410-858e-eadd10884713",
+    "created_at": "2025-01-15T10:30:00.000000Z",
+    "updated_at": "2025-01-15T10:30:00.000000Z"
+  }
+}
+```
+
+#### 2. 获取聊天列表
+
+```http
+GET /api/v2/v2/chats?limit=20&user_id=123
+```
+
+#### 3. 获取单个聊天详情
+
+```http
+GET /api/v2/v2/chats/{chat_uid}
+```
+
+#### 4. 更新聊天标题
+
+```http
+PUT /api/v2/chats/{chat_uid}
+Content-Type: application/json
+
+{
+    "title": "更新后的标题"
+}
+```
+
+#### 5. 删除聊天
+
+```http
+DELETE /api/v2/chats/{chat_uid}
+```
+
+### 消息管理 API
+
+消息不允许更新和删除,只能新建
+
+#### 1. 获取消息列表
+
+```http
+GET /api/v2/chat-messages?chat=123
+```
+
+**响应示例:**
+
+```json
+{
+  "ok": true,
+  "message": "ok",
+  "data": {
+    "rows": [
+      {
+        "id": 1,
+        "uid": "message-1",
+        "role": "user",
+        "content": "查询今天北京的天气",
+        "session_id": "session-1",
+        "editor_id": "editor-uuid-456"
+      },
+      {
+        "id": 2,
+        "uid": "message-2",
+        "session_id": "session-2",
+        "parent_id": "message-1",
+        "role": "assistant",
+        "model_id": "gpt-4",
+        "tool_calls": [
+          {
+            "id": "call_123",
+            "function": "get_weather",
+            "arguments": {
+              "city": "北京",
+              "date": "today"
+            }
+          }
+        ]
+      },
+      {
+        "id": 3,
+        "uid": "message-3",
+        "session_id": "session-2",
+        "parent_id": "message-2",
+        "role": "tool",
+        "content": "今天北京天气晴朗,气温25°C,湿度60%,风力3级",
+        "tool_call_id": "call_123"
+      },
+      {
+        "id": 4,
+        "uid": "message-4",
+        "session_id": "session-2",
+        "parent_id": "message-3",
+        "role": "tool",
+        "content": "今天北京天气晴朗,气温25°C,湿度60%,风力3级",
+        "tool_call_id": "call_123",
+        "model_id": "model-uuid-123",
+        "metadata": {
+          "temperature": 0.7,
+          "tokens": 150,
+          "max_tokens": 1000
+        }
+      }
+    ],
+    "total": 4
+  }
+}
+```
+
+#### 1. 创建用户消息
+
+```http
+POST /api/v2/chat-messages
+Content-Type: application/json
+
+{
+    "chat_id":"1234",
+    "messages": [
+        {
+            "role": "user",
+            "content": "查询今天北京的天气"
+        }
+    ]
+
+}
+```
+
+#### 2. 创建带工具调用的 Assistant 消息
+
+工具调用的 Assistant 消息,工具结果消息,最终回复消息,应该在结果正常返回后一次发送建立
+
+```http
+POST /api/v2/chats/{chat_uid}/messages
+Content-Type: application/json
+
+{
+    "chat_id":"1234",
+    "messages": [
+            {
+                "parent_id": "user-message-uuid",
+                "role": "assistant",
+                "model_id": "gpt-4",
+                "tool_calls": [
+                    {
+                        "id": "call_123",
+                        "function": "get_weather",
+                        "arguments": {
+                            "city": "北京",
+                            "date": "today"
+                        }
+                    }
+                ]
+            },
+            {
+                "parent_id": "assistant-message-uuid",
+                "role": "tool",
+                "content": "今天北京天气晴朗,气温25°C,湿度60%,风力3级",
+                "tool_call_id": "call_123"
+            },
+            {
+                "parent_id": "assistant-message-uuid",
+                "role": "tool",
+                "content": "今天北京天气晴朗,气温25°C,湿度60%,风力3级",
+                "tool_call_id": "call_123"
+            },
+    ]
+}
+```
+
+## 典型业务场景示例
+
+### 场景 1: 完整的 Function Call 对话流程
+
+````javascript
+// 1. 创建聊天
+const chatResponse = await fetch("/api/v2/chats", {
+    method: "POST",
+    headers: { "Content-Type": "application/json" },
+    body: JSON.stringify({
+        title: "天气查询助手",
+    }),
+});
+const chat = await chatResponse.json();
+
+// 2. 用户提问
+const userMessage = await fetch(`/api/v2/chat-messages`, {
+    method: "POST",
+    headers: { "Content-Type": "application/json" },
+    body: JSON.stringify({
+        chat_id: "chat-id",
+        messages: [
+            {
+                role: "user",
+                content: "今天上海天气怎么样?",
+            },
+        ],
+    }),
+});
+const userMsg = await userMessage.json();
+
+// 3. Assistant 调用工具
+
+const assistantCall = await fetch(`/api/v2/chats-messages`, {
+    method: "POST",
+    headers: { "Content-Type": "application/json" },
+    body: JSON.stringify({
+        chat_id: "chat-id",
+        messages: [
+            {
+                parent_id: userMsg.data.id,
+                role: "assistant",
+                model_name: "gpt-4",
+                tool_calls: [
+                    {
+                        id: "call_weather_001",
+                        function: "get_weather",
+                        arguments: { city: "上海", date: "today" },
+                    },
+                ],
+            },
+            {
+                parent_id: assistantMsg.data.id,
+                role: "tool",
+                content: "上海今日天气:晴,22-28°C,东南风3级,湿度65%",
+                tool_call_id: "call_weather_001",
+            },
+            {
+                parent_id: toolMsg.data.id,
+                role: "assistant",
+                content:
+                    "今天上海的天气很不错!晴天,气温在22-28度之间,很适合外出。东南风3级,体感舒适,湿度适中。建议穿轻薄的长袖或短袖都可以。",
+                model_id: 'model-uuid-123',
+                metadata: {
+                    temperature: 0.7,
+                    tokens: 150,
+                    max_tokens: 1000
+                },
+            },
+        ],
+    }),
+});
+const assistantMsg = await assistantCall.json();
+
+
+
+## 数据模型说明
+
+### Chat 模型
+
+-   `id`: 数据库自增 ID
+-   `uid`: UUID 标识符,用于 API 调用
+-   `title`: 聊天标题
+-   `user_id`: 用户 ID(可选)
+-   `created_at/updated_at`: 时间戳
+
+### ChatMessage 模型
+
+-   `id`: 数据库自增 ID(用于版本排序,替代 version 字段)
+-   `uid`: UUID 标识符,用于 API 调用
+-   `chat_id`: 所属聊天 ID
+-   `parent_id`: 父消息 UUID(构建依赖关系)
+-   `session_id`: 会话组 UUID(前端显示分组)
+-   `role`: 消息角色(user/assistant/tool)
+-   `content`: 消息内容
+-   `model_id`: 使用的 AI 模型 UUID
+-   `tool_calls`: 工具调用信息(JSON 格式)
+-   `tool_call_id`: 工具调用 ID(tool 消息专用)
+-   `metadata`: 元数据信息(temperature、tokens 等,JSON 格式)
+-   `editor_id`: 编辑者 UUID
+-   `is_active`: 是否为当前激活版本
+
+## 重要变更说明
+
+### 1. 版本控制机制改变
+
+-   **移除**: `version`字段
+-   **新机制**: 使用数据库自增 ID 自然排序实现版本控制
+-   **优势**: 避免多浏览器并发冲突,ID 天然递增保证版本顺序
+
+### 2. 新增字段
+
+-   `metadata`: JSON 字段存储模型参数和执行信息
+-   `editor_id`: UUID 字段记录编辑者信息
+-   `model_id`: 改为 UUID 格式,便于模型管理
+
+### 3. 字段类型调整
+
+-   `user_id`: 从 BIGINT 改为 UUID 格式
+-   `model_name` → `model_id`: 名称改为 UUID 引用
+
+## 错误处理
+
+API 使用标准 HTTP 状态码:
+
+-   `200`: 操作成功
+-   `201`: 资源创建成功
+-   `400`: 请求参数错误
+-   `404`: 资源不存在
+-   `422`: 验证失败
+-   `500`: 服务器错误
+
+错误响应格式:
+
+```json
+{
+    "ok":false,
+    "message": "错误描述",
+    "errors": {
+        "field_name": ["具体错误信息"]
+    }
+}
+````
+
+## 性能优化建议
+
+1. **分页查询**: 大量消息时使用分页参数`limit`
+2. **索引优化**: 数据库已包含必要索引
+3. **缓存策略**: 对频繁查询的聊天历史考虑使用 Redis 缓存
+4. **批量操作**: 使用批量更新 API 减少网络请求
+5. **懒加载**: 前端可按需加载消息版本和历史记录
+
+## 安全考虑
+
+1. **权限验证**: 根据需要添加用户认证中间件
+2. **输入验证**: 所有输入都经过 FormRequest 验证
+3. **SQL 注入防护**: 使用 Eloquent ORM 和参数化查询
+4. **XSS 防护**: 输出时进行适当转义
+5. **CSRF 保护**: API 接口建议配置 CSRF 中间件
+
+## 扩展功能
+
+系统支持以下扩展:
+
+1. **多用户支持**: 通过 user_id 字段隔离数据
+2. **消息搜索**: 可在 content 字段上建立全文索引
+3. **文件附件**: content 字段支持 JSON 格式存储多媒体内容
+4. **消息导出**: 可导出完整对话历史

+ 2260 - 0
dashboard-v4/documents/development/chat/frontend.md

@@ -0,0 +1,2260 @@
+# AI Chat 前端重构设计文档
+
+## 1. 项目概述
+
+本项目旨在重构一个类似 Claude 的 AI 聊天系统前端,支持以下核心功能:
+
+- 多轮对话管理和消息树结构
+- Function Call 集成(支持多厂商模型)
+- 消息版本控制和切换
+- 实时流式输出和打字机效果
+- 消息编辑、刷新、点赞、分享等操作
+- 乐观更新和失败重试机制
+- 与后端 API 的完整集成
+
+## 2. 核心设计理念
+
+### 2.1 数据流设计
+
+- **线性存储**:从数据库加载的消息为线性数组,无需构建树结构
+- **激活路径**:通过 `is_active` 字段标记用户当前查看的消息链
+- **Session 分组**:相同 `session_id` 的消息在前端显示为一个对话组
+- **乐观更新**:用户操作立即响应,异步同步到服务器
+
+### 2.2 版本管理
+
+- 同一 `parent_id` 的消息作为不同版本
+- 用户可通过版本切换器查看历史版本
+- 编辑和刷新都创建新版本,不修改原消息
+- 组件加载时载入全部 message, 版本切换时无需请求服务器,在已有的 message 数组中计算新的当前激活版本
+
+### 2.3 错误处理
+
+- 手动重试机制,不自动重试
+- 临时消息状态标记,区分已保存/待保存/失败状态
+- 完整的错误恢复流程
+
+## 3. 架构设计
+
+```text
+├── types/
+│   └── chat.ts                # 所有类型定义
+├── hooks/                     # 数据管理层
+│   ├── useChatData.ts        # 主要数据管理
+│   ├── useActivePath.ts      # 激活路径计算
+│   └── useSessionGroups.ts   # Session 分组管理
+├── services/                  # API调用层
+│   ├── chatApi.ts            # 聊天 API
+│   ├── messageApi.ts         # 消息 API
+│   └── modelAdapters/        # 模型适配器
+│       ├── base.ts           # 基础适配器
+│       ├── openai.ts         # OpenAI 适配器
+│       └── index.ts          # 适配器工厂
+└── components/               # 显示层
+    └── chat
+        ├── ChatContainer.tsx     # 主容器组件
+        ├── SessionGroup.tsx      # Session 消息组
+        ├── UserMessage.tsx       # 用户消息
+        ├── AssistantMessage.tsx  # AI 回答
+        ├── MessageActions.tsx    # 消息操作
+        ├── ChatInput.tsx         # 输入组件
+        ├── StreamingMessage.tsx  # 打字机输入组件
+        └── VersionSwitcher.tsx   # 版本切换
+```
+
+## 4. 类型定义
+
+### types/chat.ts
+
+```typescript
+// 工具调用相关类型
+export interface ToolCall {
+  id: string;
+  function: string;
+  arguments: Record<string, any>;
+}
+
+// 消息元数据
+export interface MessageMetadata {
+  generation_params?: {
+    temperature?: number;
+    max_tokens?: number;
+    top_p?: number;
+    frequency_penalty?: number;
+    presence_penalty?: number;
+  };
+  token_usage?: {
+    prompt_tokens?: number;
+    completion_tokens?: number;
+    total_tokens?: number;
+  };
+  performance?: {
+    response_time_ms?: number;
+    first_token_time_ms?: number;
+  };
+  tool_stats?: {
+    total_calls?: number;
+    successful_calls?: number;
+    execution_time_ms?: number;
+  };
+  custom_data?: Record<string, any>;
+}
+
+// 消息节点(对应数据库结构)
+export interface MessageNode {
+  id: number; // DB自增ID,用于版本排序
+  uid: string; // UUID
+  chat_id: string;
+  parent_id?: string;
+  session_id: string;
+  role: "system" | "user" | "assistant" | "tool";
+  content?: string;
+  model_id?: string;
+  tool_calls?: ToolCall[];
+  tool_call_id?: string;
+  metadata?: MessageMetadata;
+  is_active: boolean;
+  editor_id?: string;
+  created_at: string;
+  updated_at: string;
+  deleted_at?: string;
+
+  // 临时状态字段(前端使用)
+  save_status?: "saved" | "pending" | "failed";
+  temp_id?: string; // 临时ID,用于未保存消息
+}
+
+// 版本信息
+export interface VersionInfo {
+  version_index: number; // 版本索引(0,1,2...)
+  model_id?: string; // 该版本使用的模型
+  model_name?: string; // 模型显示名称
+  created_at: string; // 版本创建时间
+  message_count: number; // 该版本包含的消息数量
+  token_usage?: number; // 该版本的token使用量
+}
+
+// Session 信息
+export interface SessionInfo {
+  session_id: string;
+  messages: MessageNode[]; // 该session的所有消息(按激活路径过滤)
+  versions: VersionInfo[]; // 该session所有版本信息
+  current_version: number; // 当前显示的版本索引
+  user_message?: MessageNode; // 该session的用户消息(便于访问)
+  ai_messages: MessageNode[]; // 该session的AI消息列表
+}
+
+// 待保存消息组
+export interface PendingMessage {
+  temp_id: string;
+  session_id: string;
+  messages: MessageNode[]; // 待保存的消息组
+  retry_count: number;
+  error?: string;
+  created_at: string;
+}
+
+// 聊天状态
+export interface ChatState {
+  chat_id: string;
+  title: string;
+  raw_messages: MessageNode[]; // 从DB加载的原始线性数据
+  active_path: MessageNode[]; // 当前激活路径上的消息
+  session_groups: SessionInfo[]; // 按session分组的显示数据
+  pending_messages: PendingMessage[]; // 待保存的消息组
+  is_loading: boolean;
+  streaming_message?: string;
+  streaming_session_id?: string;
+  current_model: string;
+  error?: string;
+}
+
+// 聊天操作接口
+export interface ChatActions {
+  switchVersion: (sessionId: string, versionIndex: number) => Promise<void>;
+  editMessage: (
+    sessionId: string,
+    content: string,
+    role?: "user" | "assistant"
+  ) => Promise<void>;
+  retryMessage: (tempId: string) => Promise<void>;
+  refreshResponse: (sessionId: string, modelId?: string) => Promise<void>;
+  loadMessages: () => Promise<void>;
+  likeMessage: (messageId: string) => Promise<void>;
+  dislikeMessage: (messageId: string) => Promise<void>;
+  copyMessage: (messageId: string) => void;
+  shareMessage: (messageId: string) => Promise<string>;
+  deleteMessage: (messageId: string) => Promise<void>;
+}
+
+// API 请求类型
+export interface CreateMessageRequest {
+  messages: Array<{
+    parent_id?: string;
+    role: "user" | "assistant" | "tool";
+    content?: string;
+    model_id?: string;
+    tool_calls?: ToolCall[];
+    tool_call_id?: string;
+    metadata?: MessageMetadata;
+  }>;
+}
+
+export interface CreateChatRequest {
+  title: string;
+  user_id?: string;
+}
+
+// API 响应类型
+export interface ApiResponse<T> {
+  ok: boolean;
+  message: string;
+  data: T;
+}
+
+export interface ChatResponse {
+  id: string;
+  title: string;
+  user_id?: string;
+  created_at: string;
+  updated_at: string;
+}
+
+export interface MessageListResponse {
+  rows: MessageNode[];
+  total: number;
+}
+
+// 模型适配器相关类型
+export interface ModelAdapter {
+  name: string;
+  supportsFunctionCall: boolean;
+  sendMessage(
+    messages: OpenAIMessage[],
+    options: SendOptions
+  ): Promise<StreamResponse>;
+  parseStreamChunk(chunk: string): ParsedChunk | null;
+  handleFunctionCall(functionCall: ToolCall): Promise<any>;
+}
+
+export interface OpenAIMessage {
+  role: "system" | "user" | "assistant" | "function" | "tool";
+  content?: string;
+  name?: string;
+  tool_calls?: ToolCall[];
+  tool_call_id?: string;
+}
+
+export interface SendOptions {
+  temperature?: number;
+  max_tokens?: number;
+  top_p?: number;
+  functions?: Array<{
+    name: string;
+    description: string;
+    parameters: any;
+  }>;
+  function_call?: string | { name: string };
+}
+
+export interface StreamResponse {
+  messages: MessageNode[];
+  metadata?: MessageMetadata;
+}
+
+export interface ParsedChunk {
+  content?: string;
+  function_call?: {
+    name?: string;
+    arguments?: string;
+  };
+  finish_reason?: string;
+}
+
+// 组件 Props 类型
+export interface SessionGroupProps {
+  session: SessionInfo;
+  onVersionSwitch: (sessionId: string, versionIndex: number) => void;
+  onRefresh: (sessionId: string, modelId?: string) => void;
+  onEdit: (sessionId: string, content: string) => void;
+  onRetry?: (tempId: string) => void;
+  onLike?: (messageId: string) => void;
+  onDislike?: (messageId: string) => void;
+  onCopy?: (messageId: string) => void;
+  onShare?: (messageId: string) => void;
+}
+
+export interface UserMessageProps {
+  message: MessageNode;
+  onEdit?: (content: string) => void;
+  onCopy?: () => void;
+}
+
+export interface AssistantMessageProps {
+  messages: MessageNode[];
+  onRefresh?: () => void;
+  onEdit?: (content: string) => void;
+  isPending?: boolean;
+  onLike?: (messageId: string) => void;
+  onDislike?: (messageId: string) => void;
+  onCopy?: (messageId: string) => void;
+  onShare?: (messageId: string) => void;
+}
+
+export interface VersionSwitcherProps {
+  versions: VersionInfo[];
+  currentVersion: number;
+  onSwitch: (versionIndex: number) => void;
+}
+
+export interface ChatInputProps {
+  onSend: (content: string) => void;
+  disabled?: boolean;
+  placeholder?: string;
+}
+```
+
+## 5. 数据管理层
+
+### hooks/useActivePath.ts
+
+```typescript
+import { useMemo, useCallback } from "react";
+import { MessageNode } from "../types/chat";
+
+export function useActivePath(rawMessages: MessageNode[]) {
+  const computeActivePath = useCallback(() => {
+    // 从system消息开始,沿着is_active=true的路径构建激活链
+    const messageMap = new Map(rawMessages.map((m) => [m.uid, m]));
+    const activePath: MessageNode[] = [];
+
+    // 找到system消息(根节点)
+    const systemMsg = rawMessages.find(
+      (m) => m.role === "system" && !m.parent_id && m.is_active
+    );
+    if (!systemMsg) return [];
+
+    // 沿着激活路径构建链
+    let current: MessageNode | undefined = systemMsg;
+    while (current) {
+      activePath.push(current);
+
+      // 找到当前消息的激活子消息
+      current = rawMessages.find(
+        (m) => m.parent_id === current?.uid && m.is_active
+      );
+    }
+
+    return activePath;
+  }, [rawMessages]);
+
+  return useMemo(() => computeActivePath(), [computeActivePath]);
+}
+```
+
+### hooks/useSessionGroups.ts
+
+```typescript
+import { useMemo, useCallback } from "react";
+import { MessageNode, SessionInfo, VersionInfo } from "../types/chat";
+
+export function useSessionGroups(
+  activePath: MessageNode[],
+  rawMessages: MessageNode[]
+) {
+  const computeSessionVersions = useCallback(
+    (sessionId: string): VersionInfo[] => {
+      // 找到该session的所有消息
+      const sessionMessages = rawMessages.filter(
+        (m) => m.session_id === sessionId
+      );
+
+      // 按不同的创建时间和父消息分组,计算版本
+      const versionMap = new Map<string, MessageNode[]>();
+
+      sessionMessages.forEach((msg) => {
+        // 使用第一个AI消息的创建时间作为版本标识
+        const firstAiMsg = sessionMessages
+          .filter((m) => m.role === "assistant")
+          .sort((a, b) => a.id - b.id)[0];
+
+        const versionKey = firstAiMsg ? firstAiMsg.created_at : msg.created_at;
+
+        if (!versionMap.has(versionKey)) {
+          versionMap.set(versionKey, []);
+        }
+        versionMap.get(versionKey)!.push(msg);
+      });
+
+      // 转换为VersionInfo数组
+      const versions: VersionInfo[] = Array.from(versionMap.entries())
+        .map(([timestamp, messages], index) => {
+          const aiMessage = messages.find((m) => m.role === "assistant");
+          return {
+            version_index: index,
+            model_id: aiMessage?.model_id,
+            created_at: timestamp,
+            message_count: messages.length,
+            token_usage: aiMessage?.metadata?.token_usage?.total_tokens,
+          };
+        })
+        .sort(
+          (a, b) =>
+            new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
+        );
+
+      return versions;
+    },
+    [rawMessages]
+  );
+
+  const findCurrentVersion = useCallback(
+    (sessionMessages: MessageNode[], versions: VersionInfo[]): number => {
+      // 找到当前激活的AI消息
+      const activeAiMsg = sessionMessages.find(
+        (m) => m.role === "assistant" && m.is_active
+      );
+      if (!activeAiMsg) return 0;
+
+      // 根据创建时间找到对应的版本索引
+      const versionIndex = versions.findIndex(
+        (v) => v.created_at === activeAiMsg.created_at
+      );
+      return Math.max(0, versionIndex);
+    },
+    []
+  );
+
+  const computeSessionGroups = useCallback((): SessionInfo[] => {
+    const sessionMap = new Map<string, MessageNode[]>();
+
+    // 按session_id分组激活路径上的消息(排除system消息)
+    activePath.forEach((msg) => {
+      if (msg.role !== "system") {
+        const sessionId = msg.session_id;
+        if (!sessionMap.has(sessionId)) {
+          sessionMap.set(sessionId, []);
+        }
+        sessionMap.get(sessionId)!.push(msg);
+      }
+    });
+
+    // 为每个session计算版本信息
+    const sessionGroups: SessionInfo[] = [];
+
+    sessionMap.forEach((messages, sessionId) => {
+      const versions = computeSessionVersions(sessionId);
+      const currentVersion = findCurrentVersion(messages, versions);
+
+      const userMessage = messages.find((m) => m.role === "user");
+      const aiMessages = messages.filter((m) => m.role !== "user");
+
+      sessionGroups.push({
+        session_id: sessionId,
+        messages,
+        versions,
+        current_version: currentVersion,
+        user_message: userMessage,
+        ai_messages: aiMessages,
+      });
+    });
+
+    // 按消息ID排序,保证显示顺序
+    return sessionGroups.sort((a, b) => {
+      const aFirstId = Math.min(...a.messages.map((m) => m.id));
+      const bFirstId = Math.min(...b.messages.map((m) => m.id));
+      return aFirstId - bFirstId;
+    });
+  }, [activePath, rawMessages, computeSessionVersions, findCurrentVersion]);
+
+  return useMemo(() => computeSessionGroups(), [computeSessionGroups]);
+}
+```
+
+### hooks/useChatData.ts
+
+```typescript
+import { useState, useCallback, useMemo } from "react";
+import {
+  MessageNode,
+  ChatState,
+  ChatActions,
+  PendingMessage,
+} from "../types/chat";
+import { useActivePath } from "./useActivePath";
+import { useSessionGroups } from "./useSessionGroups";
+import { chatApi, messageApi } from "../services";
+import { getModelAdapter } from "../services/modelAdapters";
+
+export function useChatData(chatId: string): {
+  chatState: ChatState;
+  actions: ChatActions;
+} {
+  const [rawMessages, setRawMessages] = useState<MessageNode[]>([]);
+  const [pendingMessages, setPendingMessages] = useState<PendingMessage[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [streamingMessage, setStreamingMessage] = useState<string>();
+  const [streamingSessionId, setStreamingSessionId] = useState<string>();
+  const [currentModel, setCurrentModel] = useState("gpt-4");
+  const [error, setError] = useState<string>();
+
+  // 合并已保存和待保存的消息用于显示
+  const allMessages = useMemo(() => {
+    const pending = pendingMessages.flatMap((p) => p.messages);
+    return [...rawMessages, ...pending];
+  }, [rawMessages, pendingMessages]);
+
+  const activePath = useActivePath(allMessages);
+  const sessionGroups = useSessionGroups(activePath, allMessages);
+
+  // 加载消息列表
+  const loadMessages = useCallback(async () => {
+    try {
+      setIsLoading(true);
+      const response = await messageApi.getMessages(chatId);
+      setRawMessages(response.data.rows);
+    } catch (err) {
+      setError(err instanceof Error ? err.message : "加载消息失败");
+    } finally {
+      setIsLoading(false);
+    }
+  }, [chatId]);
+
+  // 构建对话历史(用于AI API调用)
+  const buildConversationHistory = useCallback(
+    (baseMessages: MessageNode[], newUserMessage?: MessageNode) => {
+      const history = activePath
+        .filter((m) => m.role !== "tool") // 排除tool消息
+        .map((m) => ({
+          role: m.role as any,
+          content: m.content || "",
+          tool_calls: m.tool_calls,
+          tool_call_id: m.tool_call_id,
+        }));
+
+      if (newUserMessage) {
+        history.push({
+          role: "user",
+          content: newUserMessage.content || "",
+        });
+      }
+
+      return history;
+    },
+    [activePath]
+  );
+
+  // 发送消息给AI并处理响应
+  const sendMessageToAI = useCallback(
+    async (userMessage: MessageNode, pendingGroup: PendingMessage) => {
+      try {
+        setIsLoading(true);
+        setStreamingSessionId(pendingGroup.session_id);
+
+        const conversationHistory = buildConversationHistory(
+          rawMessages,
+          userMessage
+        );
+        const adapter = getModelAdapter(currentModel);
+
+        // 处理Function Call的循环逻辑
+        let currentMessages = conversationHistory;
+        let maxIterations = 10;
+        let allAiMessages: MessageNode[] = [];
+
+        while (maxIterations-- > 0) {
+          // 流式处理AI响应
+          let responseContent = "";
+          let functionCalls: any[] = [];
+          let metadata: any = {};
+
+          const streamResponse = await adapter.sendMessage(currentMessages, {
+            temperature: 0.7,
+            max_tokens: 2048,
+          });
+
+          // 模拟流式输出处理
+          await new Promise((resolve, reject) => {
+            const processStream = async () => {
+              try {
+                // 这里应该是实际的流处理逻辑
+                for await (const chunk of streamResponse) {
+                  const parsed = adapter.parseStreamChunk(chunk);
+                  if (parsed?.content) {
+                    responseContent += parsed.content;
+                    setStreamingMessage(responseContent);
+                  }
+                  if (parsed?.function_call) {
+                    // 处理function call
+                  }
+                }
+                resolve(undefined);
+              } catch (err) {
+                reject(err);
+              }
+            };
+            processStream();
+          });
+
+          // 创建AI响应消息
+          const aiMessage: MessageNode = {
+            id: 0,
+            uid: `temp_ai_${pendingGroup.temp_id}_${allAiMessages.length}`,
+            temp_id: pendingGroup.temp_id,
+            chat_id: chatId,
+            session_id: pendingGroup.session_id,
+            parent_id:
+              allAiMessages.length === 0
+                ? userMessage.uid
+                : allAiMessages[allAiMessages.length - 1].uid,
+            role: "assistant",
+            content: responseContent,
+            model_id: currentModel,
+            tool_calls: functionCalls.length > 0 ? functionCalls : undefined,
+            metadata,
+            is_active: true,
+            save_status: "pending",
+            created_at: new Date().toISOString(),
+            updated_at: new Date().toISOString(),
+          };
+
+          allAiMessages.push(aiMessage);
+
+          // 如果有function calls,处理它们
+          if (functionCalls.length > 0) {
+            const toolResults = await Promise.all(
+              functionCalls.map((call) => adapter.handleFunctionCall(call))
+            );
+
+            const toolMessages = functionCalls.map((call, index) => ({
+              id: 0,
+              uid: `temp_tool_${pendingGroup.temp_id}_${index}`,
+              temp_id: pendingGroup.temp_id,
+              chat_id: chatId,
+              session_id: pendingGroup.session_id,
+              parent_id: aiMessage.uid,
+              role: "tool" as const,
+              content: JSON.stringify(toolResults[index]),
+              tool_call_id: call.id,
+              is_active: true,
+              save_status: "pending" as const,
+              created_at: new Date().toISOString(),
+              updated_at: new Date().toISOString(),
+            }));
+
+            allAiMessages.push(...toolMessages);
+
+            // 更新对话历史,继续循环
+            currentMessages.push(
+              {
+                role: "assistant",
+                content: responseContent,
+                tool_calls: functionCalls,
+              },
+              ...toolMessages.map((tm) => ({
+                role: "tool" as const,
+                content: tm.content || "",
+                tool_call_id: tm.tool_call_id,
+              }))
+            );
+
+            continue;
+          }
+
+          // 没有function call,结束循环
+          break;
+        }
+
+        // 更新pending消息组
+        setPendingMessages((prev) =>
+          prev.map((p) =>
+            p.temp_id === pendingGroup.temp_id
+              ? { ...p, messages: [...p.messages, ...allAiMessages] }
+              : p
+          )
+        );
+
+        // 保存整个消息组到数据库
+        await saveMessageGroup(pendingGroup.temp_id, [
+          userMessage,
+          ...allAiMessages,
+        ]);
+      } catch (err) {
+        console.error("AI响应失败:", err);
+        setPendingMessages((prev) =>
+          prev.map((p) =>
+            p.temp_id === pendingGroup.temp_id
+              ? {
+                  ...p,
+                  error: err instanceof Error ? err.message : "未知错误",
+                  retry_count: p.retry_count + 1,
+                }
+              : p
+          )
+        );
+      } finally {
+        setIsLoading(false);
+        setStreamingMessage(undefined);
+        setStreamingSessionId(undefined);
+      }
+    },
+    [rawMessages, currentModel, chatId, buildConversationHistory]
+  );
+
+  // 保存消息组到数据库
+  const saveMessageGroup = useCallback(
+    async (tempId: string, messages: MessageNode[]) => {
+      try {
+        const savedMessages = await messageApi.createMessages(chatId, {
+          messages: messages.map((m) => ({
+            parent_id: m.parent_id,
+            role: m.role as any,
+            content: m.content,
+            model_id: m.model_id,
+            tool_calls: m.tool_calls,
+            tool_call_id: m.tool_call_id,
+            metadata: m.metadata,
+          })),
+        });
+
+        // 更新本地状态:移除pending,添加到已保存消息
+        setPendingMessages((prev) => prev.filter((p) => p.temp_id !== tempId));
+        setRawMessages((prev) => [...prev, ...savedMessages.data]);
+      } catch (err) {
+        console.error("保存消息组失败:", err);
+        setPendingMessages((prev) =>
+          prev.map((p) =>
+            p.temp_id === tempId
+              ? {
+                  ...p,
+                  error: err instanceof Error ? err.message : "保存失败",
+                  messages: p.messages.map((m) => ({
+                    ...m,
+                    save_status: "failed" as const,
+                  })),
+                }
+              : p
+          )
+        );
+      }
+    },
+    [chatId]
+  );
+
+  // 编辑消息 - 创建新版本
+  const editMessage = useCallback(
+    async (
+      sessionId: string,
+      content: string,
+      role: "user" | "assistant" = "user"
+    ) => {
+      const tempId = `temp_${Date.now()}`;
+
+      try {
+        // 找到要编辑的消息的父消息
+        let parentId: string | undefined;
+
+        if (sessionId === "new") {
+          // 新消息,找到最后一个激活消息作为父消息
+          const lastMessage = activePath[activePath.length - 1];
+          parentId = lastMessage?.uid;
+        } else {
+          // 编辑现有session,找到该session的父消息
+          const sessionMessages = activePath.filter(
+            (m) => m.session_id === sessionId
+          );
+          const firstMessage = sessionMessages[0];
+          parentId = firstMessage?.parent_id;
+        }
+
+        const newSessionId =
+          sessionId === "new" ? `session_${tempId}` : `session_${tempId}`;
+
+        // 创建新的用户消息
+        const newUserMessage: MessageNode = {
+          id: 0,
+          uid: `temp_user_${tempId}`,
+          temp_id: tempId,
+          chat_id: chatId,
+          parent_id: parentId,
+          session_id: newSessionId,
+          role: "user",
+          content,
+          is_active: true,
+          save_status: "pending",
+          created_at: new Date().toISOString(),
+          updated_at: new Date().toISOString(),
+        };
+
+        // 创建待保存消息组
+        const pendingGroup: PendingMessage = {
+          temp_id: tempId,
+          session_id: newSessionId,
+          messages: [newUserMessage],
+          retry_count: 0,
+          created_at: new Date().toISOString(),
+        };
+
+        setPendingMessages((prev) => [...prev, pendingGroup]);
+
+        // 如果是用户消息,发送给AI
+        if (role === "user") {
+          await sendMessageToAI(newUserMessage, pendingGroup);
+        }
+      } catch (err) {
+        console.error("编辑消息失败:", err);
+        setPendingMessages((prev) =>
+          prev.map((p) =>
+            p.temp_id === tempId
+              ? {
+                  ...p,
+                  messages: p.messages.map((m) => ({
+                    ...m,
+                    save_status: "failed" as const,
+                  })),
+                  error: err instanceof Error ? err.message : "编辑失败",
+                }
+              : p
+          )
+        );
+      }
+    },
+    [chatId, activePath, sendMessageToAI]
+  );
+
+  // 重试失败的消息
+  const retryMessage = useCallback(
+    async (tempId: string) => {
+      const pendingGroup = pendingMessages.find((p) => p.temp_id === tempId);
+      if (!pendingGroup) return;
+
+      const userMessage = pendingGroup.messages.find((m) => m.role === "user");
+      if (!userMessage) return;
+
+      // 重置状态并重试
+      setPendingMessages((prev) =>
+        prev.map((p) =>
+          p.temp_id === tempId
+            ? {
+                ...p,
+                messages: [{ ...userMessage, save_status: "pending" }],
+                error: undefined,
+              }
+            : p
+        )
+      );
+
+      await sendMessageToAI(userMessage, {
+        ...pendingGroup,
+        messages: [userMessage],
+      });
+    },
+    [pendingMessages, sendMessageToAI]
+  );
+
+  // 切换版本
+  const switchVersion = useCallback(
+    async (sessionId: string, versionIndex: number) => {
+      try {
+        // 找到指定版本的消息
+        const sessionMessages = rawMessages.filter(
+          (m) => m.session_id === sessionId
+        );
+        const versions =
+          sessionGroups.find((sg) => sg.session_id === sessionId)?.versions ||
+          [];
+
+        if (versionIndex >= versions.length) return;
+
+        const targetVersion = versions[versionIndex];
+        const versionMessages = sessionMessages.filter(
+          (m) => m.created_at === targetVersion.created_at
+        );
+
+        // 调用API更新激活状态
+        await messageApi.switchVersion(
+          chatId,
+          versionMessages.map((m) => m.uid)
+        );
+
+        // 重新加载数据
+        await loadMessages();
+      } catch (err) {
+        console.error("切换版本失败:", err);
+        setError(err instanceof Error ? err.message : "切换版本失败");
+      }
+    },
+    [rawMessages, sessionGroups, chatId, loadMessages]
+  );
+
+  // 刷新AI回答
+  const refreshResponse = useCallback(
+    async (sessionId: string, modelId?: string) => {
+      const session = sessionGroups.find((sg) => sg.session_id === sessionId);
+      if (!session?.user_message) return;
+
+      // 使用指定的模型或当前模型
+      const useModel = modelId || currentModel;
+      const tempId = `temp_refresh_${Date.now()}`;
+
+      try {
+        // 创建基于原用户消息的新AI回答
+        const userMsg = session.user_message;
+        const newSessionId = `session_${tempId}`;
+
+        const pendingGroup: PendingMessage = {
+          temp_id: tempId,
+          session_id: newSessionId,
+          messages: [
+            {
+              ...userMsg,
+              temp_id: tempId,
+              session_id: newSessionId,
+              save_status: "pending",
+            },
+          ],
+          retry_count: 0,
+          created_at: new Date().toISOString(),
+        };
+
+        setPendingMessages((prev) => [...prev, pendingGroup]);
+
+        // 发送给AI获取新回答
+        await sendMessageToAI(pendingGroup.messages[0], pendingGroup);
+      } catch (err) {
+        console.error("刷新回答失败:", err);
+        setError(err instanceof Error ? err.message : "刷新失败");
+      }
+    },
+    [sessionGroups, currentModel, sendMessageToAI]
+  );
+
+  // 消息操作功能
+  const likeMessage = useCallback(async (messageId: string) => {
+    try {
+      await messageApi.likeMessage(messageId);
+      // 可以添加本地状态更新
+    } catch (err) {
+      console.error("点赞失败:", err);
+    }
+  }, []);
+
+  const dislikeMessage = useCallback(async (messageId: string) => {
+    try {
+      await messageApi.dislikeMessage(messageId);
+      // 可以添加本地状态更新
+    } catch (err) {
+      console.error("点踩失败:", err);
+    }
+  }, []);
+
+  const copyMessage = useCallback(
+    (messageId: string) => {
+      const message = allMessages.find((m) => m.uid === messageId);
+      if (message?.content) {
+        navigator.clipboard.writeText(message.content);
+      }
+    },
+    [allMessages]
+  );
+
+  const shareMessage = useCallback(
+    async (messageId: string): Promise<string> => {
+      try {
+        const response = await messageApi.shareMessage(messageId);
+        return response.data.shareUrl;
+      } catch (err) {
+        console.error("分享失败:", err);
+        throw err;
+      }
+    },
+    []
+  );
+
+  const deleteMessage = useCallback(
+    async (messageId: string) => {
+      try {
+        await messageApi.deleteMessage(messageId);
+        await loadMessages(); // 重新加载数据
+      } catch (err) {
+        console.error("删除失败:", err);
+        setError(err instanceof Error ? err.message : "删除失败");
+      }
+    },
+    [loadMessages]
+  );
+
+  return {
+    chatState: {
+      chat_id: chatId,
+      title: "", // 可以从props传入或另行管理
+      raw_messages: rawMessages,
+      active_path: activePath,
+      session_groups: sessionGroups,
+      pending_messages: pendingMessages,
+      is_loading: isLoading,
+      streaming_message: streamingMessage,
+      streaming_session_id: streamingSessionId,
+      current_model: currentModel,
+      error,
+    },
+    actions: {
+      switchVersion,
+      editMessage,
+      retryMessage,
+      refreshResponse,
+      loadMessages,
+      likeMessage,
+      dislikeMessage,
+      copyMessage,
+      shareMessage,
+      deleteMessage,
+    },
+  };
+}
+```
+
+## 6. API 调用层
+
+### services/chatApi.ts
+
+```typescript
+import { CreateChatRequest, ChatResponse, ApiResponse } from "../types/chat";
+
+export const chatApi = {
+  async createChat(
+    request: CreateChatRequest
+  ): Promise<ApiResponse<ChatResponse>> {
+    const response = await fetch("/api/v2/chats", {
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      body: JSON.stringify(request),
+    });
+    return response.json();
+  },
+
+  async getChat(chatId: string): Promise<ApiResponse<ChatResponse>> {
+    const response = await fetch(`/api/v2/chats/${chatId}`);
+    return response.json();
+  },
+
+  async updateChat(
+    chatId: string,
+    updates: Partial<CreateChatRequest>
+  ): Promise<ApiResponse<ChatResponse>> {
+    const response = await fetch(`/api/v2/chats/${chatId}`, {
+      method: "PUT",
+      headers: { "Content-Type": "application/json" },
+      body: JSON.stringify(updates),
+    });
+    return response.json();
+  },
+
+  async deleteChat(chatId: string): Promise<ApiResponse<void>> {
+    const response = await fetch(`/api/v2/chats/${chatId}`, {
+      method: "DELETE",
+    });
+    return response.json();
+  },
+};
+```
+
+### services/messageApi.ts
+
+```typescript
+import {
+  CreateMessageRequest,
+  MessageListResponse,
+  ApiResponse,
+  MessageNode,
+} from "../types/chat";
+
+export const messageApi = {
+  async getMessages(chatId: string): Promise<ApiResponse<MessageListResponse>> {
+    const response = await fetch(`/api/v2/chat-messages?chat=${chatId}`);
+    return response.json();
+  },
+
+  async createMessages(
+    chatId: string,
+    request: CreateMessageRequest
+  ): Promise<ApiResponse<MessageNode[]>> {
+    const response = await fetch("/api/v2/chat-messages", {
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      body: JSON.stringify({
+        chat_id: chatId,
+        ...request,
+      }),
+    });
+    return response.json();
+  },
+
+  async switchVersion(
+    chatId: string,
+    messageUids: string[]
+  ): Promise<ApiResponse<void>> {
+    const response = await fetch("/api/v2/chat-messages/switch-version", {
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      body: JSON.stringify({
+        chat_id: chatId,
+        message_uids: messageUids,
+      }),
+    });
+    return response.json();
+  },
+
+  async likeMessage(messageId: string): Promise<ApiResponse<void>> {
+    const response = await fetch(`/api/v2/chat-messages/${messageId}/like`, {
+      method: "POST",
+    });
+    return response.json();
+  },
+
+  async dislikeMessage(messageId: string): Promise<ApiResponse<void>> {
+    const response = await fetch(`/api/v2/chat-messages/${messageId}/dislike`, {
+      method: "POST",
+    });
+    return response.json();
+  },
+
+  async shareMessage(
+    messageId: string
+  ): Promise<ApiResponse<{ shareUrl: string }>> {
+    const response = await fetch(`/api/v2/chat-messages/${messageId}/share`, {
+      method: "POST",
+    });
+    return response.json();
+  },
+
+  async deleteMessage(messageId: string): Promise<ApiResponse<void>> {
+    const response = await fetch(`/api/v2/chat-messages/${messageId}`, {
+      method: "DELETE",
+    });
+    return response.json();
+  },
+};
+```
+
+### services/modelAdapters/base.ts
+
+```typescript
+import {
+  ModelAdapter,
+  OpenAIMessage,
+  SendOptions,
+  ParsedChunk,
+  ToolCall,
+} from "../../types/chat";
+
+export abstract class BaseModelAdapter implements ModelAdapter {
+  abstract name: string;
+  abstract supportsFunctionCall: boolean;
+
+  abstract sendMessage(
+    messages: OpenAIMessage[],
+    options: SendOptions
+  ): Promise<AsyncIterable<string>>;
+  abstract parseStreamChunk(chunk: string): ParsedChunk | null;
+  abstract handleFunctionCall(functionCall: ToolCall): Promise<any>;
+
+  protected createStreamController() {
+    return {
+      addToken: (token: string) => {
+        // 流式输出控制逻辑
+      },
+      complete: () => {
+        // 完成处理逻辑
+      },
+    };
+  }
+
+  protected buildRequestPayload(
+    messages: OpenAIMessage[],
+    options: SendOptions
+  ) {
+    return {
+      model: this.name,
+      messages,
+      stream: true,
+      temperature: options.temperature || 0.7,
+      max_tokens: options.max_tokens || 2048,
+      top_p: options.top_p || 1,
+      functions: options.functions,
+      function_call: options.function_call || "auto",
+    };
+  }
+}
+```
+
+### services/modelAdapters/openai.ts
+
+```typescript
+import { BaseModelAdapter } from "./base";
+import {
+  OpenAIMessage,
+  SendOptions,
+  ParsedChunk,
+  ToolCall,
+} from "../../types/chat";
+
+export class OpenAIAdapter extends BaseModelAdapter {
+  name = "gpt-4";
+  supportsFunctionCall = true;
+
+  async *sendMessage(
+    messages: OpenAIMessage[],
+    options: SendOptions
+  ): AsyncIterable<string> {
+    const payload = this.buildRequestPayload(messages, options);
+
+    const response = await fetch(process.env.REACT_APP_OPENAI_PROXY!, {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+        Authorization: `Bearer ${process.env.REACT_APP_OPENAI_KEY}`,
+      },
+      body: JSON.stringify({
+        model_id: "gpt-4",
+        payload,
+      }),
+    });
+
+    if (!response.ok) {
+      throw new Error(`HTTP error! status: ${response.status}`);
+    }
+
+    const reader = response.body?.getReader();
+    if (!reader) {
+      throw new Error("无法获取响应流");
+    }
+
+    const decoder = new TextDecoder();
+    let buffer = "";
+
+    try {
+      while (true) {
+        const { done, value } = await reader.read();
+        if (done) break;
+
+        buffer += decoder.decode(value, { stream: true });
+        const lines = buffer.split("\n");
+        buffer = lines.pop() || "";
+
+        for (const line of lines) {
+          if (!line.trim() || !line.startsWith("data: ")) continue;
+          const data = line.slice(6);
+          if (data === "[DONE]") return;
+
+          yield data;
+        }
+      }
+    } finally {
+      reader.releaseLock();
+    }
+  }
+
+  parseStreamChunk(chunk: string): ParsedChunk | null {
+    try {
+      const parsed = JSON.parse(chunk);
+      const delta = parsed.choices?.[0]?.delta;
+      const finishReason = parsed.choices?.[0]?.finish_reason;
+
+      return {
+        content: delta?.content,
+        function_call: delta?.function_call,
+        finish_reason: finishReason,
+      };
+    } catch {
+      return null;
+    }
+  }
+
+  async handleFunctionCall(functionCall: ToolCall): Promise<any> {
+    // 实现具体的函数调用逻辑
+    // 这里应该根据function name调用相应的处理器
+
+    switch (functionCall.function) {
+      case "searchTerm":
+        return await this.searchTerm(functionCall.arguments.term);
+      case "getWeather":
+        return await this.getWeather(functionCall.arguments.city);
+      default:
+        throw new Error(`未知函数: ${functionCall.function}`);
+    }
+  }
+
+  private async searchTerm(term: string) {
+    // 调用搜索API
+    const response = await fetch(
+      `/v2/search-pali-wbw?view=pali&key=${term}&limit=20&offset=0`
+    );
+    const result = await response.json();
+    return result.ok ? result.data.rows : { error: "搜索失败" };
+  }
+
+  private async getWeather(city: string) {
+    // 模拟天气查询
+    return {
+      city,
+      temperature: "25°C",
+      condition: "晴朗",
+      humidity: "60%",
+    };
+  }
+}
+```
+
+### services/modelAdapters/index.ts
+
+```typescript
+import { ModelAdapter } from "../../types/chat";
+import { OpenAIAdapter } from "./openai";
+
+const adapters = new Map<string, ModelAdapter>();
+
+// 注册适配器
+adapters.set("gpt-4", new OpenAIAdapter());
+adapters.set("gpt-3.5-turbo", new OpenAIAdapter());
+
+export function getModelAdapter(modelId: string): ModelAdapter {
+  const adapter = adapters.get(modelId);
+  if (!adapter) {
+    throw new Error(`未找到模型适配器: ${modelId}`);
+  }
+  return adapter;
+}
+
+export function registerAdapter(modelId: string, adapter: ModelAdapter) {
+  adapters.set(modelId, adapter);
+}
+```
+
+## 7. 显示层组件
+
+### components/ChatContainer.tsx
+
+```typescript
+import React, { useEffect } from "react";
+import { useChatData } from "../hooks/useChatData";
+import { SessionGroup } from "./SessionGroup";
+import { ChatInput } from "./ChatInput";
+import { StreamingMessage } from "./StreamingMessage";
+
+interface ChatContainerProps {
+  chatId: string;
+}
+
+export function ChatContainer({ chatId }: ChatContainerProps) {
+  const { chatState, actions } = useChatData(chatId);
+
+  useEffect(() => {
+    actions.loadMessages();
+  }, [chatId, actions.loadMessages]);
+
+  return (
+    <div className="chat-container">
+      <div className="messages-area">
+        {chatState.session_groups.map((session) => (
+          <SessionGroup
+            key={session.session_id}
+            session={session}
+            onVersionSwitch={actions.switchVersion}
+            onRefresh={actions.refreshResponse}
+            onEdit={actions.editMessage}
+            onRetry={actions.retryMessage}
+            onLike={actions.likeMessage}
+            onDislike={actions.dislikeMessage}
+            onCopy={actions.copyMessage}
+            onShare={actions.shareMessage}
+          />
+        ))}
+
+        {/* 流式消息显示 */}
+        {chatState.streaming_message && (
+          <StreamingMessage
+            content={chatState.streaming_message}
+            sessionId={chatState.streaming_session_id}
+          />
+        )}
+
+        {/* 错误提示 */}
+        {chatState.error && (
+          <div className="error-message">{chatState.error}</div>
+        )}
+      </div>
+
+      <ChatInput
+        onSend={(content) => actions.editMessage("new", content)}
+        disabled={chatState.is_loading}
+        placeholder="输入你的问题..."
+      />
+    </div>
+  );
+}
+```
+
+### components/SessionGroup.tsx
+
+```typescript
+import React from "react";
+import { Button } from "antd";
+import { SessionGroupProps } from "../types/chat";
+import { UserMessage } from "./UserMessage";
+import { AssistantMessage } from "./AssistantMessage";
+import { VersionSwitcher } from "./VersionSwitcher";
+
+export function SessionGroup({
+  session,
+  onVersionSwitch,
+  onRefresh,
+  onEdit,
+  onRetry,
+  onLike,
+  onDislike,
+  onCopy,
+  onShare,
+}: SessionGroupProps) {
+  const hasFailed = session.messages.some((m) => m.save_status === "failed");
+  const isPending = session.messages.some((m) => m.save_status === "pending");
+  const hasMultipleVersions = session.versions.length > 1;
+
+  return (
+    <div
+      className={`session-group ${isPending ? "pending" : ""} ${
+        hasFailed ? "failed" : ""
+      }`}
+    >
+      {/* 用户消息 */}
+      {session.user_message && (
+        <UserMessage
+          message={session.user_message}
+          onEdit={(content) => onEdit && onEdit(session.session_id, content)}
+          onCopy={() => onCopy && onCopy(session.user_message!.uid)}
+        />
+      )}
+
+      {/* AI回答区域 */}
+      <div className="ai-response">
+        {/* 失败重试提示 */}
+        {hasFailed && onRetry && (
+          <div className="retry-section">
+            <span className="error-message">回答生成失败</span>
+            <Button
+              size="small"
+              type="primary"
+              onClick={() => onRetry(session.messages[0].temp_id!)}
+            >
+              重试
+            </Button>
+          </div>
+        )}
+
+        {/* 版本切换器 */}
+        {hasMultipleVersions && !isPending && !hasFailed && (
+          <VersionSwitcher
+            versions={session.versions}
+            currentVersion={session.current_version}
+            onSwitch={(versionIndex) =>
+              onVersionSwitch(session.session_id, versionIndex)
+            }
+          />
+        )}
+
+        {/* AI消息内容 */}
+        {!hasFailed && session.ai_messages.length > 0 && (
+          <AssistantMessage
+            messages={session.ai_messages}
+            onRefresh={() => onRefresh && onRefresh(session.session_id)}
+            onEdit={(content) =>
+              onEdit && onEdit(session.session_id, content, "assistant")
+            }
+            isPending={isPending}
+            onLike={onLike}
+            onDislike={onDislike}
+            onCopy={onCopy}
+            onShare={onShare}
+          />
+        )}
+      </div>
+    </div>
+  );
+}
+```
+
+### components/UserMessage.tsx
+
+```typescript
+import React, { useState } from "react";
+import { Button, Input } from "antd";
+import { EditOutlined, CopyOutlined } from "@ant-design/icons";
+import { UserMessageProps } from "../types/chat";
+
+const { TextArea } = Input;
+
+export function UserMessage({ message, onEdit, onCopy }: UserMessageProps) {
+  const [isEditing, setIsEditing] = useState(false);
+  const [editContent, setEditContent] = useState(message.content || "");
+
+  const handleEdit = () => {
+    if (onEdit && editContent.trim()) {
+      onEdit(editContent.trim());
+      setIsEditing(false);
+    }
+  };
+
+  const handleCancel = () => {
+    setEditContent(message.content || "");
+    setIsEditing(false);
+  };
+
+  return (
+    <div className="user-message">
+      <div className="message-header">
+        <span className="role-label">You</span>
+        <div className="message-actions">
+          {!isEditing && (
+            <>
+              <Button
+                size="small"
+                type="text"
+                icon={<EditOutlined />}
+                onClick={() => setIsEditing(true)}
+              />
+              <Button
+                size="small"
+                type="text"
+                icon={<CopyOutlined />}
+                onClick={onCopy}
+              />
+            </>
+          )}
+        </div>
+      </div>
+
+      <div className="message-content">
+        {isEditing ? (
+          <div className="edit-area">
+            <TextArea
+              value={editContent}
+              onChange={(e) => setEditContent(e.target.value)}
+              autoSize={{ minRows: 2, maxRows: 8 }}
+              autoFocus
+            />
+            <div className="edit-actions">
+              <Button size="small" onClick={handleCancel}>
+                取消
+              </Button>
+              <Button size="small" type="primary" onClick={handleEdit}>
+                保存
+              </Button>
+            </div>
+          </div>
+        ) : (
+          <div className="message-text">
+            {message.content}
+            {message.save_status === "pending" && (
+              <span className="status-indicator pending">发送中...</span>
+            )}
+            {message.save_status === "failed" && (
+              <span className="status-indicator failed">发送失败</span>
+            )}
+          </div>
+        )}
+      </div>
+    </div>
+  );
+}
+```
+
+### components/AssistantMessage.tsx
+
+```typescript
+import React from "react";
+import { Button, Space } from "antd";
+import {
+  RefreshOutlined,
+  LikeOutlined,
+  DislikeOutlined,
+  CopyOutlined,
+  ShareOutlined,
+} from "@ant-design/icons";
+import { AssistantMessageProps } from "../types/chat";
+
+export function AssistantMessage({
+  messages,
+  onRefresh,
+  onEdit,
+  isPending,
+  onLike,
+  onDislike,
+  onCopy,
+  onShare,
+}: AssistantMessageProps) {
+  const mainMessage = messages.find((m) => m.role === "assistant" && m.content);
+  const toolMessages = messages.filter((m) => m.role === "tool");
+
+  const handleCopy = () => {
+    if (mainMessage?.content && onCopy) {
+      onCopy(mainMessage.uid);
+    }
+  };
+
+  const handleShare = async () => {
+    if (mainMessage && onShare) {
+      try {
+        const shareUrl = await onShare(mainMessage.uid);
+        // 可以显示分享链接或复制到剪贴板
+        navigator.clipboard.writeText(shareUrl);
+      } catch (err) {
+        console.error("分享失败:", err);
+      }
+    }
+  };
+
+  return (
+    <div className="assistant-message">
+      <div className="message-header">
+        <span className="role-label">Assistant</span>
+        {mainMessage?.model_id && (
+          <span className="model-info">{mainMessage.model_id}</span>
+        )}
+
+        {!isPending && (
+          <div className="message-actions">
+            <Space size="small">
+              <Button
+                size="small"
+                type="text"
+                icon={<RefreshOutlined />}
+                onClick={onRefresh}
+              />
+              <Button
+                size="small"
+                type="text"
+                icon={<LikeOutlined />}
+                onClick={() => mainMessage && onLike && onLike(mainMessage.uid)}
+              />
+              <Button
+                size="small"
+                type="text"
+                icon={<DislikeOutlined />}
+                onClick={() =>
+                  mainMessage && onDislike && onDislike(mainMessage.uid)
+                }
+              />
+              <Button
+                size="small"
+                type="text"
+                icon={<CopyOutlined />}
+                onClick={handleCopy}
+              />
+              <Button
+                size="small"
+                type="text"
+                icon={<ShareOutlined />}
+                onClick={handleShare}
+              />
+            </Space>
+          </div>
+        )}
+      </div>
+
+      <div className="message-content">
+        {/* Tool calls 显示 */}
+        {toolMessages.length > 0 && (
+          <div className="tool-calls">
+            {toolMessages.map((toolMsg, index) => (
+              <div key={toolMsg.uid} className="tool-result">
+                <span className="tool-label">Tool {index + 1}</span>
+                <div className="tool-content">{toolMsg.content}</div>
+              </div>
+            ))}
+          </div>
+        )}
+
+        {/* 主要回答内容 */}
+        {mainMessage?.content && (
+          <div className="message-text">
+            {mainMessage.content}
+            {isPending && (
+              <span className="status-indicator pending">生成中...</span>
+            )}
+          </div>
+        )}
+
+        {/* Token 使用信息 */}
+        {mainMessage?.metadata?.token_usage && (
+          <div className="token-info">
+            Token: {mainMessage.metadata.token_usage.total_tokens}
+          </div>
+        )}
+      </div>
+    </div>
+  );
+}
+```
+
+### components/VersionSwitcher.tsx
+
+```typescript
+import React from "react";
+import { Button, Space, Tooltip } from "antd";
+import { LeftOutlined, RightOutlined } from "@ant-design/icons";
+import { VersionSwitcherProps } from "../types/chat";
+
+export function VersionSwitcher({
+  versions,
+  currentVersion,
+  onSwitch,
+}: VersionSwitcherProps) {
+  if (versions.length <= 1) return null;
+
+  const canGoPrev = currentVersion > 0;
+  const canGoNext = currentVersion < versions.length - 1;
+
+  const currentVersionInfo = versions[currentVersion];
+
+  return (
+    <div className="version-switcher">
+      <Space align="center">
+        <Button
+          size="small"
+          type="text"
+          icon={<LeftOutlined />}
+          disabled={!canGoPrev}
+          onClick={() => canGoPrev && onSwitch(currentVersion - 1)}
+        />
+
+        <Tooltip
+          title={
+            <div>
+              <div>
+                版本 {currentVersion + 1} / {versions.length}
+              </div>
+              <div>模型: {currentVersionInfo.model_id || "未知"}</div>
+              <div>
+                创建: {new Date(currentVersionInfo.created_at).toLocaleString()}
+              </div>
+              {currentVersionInfo.token_usage && (
+                <div>Token: {currentVersionInfo.token_usage}</div>
+              )}
+            </div>
+          }
+        >
+          <span className="version-info">
+            {currentVersion + 1} / {versions.length}
+          </span>
+        </Tooltip>
+
+        <Button
+          size="small"
+          type="text"
+          icon={<RightOutlined />}
+          disabled={!canGoNext}
+          onClick={() => canGoNext && onSwitch(currentVersion + 1)}
+        />
+      </Space>
+    </div>
+  );
+}
+```
+
+### components/ChatInput.tsx
+
+```typescript
+import React, { useState, useCallback } from "react";
+import { Button, Input, Space } from "antd";
+import { SendOutlined, PaperClipOutlined } from "@ant-design/icons";
+import { ChatInputProps } from "../types/chat";
+
+const { TextArea } = Input;
+
+export function ChatInput({ onSend, disabled, placeholder }: ChatInputProps) {
+  const [inputValue, setInputValue] = useState("");
+
+  const handleSend = useCallback(() => {
+    if (!inputValue.trim() || disabled) return;
+
+    onSend(inputValue.trim());
+    setInputValue("");
+  }, [inputValue, disabled, onSend]);
+
+  const handleKeyPress = useCallback(
+    (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
+      if (e.key === "Enter" && !e.shiftKey) {
+        e.preventDefault();
+        handleSend();
+      }
+    },
+    [handleSend]
+  );
+
+  return (
+    <div className="chat-input">
+      <div className="input-area">
+        <TextArea
+          value={inputValue}
+          onChange={(e) => setInputValue(e.target.value)}
+          onKeyPress={handleKeyPress}
+          placeholder={placeholder || "输入你的问题..."}
+          autoSize={{ minRows: 1, maxRows: 6 }}
+          disabled={disabled}
+        />
+
+        <div className="input-actions">
+          <Space>
+            <Button
+              size="small"
+              type="text"
+              icon={<PaperClipOutlined />}
+              disabled={disabled}
+            />
+            <Button
+              type="primary"
+              icon={<SendOutlined />}
+              onClick={handleSend}
+              disabled={!inputValue.trim() || disabled}
+            />
+          </Space>
+        </div>
+      </div>
+    </div>
+  );
+}
+```
+
+### components/StreamingMessage.tsx
+
+```typescript
+import React from "react";
+
+interface StreamingMessageProps {
+  content: string;
+  sessionId?: string;
+}
+
+export function StreamingMessage({ content }: StreamingMessageProps) {
+  return (
+    <div className="streaming-message">
+      <div className="message-header">
+        <span className="role-label">Assistant</span>
+        <span className="streaming-indicator">正在生成中...</span>
+      </div>
+
+      <div className="message-content">
+        <div className="message-text">
+          {content}
+          <span className="cursor">|</span>
+        </div>
+      </div>
+    </div>
+  );
+}
+```
+
+## 8. 使用示例
+
+### 基本使用
+
+```typescript
+import React from "react";
+import { ChatContainer } from "./components/ChatContainer";
+
+function App() {
+  const chatId = "your-chat-id";
+
+  return (
+    <div className="app">
+      <ChatContainer chatId={chatId} />
+    </div>
+  );
+}
+
+export default App;
+```
+
+### 样式示例 (CSS)
+
+```css
+.chat-container {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+}
+
+.messages-area {
+  flex: 1;
+  overflow-y: auto;
+  padding: 16px;
+}
+
+.session-group {
+  margin-bottom: 24px;
+  border-radius: 8px;
+  padding: 16px;
+}
+
+.session-group.pending {
+  opacity: 0.7;
+}
+
+.session-group.failed {
+  border: 1px solid #ff4d4f;
+  background-color: #fff2f0;
+}
+
+.user-message,
+.assistant-message {
+  margin-bottom: 12px;
+}
+
+.message-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8px;
+}
+
+.role-label {
+  font-weight: bold;
+  color: #1890ff;
+}
+
+.message-content {
+  padding: 12px;
+  border-radius: 6px;
+  background-color: #f5f5f5;
+}
+
+.version-switcher {
+  margin: 8px 0;
+  text-align: center;
+}
+
+.chat-input {
+  border-top: 1px solid #d9d9d9;
+  padding: 16px;
+}
+
+.streaming-message .cursor {
+  animation: blink 1s infinite;
+}
+
+@keyframes blink {
+  0%,
+  50% {
+    opacity: 1;
+  }
+  51%,
+  100% {
+    opacity: 0;
+  }
+}
+
+.retry-section {
+  padding: 12px;
+  background-color: #fff2f0;
+  border: 1px solid #ffccc7;
+  border-radius: 6px;
+  margin-bottom: 12px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.error-message {
+  color: #ff4d4f;
+  font-size: 14px;
+}
+
+.status-indicator {
+  margin-left: 8px;
+  font-size: 12px;
+}
+
+.status-indicator.pending {
+  color: #1890ff;
+}
+
+.status-indicator.failed {
+  color: #ff4d4f;
+}
+
+.tool-calls {
+  margin-bottom: 12px;
+  padding: 8px;
+  background-color: #f0f0f0;
+  border-radius: 4px;
+}
+
+.tool-result {
+  margin-bottom: 8px;
+}
+
+.tool-label {
+  font-size: 12px;
+  color: #666;
+  font-weight: bold;
+}
+
+.tool-content {
+  margin-top: 4px;
+  font-family: monospace;
+  font-size: 12px;
+  padding: 4px;
+  background-color: #fff;
+  border-radius: 2px;
+}
+
+.token-info {
+  margin-top: 8px;
+  font-size: 12px;
+  color: #666;
+  text-align: right;
+}
+
+.edit-area {
+  margin-top: 8px;
+}
+
+.edit-actions {
+  margin-top: 8px;
+  text-align: right;
+}
+
+.edit-actions .ant-btn {
+  margin-left: 8px;
+}
+
+.input-area {
+  position: relative;
+}
+
+.input-actions {
+  position: absolute;
+  right: 8px;
+  bottom: 8px;
+  display: flex;
+  align-items: center;
+}
+
+.version-info {
+  font-size: 12px;
+  color: #666;
+  padding: 0 8px;
+  user-select: none;
+}
+
+.model-info {
+  font-size: 12px;
+  color: #666;
+  background-color: #f0f0f0;
+  padding: 2px 6px;
+  border-radius: 4px;
+}
+
+.streaming-indicator {
+  font-size: 12px;
+  color: #1890ff;
+  animation: pulse 1.5s infinite;
+}
+
+@keyframes pulse {
+  0% {
+    opacity: 0.6;
+  }
+  50% {
+    opacity: 1;
+  }
+  100% {
+    opacity: 0.6;
+  }
+}
+```
+
+## 9. 核心流程说明
+
+### 9.1 消息发送流程
+
+1. **用户输入** → `ChatInput.onSend` → `actions.editMessage('new', content)`
+2. **创建临时消息** → 显示用户消息(pending 状态)
+3. **调用 AI API** → 流式显示 AI 回答 → `StreamingMessage`
+4. **Function Call 处理** → 循环处理工具调用
+5. **保存到数据库** → 批量保存整个对话组
+6. **状态同步** → 更新本地状态,移除临时标记
+
+### 9.2 版本切换流程
+
+1. **用户点击版本按钮** → `VersionSwitcher.onSwitch`
+2. **计算目标版本** → 找到对应版本的消息组
+3. **调用 API 更新** → 更新数据库激活状态
+4. **重新加载数据** → 刷新本地状态
+5. **重新渲染** → 显示新版本内容
+
+### 9.3 消息编辑流程
+
+1. **用户点击编辑** → `UserMessage` 进入编辑模式
+2. **用户确认修改** → 创建新版本消息
+3. **自动获取 AI 回答** → 发送新问题到 AI
+4. **保存新对话链** → 批量保存到数据库
+
+### 9.4 失败重试流程
+
+1. **检测失败状态** → `save_status: 'failed'`
+2. **显示重试按钮** → `SessionGroup` 渲染重试 UI
+3. **用户点击重试** → `actions.retryMessage(tempId)`
+4. **重新发送请求** → 使用原始用户消息重新调用 AI
+5. **更新状态** → 成功则保存,失败则继续显示重试
+
+## 10. 扩展功能
+
+### 10.1 多模型支持
+
+```typescript
+// 添加新的模型适配器
+class ClaudeAdapter extends BaseModelAdapter {
+  name = "claude-3-sonnet";
+  supportsFunctionCall = true;
+
+  async *sendMessage(messages: OpenAIMessage[], options: SendOptions) {
+    // Claude API 特定实现
+  }
+
+  parseStreamChunk(chunk: string): ParsedChunk | null {
+    // Claude 响应格式解析
+  }
+
+  async handleFunctionCall(functionCall: ToolCall): Promise<any> {
+    // Claude function call 处理
+  }
+}
+
+// 注册新适配器
+registerAdapter("claude-3-sonnet", new ClaudeAdapter());
+```
+
+### 10.2 消息搜索
+
+```typescript
+const searchMessages = useCallback(
+  async (query: string) => {
+    const response = await messageApi.searchMessages(chatId, query);
+    return response.data;
+  },
+  [chatId]
+);
+```
+
+### 10.3 导出功能
+
+```typescript
+const exportChat = useCallback(
+  async (format: "json" | "markdown" | "pdf") => {
+    const response = await chatApi.exportChat(chatId, format);
+    // 处理导出文件
+  },
+  [chatId]
+);
+```
+
+### 10.4 实时协作
+
+```typescript
+// WebSocket 连接处理多用户协作
+const useRealtimeSync = (chatId: string) => {
+  useEffect(() => {
+    const ws = new WebSocket(`ws://localhost:8080/chat/${chatId}`);
+
+    ws.onmessage = (event) => {
+      const update = JSON.parse(event.data);
+      // 处理其他用户的消息更新
+    };
+
+    return () => ws.close();
+  }, [chatId]);
+};
+```
+
+## 11. 性能优化
+
+### 11.1 渲染优化
+
+- 使用 React.memo 防止不必要的重新渲染
+- 消息列表虚拟化(react-window)
+- 懒加载历史消息版本
+
+### 11.2 网络优化
+
+- 请求去重和缓存
+- 批量 API 调用
+- 断点续传支持
+
+### 11.3 内存管理
+
+- 限制内存中保存的消息数量
+- 及时清理未使用的临时状态
+- 图片和文件的懒加载
+
+## 12. 测试策略
+
+### 12.1 单元测试
+
+- Hook 功能测试:`useChatData`, `useActivePath`, `useSessionGroups`
+- 组件渲染测试:所有 UI 组件的基本渲染
+- API 服务测试:模拟 API 响应
+
+### 12.2 集成测试
+
+- 完整对话流程测试
+- 版本切换功能测试
+- 错误处理和重试测试
+
+### 12.3 端到端测试
+
+- 用户操作模拟:发送消息、编辑、版本切换
+- 流式响应测试
+- 多标签页同步测试
+
+## 13. 部署配置
+
+### 13.1 环境变量
+
+```env
+REACT_APP_API_BASE_URL=https://api.yourapp.com
+REACT_APP_OPENAI_PROXY=https://api.yourapp.com/v2/openai-proxy
+REACT_APP_OPENAI_KEY=your_openai_key
+REACT_APP_WS_URL=wss://api.yourapp.com/ws
+```
+
+### 13.2 构建优化
+
+```json
+{
+  "scripts": {
+    "build": "react-scripts build",
+    "build:analyze": "npm run build && npx webpack-bundle-analyzer build/static/js/*.js"
+  }
+}
+```
+
+## 14. 总结
+
+这个重构方案提供了:
+
+1. **清晰的架构分层**:数据管理、API 调用、UI 显示职责明确
+2. **完整的类型定义**:所有类型统一在 `chat.ts` 中管理
+3. **灵活的模型适配**:支持多厂商 AI 模型的无缝集成
+4. **优秀的用户体验**:乐观更新、实时反馈、错误恢复
+5. **强大的版本管理**:消息版本控制和切换功能
+6. **可扩展的设计**:支持未来功能扩展和性能优化
+
+设计遵循了 React 最佳实践,使用现代 Hooks 模式,确保代码的可维护性和可测试性。通过模块化设计,各个部分可以独立开发和测试,便于团队协作。
+
+## 建议开发策略
+
+1. 第一阶段:直接使用类型定义和组件结构
+2. 第二阶段:实现核心 Hook 逻辑,先不考虑流式处理
+3. 第三阶段:完善 API 调用和流式处理
+4. 第四阶段:优化错误处理和用户体验