feat: Add AI report generation with DIFY integration

- Add Users table for display name resolution from AD authentication
- Integrate DIFY AI service for report content generation
- Create docx assembly service with image embedding from MinIO
- Add REST API endpoints for report generation and download
- Add WebSocket notifications for generation progress
- Add frontend UI with progress modal and download functionality
- Add integration tests for report generation flow

Report sections (Traditional Chinese):
- 事件摘要 (Summary)
- 時間軸 (Timeline)
- 參與人員 (Participants)
- 處理過程 (Resolution Process)
- 目前狀態 (Current Status)
- 最終處置結果 (Final Resolution)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
egg
2025-12-04 18:32:40 +08:00
parent 77091eefb5
commit 3927441103
32 changed files with 4374 additions and 8 deletions

View File

@@ -31,6 +31,9 @@ class ADAuthService:
Dict containing:
- token: AD authentication token
- username: Display name from AD
- email: User email address
- office_location: Office location (optional)
- job_title: Job title (optional)
- expires_at: Estimated token expiry datetime
Raises:
@@ -58,6 +61,9 @@ class ADAuthService:
ad_token = token_data.get("access_token")
user_info = token_data.get("userInfo", {})
display_name = user_info.get("name") or username
email = user_info.get("email") or username
office_location = user_info.get("officeLocation")
job_title = user_info.get("jobTitle")
if not ad_token:
raise ValueError("No token received from AD API")
@@ -74,7 +80,14 @@ class ADAuthService:
# Fallback: assume 1 hour if not provided
expires_at = datetime.utcnow() + timedelta(hours=1)
return {"token": ad_token, "username": display_name, "expires_at": expires_at}
return {
"token": ad_token,
"username": display_name,
"email": email,
"office_location": office_location,
"job_title": job_title,
"expires_at": expires_at,
}
except httpx.HTTPStatusError as e:
# Authentication failed (401) or other HTTP errors

View File

@@ -0,0 +1,89 @@
"""User service for permanent user information storage
This service handles upsert operations for the users table,
which stores display names and metadata for report generation.
"""
from datetime import datetime
from typing import Optional
from sqlalchemy.orm import Session
from app.modules.auth.models import User
def upsert_user(
db: Session,
user_id: str,
display_name: str,
office_location: Optional[str] = None,
job_title: Optional[str] = None,
) -> User:
"""Create or update user record with AD information
This function is called on every successful login to keep
user information up to date. Uses SQLAlchemy merge for
atomic upsert operation.
Args:
db: Database session
user_id: User email address (primary key)
display_name: Display name from AD API
office_location: Office location from AD API (optional)
job_title: Job title from AD API (optional)
Returns:
User: The created or updated user record
"""
# Check if user exists
existing_user = db.query(User).filter(User.user_id == user_id).first()
if existing_user:
# Update existing user
existing_user.display_name = display_name
existing_user.office_location = office_location
existing_user.job_title = job_title
existing_user.last_login_at = datetime.utcnow()
db.commit()
db.refresh(existing_user)
return existing_user
else:
# Create new user
new_user = User(
user_id=user_id,
display_name=display_name,
office_location=office_location,
job_title=job_title,
last_login_at=datetime.utcnow(),
created_at=datetime.utcnow(),
)
db.add(new_user)
db.commit()
db.refresh(new_user)
return new_user
def get_user_by_id(db: Session, user_id: str) -> Optional[User]:
"""Get user by user_id (email)
Args:
db: Database session
user_id: User email address
Returns:
User or None if not found
"""
return db.query(User).filter(User.user_id == user_id).first()
def get_display_name(db: Session, user_id: str) -> str:
"""Get display name for a user, falling back to email if not found
Args:
db: Database session
user_id: User email address
Returns:
Display name or email address as fallback
"""
user = get_user_by_id(db, user_id)
if user:
return user.display_name
return user_id # Fallback to email if user not in database