feat: Excel template export with meeting number auto-generation

- Add meeting_number field (M-YYYYMMDD-XX format) with auto-generation
- Refactor Excel export to use cell coordinates instead of placeholders
- Export files saved to backend/record/ directory with meeting number filename
- Add database migration for meeting_number column
- Add start.sh script for managing frontend/backend/sidecar services
- Update OpenSpec documentation

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
egg
2025-12-11 19:45:53 +08:00
parent a4a2fc3ae7
commit e790f48967
16 changed files with 1139 additions and 122 deletions

3
.gitignore vendored
View File

@@ -47,3 +47,6 @@ Thumbs.db
# Logs # Logs
*.log *.log
logs/ logs/
# Generated Excel records
backend/record/

View File

@@ -96,6 +96,7 @@ class MeetingUpdate(BaseModel):
class MeetingResponse(BaseModel): class MeetingResponse(BaseModel):
meeting_id: int meeting_id: int
uuid: str uuid: str
meeting_number: Optional[str] = None
subject: str subject: str
meeting_time: datetime meeting_time: datetime
location: Optional[str] = None location: Optional[str] = None

View File

@@ -11,11 +11,67 @@ from .auth import get_current_user, is_admin
router = APIRouter() router = APIRouter()
TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "..", "templates") # Directory paths
TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "template")
RECORD_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "record")
def fill_template_workbook(
wb: Workbook, meeting: dict, conclusions: list, actions: list
) -> Workbook:
"""Fill Excel template with meeting data using cell coordinates."""
ws = wb.active
# Fill meeting metadata using cell coordinates
# D3: 會議主題
ws["D3"] = meeting.get("subject", "")
# D4: 會議時間
meeting_time = meeting.get("meeting_time")
if meeting_time:
ws["D4"] = meeting_time.strftime("%Y-%m-%d %H:%M") if hasattr(meeting_time, 'strftime') else str(meeting_time)
# D5: 會議主席
ws["D5"] = meeting.get("chairperson", "")
# F4: 會議地點
ws["F4"] = meeting.get("location", "")
# F5: 記錄人員
ws["F5"] = meeting.get("recorder", "")
# D6: 會議參與人員
ws["D6"] = meeting.get("attendees", "")
# C8: 會議編號
ws["C8"] = meeting.get("meeting_number", "")
# D8: 會議結論 (多條用換行合併)
if conclusions:
conclusion_texts = [c.get("content", "") for c in conclusions]
ws["D8"] = "\n".join(conclusion_texts)
# Enable text wrap for multi-line content
ws["D8"].alignment = Alignment(wrap_text=True, vertical="top")
# Fill action items starting from row 10
# C: 編號, D: 內容, F: 負責人, G: 預計完成日期, H: 執行現況
start_row = 10
for i, action in enumerate(actions):
row = start_row + i
# C column: 代辦事項編號
ws[f"C{row}"] = action.get("system_code", "")
# D column: 事項內容
ws[f"D{row}"] = action.get("content", "")
# F column: 負責人
ws[f"F{row}"] = action.get("owner", "")
# G column: 預計完成日期
due_date = action.get("due_date")
if due_date:
ws[f"G{row}"] = due_date.strftime("%Y-%m-%d") if hasattr(due_date, 'strftime') else str(due_date)
else:
ws[f"G{row}"] = ""
# H column: 執行現況 (status)
ws[f"H{row}"] = action.get("status", "")
return wb
def create_default_workbook(meeting: dict, conclusions: list, actions: list) -> Workbook: def create_default_workbook(meeting: dict, conclusions: list, actions: list) -> Workbook:
"""Create Excel workbook with meeting data.""" """Create Excel workbook with meeting data (fallback when no template)."""
wb = Workbook() wb = Workbook()
ws = wb.active ws = wb.active
ws.title = "Meeting Record" ws.title = "Meeting Record"
@@ -31,77 +87,66 @@ def create_default_workbook(meeting: dict, conclusions: list, actions: list) ->
) )
# Title # Title
ws.merge_cells("A1:F1") ws.merge_cells("A1:H1")
ws["A1"] = "Meeting Record" ws["A1"] = "會議紀錄"
ws["A1"].font = Font(bold=True, size=16) ws["A1"].font = Font(bold=True, size=16)
ws["A1"].alignment = Alignment(horizontal="center") ws["A1"].alignment = Alignment(horizontal="center")
# Metadata section # Metadata section
row = 3 ws["C3"] = "會議主題:"
metadata = [ ws["D3"] = meeting.get("subject", "")
("Subject", meeting.get("subject", "")), ws["C4"] = "會議時間:"
("Date/Time", str(meeting.get("meeting_time", ""))), meeting_time = meeting.get("meeting_time")
("Location", meeting.get("location", "")), ws["D4"] = meeting_time.strftime("%Y-%m-%d %H:%M") if meeting_time and hasattr(meeting_time, 'strftime') else str(meeting_time or "")
("Chairperson", meeting.get("chairperson", "")), ws["C5"] = "會議主席:"
("Recorder", meeting.get("recorder", "")), ws["D5"] = meeting.get("chairperson", "")
("Attendees", meeting.get("attendees", "")), ws["E4"] = "會議地點:"
] ws["F4"] = meeting.get("location", "")
ws["E5"] = "記錄人員:"
ws["F5"] = meeting.get("recorder", "")
ws["C6"] = "參與人員:"
ws["D6"] = meeting.get("attendees", "")
for label, value in metadata: # Meeting number and conclusions
ws[f"A{row}"] = label ws["C8"] = meeting.get("meeting_number", "")
ws[f"A{row}"].font = label_font ws["C8"].font = label_font
ws.merge_cells(f"B{row}:F{row}") if conclusions:
ws[f"B{row}"] = value conclusion_texts = [c.get("content", "") for c in conclusions]
row += 1 ws["D8"] = "\n".join(conclusion_texts)
ws["D8"].alignment = Alignment(wrap_text=True, vertical="top")
# Conclusions section # Action Items header
row += 1 row = 9
ws.merge_cells(f"A{row}:F{row}") headers = ["編號", "事項內容", "", "負責人", "預計完成日期", "執行現況"]
ws[f"A{row}"] = "Conclusions" cols = ["C", "D", "E", "F", "G", "H"]
ws[f"A{row}"].font = header_font for col, header in zip(cols, headers):
row += 1 ws[f"{col}{row}"] = header
ws[f"{col}{row}"].font = label_font
ws[f"{col}{row}"].border = thin_border
ws[f"A{row}"] = "Code" # Action Items data
ws[f"B{row}"] = "Content" start_row = 10
ws[f"A{row}"].font = label_font for i, action in enumerate(actions):
ws[f"B{row}"].font = label_font r = start_row + i
row += 1 ws[f"C{r}"] = action.get("system_code", "")
ws[f"C{r}"].border = thin_border
for c in conclusions: ws[f"D{r}"] = action.get("content", "")
ws[f"A{row}"] = c.get("system_code", "") ws[f"D{r}"].border = thin_border
ws.merge_cells(f"B{row}:F{row}") ws[f"F{r}"] = action.get("owner", "")
ws[f"B{row}"] = c.get("content", "") ws[f"F{r}"].border = thin_border
row += 1 due_date = action.get("due_date")
ws[f"G{r}"] = due_date.strftime("%Y-%m-%d") if due_date and hasattr(due_date, 'strftime') else str(due_date or "")
# Action Items section ws[f"G{r}"].border = thin_border
row += 1 ws[f"H{r}"] = action.get("status", "")
ws.merge_cells(f"A{row}:F{row}") ws[f"H{r}"].border = thin_border
ws[f"A{row}"] = "Action Items"
ws[f"A{row}"].font = header_font
row += 1
headers = ["Code", "Content", "Owner", "Due Date", "Status"]
for col, header in enumerate(headers, 1):
cell = ws.cell(row=row, column=col, value=header)
cell.font = label_font
cell.border = thin_border
row += 1
for a in actions:
ws.cell(row=row, column=1, value=a.get("system_code", "")).border = thin_border
ws.cell(row=row, column=2, value=a.get("content", "")).border = thin_border
ws.cell(row=row, column=3, value=a.get("owner", "")).border = thin_border
ws.cell(row=row, column=4, value=str(a.get("due_date", "") or "")).border = thin_border
ws.cell(row=row, column=5, value=a.get("status", "")).border = thin_border
row += 1
# Adjust column widths # Adjust column widths
ws.column_dimensions["A"].width = 18 ws.column_dimensions["C"].width = 18
ws.column_dimensions["B"].width = 40 ws.column_dimensions["D"].width = 40
ws.column_dimensions["C"].width = 15 ws.column_dimensions["E"].width = 5
ws.column_dimensions["D"].width = 12 ws.column_dimensions["F"].width = 15
ws.column_dimensions["E"].width = 12 ws.column_dimensions["G"].width = 15
ws.column_dimensions["F"].width = 12 ws.column_dimensions["H"].width = 15
return wb return wb
@@ -110,7 +155,7 @@ def create_default_workbook(meeting: dict, conclusions: list, actions: list) ->
async def export_meeting( async def export_meeting(
meeting_id: int, current_user: TokenPayload = Depends(get_current_user) meeting_id: int, current_user: TokenPayload = Depends(get_current_user)
): ):
"""Export meeting to Excel file.""" """Export meeting to Excel file using template."""
with get_db_cursor() as cursor: with get_db_cursor() as cursor:
cursor.execute( cursor.execute(
"SELECT * FROM meeting_records WHERE meeting_id = %s", (meeting_id,) "SELECT * FROM meeting_records WHERE meeting_id = %s", (meeting_id,)
@@ -141,35 +186,35 @@ async def export_meeting(
) )
actions = cursor.fetchall() actions = cursor.fetchall()
# Check for custom template # Check for template file
template_path = os.path.join(TEMPLATE_DIR, "template.xlsx") template_path = os.path.join(TEMPLATE_DIR, "meeting_template.xlsx")
if os.path.exists(template_path): if os.path.exists(template_path):
# Load and fill template
wb = load_workbook(template_path) wb = load_workbook(template_path)
ws = wb.active wb = fill_template_workbook(wb, meeting, conclusions, actions)
# Replace placeholders
for row in ws.iter_rows():
for cell in row:
if cell.value and isinstance(cell.value, str):
cell.value = (
cell.value.replace("{{subject}}", meeting.get("subject", ""))
.replace("{{time}}", str(meeting.get("meeting_time", "")))
.replace("{{location}}", meeting.get("location", ""))
.replace("{{chair}}", meeting.get("chairperson", ""))
.replace("{{recorder}}", meeting.get("recorder", ""))
.replace("{{attendees}}", meeting.get("attendees", ""))
)
else: else:
# Use default template # Use default template generation
wb = create_default_workbook(meeting, conclusions, actions) wb = create_default_workbook(meeting, conclusions, actions)
# Save to bytes buffer # Generate filename with meeting number
meeting_number = meeting.get("meeting_number", "")
if meeting_number:
filename = f"{meeting_number}.xlsx"
else:
filename = f"meeting_{meeting.get('uuid', meeting_id)}.xlsx"
# Ensure record directory exists
os.makedirs(RECORD_DIR, exist_ok=True)
# Save to record directory
record_path = os.path.join(RECORD_DIR, filename)
wb.save(record_path)
# Save to bytes buffer for download
buffer = io.BytesIO() buffer = io.BytesIO()
wb.save(buffer) wb.save(buffer)
buffer.seek(0) buffer.seek(0)
filename = f"meeting_{meeting.get('uuid', meeting_id)}.xlsx"
return StreamingResponse( return StreamingResponse(
buffer, buffer,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",

View File

@@ -28,21 +28,39 @@ def generate_system_code(prefix: str, meeting_date: date, sequence: int) -> str:
def get_next_sequence(cursor, prefix: str, date_str: str) -> int: def get_next_sequence(cursor, prefix: str, date_str: str) -> int:
"""Get next sequence number for a given prefix and date.""" """Get next sequence number for a given prefix and date."""
pattern = f"{prefix}-{date_str}-%" pattern = f"{prefix}-{date_str}-%"
cursor.execute(
""" if prefix == "M":
SELECT system_code FROM meeting_conclusions WHERE system_code LIKE %s # For meeting numbers, search in meeting_records table
UNION cursor.execute(
SELECT system_code FROM meeting_action_items WHERE system_code LIKE %s """
ORDER BY system_code DESC LIMIT 1 SELECT meeting_number FROM meeting_records WHERE meeting_number LIKE %s
""", ORDER BY meeting_number DESC LIMIT 1
(pattern, pattern), """,
) (pattern,),
result = cursor.fetchone() )
if result: result = cursor.fetchone()
last_code = result["system_code"] if result and result.get("meeting_number"):
last_seq = int(last_code.split("-")[-1]) last_code = result["meeting_number"]
return last_seq + 1 last_seq = int(last_code.split("-")[-1])
return 1 return last_seq + 1
return 1
else:
# For conclusions (C) and action items (A)
cursor.execute(
"""
SELECT system_code FROM meeting_conclusions WHERE system_code LIKE %s
UNION
SELECT system_code FROM meeting_action_items WHERE system_code LIKE %s
ORDER BY system_code DESC LIMIT 1
""",
(pattern, pattern),
)
result = cursor.fetchone()
if result:
last_code = result["system_code"]
last_seq = int(last_code.split("-")[-1])
return last_seq + 1
return 1
@router.post("/meetings", response_model=MeetingResponse) @router.post("/meetings", response_model=MeetingResponse)
@@ -56,15 +74,20 @@ async def create_meeting(
date_str = meeting_date.strftime("%Y%m%d") date_str = meeting_date.strftime("%Y%m%d")
with get_db_cursor(commit=True) as cursor: with get_db_cursor(commit=True) as cursor:
# Generate meeting number
meeting_seq = get_next_sequence(cursor, "M", date_str)
meeting_number = generate_system_code("M", meeting_date, meeting_seq)
# Insert meeting record # Insert meeting record
cursor.execute( cursor.execute(
""" """
INSERT INTO meeting_records INSERT INTO meeting_records
(uuid, subject, meeting_time, location, chairperson, recorder, attendees, transcript_blob, created_by) (uuid, meeting_number, subject, meeting_time, location, chairperson, recorder, attendees, transcript_blob, created_by)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""", """,
( (
meeting_uuid, meeting_uuid,
meeting_number,
meeting.subject, meeting.subject,
meeting.meeting_time, meeting.meeting_time,
meeting.location, meeting.location,
@@ -133,6 +156,7 @@ async def create_meeting(
return MeetingResponse( return MeetingResponse(
meeting_id=record["meeting_id"], meeting_id=record["meeting_id"],
uuid=record["uuid"], uuid=record["uuid"],
meeting_number=record.get("meeting_number"),
subject=record["subject"], subject=record["subject"],
meeting_time=record["meeting_time"], meeting_time=record["meeting_time"],
location=record["location"], location=record["location"],
@@ -215,6 +239,7 @@ async def get_meeting(
return MeetingResponse( return MeetingResponse(
meeting_id=record["meeting_id"], meeting_id=record["meeting_id"],
uuid=record["uuid"], uuid=record["uuid"],
meeting_number=record.get("meeting_number"),
subject=record["subject"], subject=record["subject"],
meeting_time=record["meeting_time"], meeting_time=record["meeting_time"],
location=record["location"], location=record["location"],

View File

@@ -0,0 +1,9 @@
-- Migration: Add meeting_number column to meeting_records table
-- Date: 2025-12-11
-- Description: Add meeting_number field for M-YYYYMMDD-XX format
ALTER TABLE meeting_records
ADD COLUMN meeting_number VARCHAR(20) NULL AFTER uuid;
-- Create index for faster lookups
CREATE INDEX idx_meeting_number ON meeting_records(meeting_number);

Binary file not shown.

View File

@@ -0,0 +1,119 @@
"""Test Excel export functionality with template filling."""
import os
import sys
from datetime import datetime, date
from openpyxl import load_workbook
# Add parent directory to path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.routers.export import fill_template_workbook, create_default_workbook, TEMPLATE_DIR, RECORD_DIR
def test_excel_export():
"""Test Excel generation with mock data."""
# Mock meeting data
meeting = {
"meeting_id": 1,
"uuid": "test-uuid-123",
"meeting_number": "M-20251211-01",
"subject": "專案進度討論會議",
"meeting_time": datetime(2025, 12, 11, 14, 30),
"location": "會議室A",
"chairperson": "王經理",
"recorder": "李小明",
"attendees": "張三, 李四, 王五",
"created_by": "test@example.com",
}
# Mock conclusions
conclusions = [
{"conclusion_id": 1, "content": "確認專案時程延後兩週", "system_code": "C-20251211-01"},
{"conclusion_id": 2, "content": "同意增加測試人力", "system_code": "C-20251211-02"},
{"conclusion_id": 3, "content": "下次會議改為線上進行", "system_code": "C-20251211-03"},
]
# Mock action items
actions = [
{
"action_id": 1,
"content": "更新專案時程表",
"owner": "張三",
"due_date": date(2025, 12, 15),
"status": "Open",
"system_code": "A-20251211-01",
},
{
"action_id": 2,
"content": "聯繫人資部門增聘測試人員",
"owner": "李四",
"due_date": date(2025, 12, 20),
"status": "In Progress",
"system_code": "A-20251211-02",
},
{
"action_id": 3,
"content": "準備線上會議設備",
"owner": "王五",
"due_date": None,
"status": "Open",
"system_code": "A-20251211-03",
},
]
# Check paths
template_path = os.path.join(TEMPLATE_DIR, "meeting_template.xlsx")
print(f"Template directory: {TEMPLATE_DIR}")
print(f"Record directory: {RECORD_DIR}")
print(f"Template exists: {os.path.exists(template_path)}")
# Ensure record directory exists
os.makedirs(RECORD_DIR, exist_ok=True)
# Generate filename with meeting number
meeting_number = meeting.get("meeting_number", "")
filename = f"{meeting_number}.xlsx" if meeting_number else f"meeting_{meeting['uuid']}.xlsx"
output_path = os.path.join(RECORD_DIR, filename)
# Test with template if exists
if os.path.exists(template_path):
print("\n=== Testing with template ===")
wb = load_workbook(template_path)
wb = fill_template_workbook(wb, meeting, conclusions, actions)
wb.save(output_path)
print(f"Saved to: {output_path}")
# Verify cell values
ws = wb.active
print("\n--- Verification ---")
print(f"D3 (Subject): {ws['D3'].value}")
print(f"D4 (Time): {ws['D4'].value}")
print(f"D5 (Chair): {ws['D5'].value}")
print(f"F4 (Location): {ws['F4'].value}")
print(f"F5 (Recorder): {ws['F5'].value}")
print(f"D6 (Attendees): {ws['D6'].value}")
print(f"C8 (Meeting Number): {ws['C8'].value}")
print(f"D8 (Conclusions): {ws['D8'].value}")
print(f"\nAction Items:")
for i in range(3):
row = 10 + i
print(f" Row {row}: C={ws[f'C{row}'].value}, D={ws[f'D{row}'].value}, F={ws[f'F{row}'].value}, G={ws[f'G{row}'].value}, H={ws[f'H{row}'].value}")
else:
print("\n=== Template not found, using default generation ===")
wb = create_default_workbook(meeting, conclusions, actions)
wb.save(output_path)
print(f"Saved to: {output_path}")
print(f"\n✅ Test completed! File saved to: {output_path}")
# List files in record directory
print(f"\n--- Files in record directory ---")
for f in os.listdir(RECORD_DIR):
print(f" {f}")
return True
if __name__ == "__main__":
test_excel_export()

View File

@@ -0,0 +1,186 @@
# Change: Update Excel Export with Template Cell Mapping
## Why
目前的 Excel 導出功能使用佔位符方式(`{{subject}}`),但實際模板 `meeting_template.xlsx` 使用固定儲存格座標。此外,數據模型缺少「會議編號」欄位,且需要支援代辦事項的「執行現況」欄位以符合模板需求。
## What Changes
### Backend 修改
- **BREAKING** 重構 `export.py`改用儲存格座標填充方式D3, D4 等)
- 修正模板路徑:`templates/``template/`
- 新增 `meeting_number` 欄位並實作自動生成邏輯
- 實作動態行填充:代辦事項從第 10 行往下延伸
### 數據模型修改
- `MeetingCreate/MeetingResponse`: 新增 `meeting_number` 欄位
### AI 整合調整
- 提供詳細的 Dify System Prompt供用戶在 Dify 後台設定)
## Impact
- Affected specs: `excel-export`, `meeting-management`, `ai-summarization`
- Affected code:
- `backend/app/routers/export.py`
- `backend/app/models/schemas.py`
- `backend/app/routers/meetings.py`
- `backend/app/routers/ai.py`
- `client/src/pages/meeting-detail.html`
## Template Cell Mapping
| 儲存格 | 內容 | 數據來源 |
|--------|------|----------|
| D3 | 會議主題 | `meeting.subject` |
| D4 | 會議時間 | `meeting.meeting_time` |
| D5 | 會議主席 | `meeting.chairperson` |
| F4 | 會議地點 | `meeting.location` |
| F5 | 記錄人員 | `meeting.recorder` |
| D6 | 會議參與人員 | `meeting.attendees`(逗號分隔) |
| C8 | 會議編號 | `meeting.meeting_number` 或生成規則 |
| D8 | 會議結論 | 結論內容(多條用換行分隔) |
| C10+ | 代辦事項編號 | `action.system_code` |
| D10+ | 事項內容 | `action.content` |
| F10+ | 負責人 | `action.owner` |
| G10+ | 預計完成日期 | `action.due_date` |
| H10+ | 執行現況 | `action.status`Open/In Progress/Done/Delayed |
## 現有編號生成機制確認
| 編號類型 | 格式 | 狀態 | 說明 |
|----------|------|------|------|
| 結論編號 | `C-YYYYMMDD-XX` | ✅ 已實作 | `generate_system_code("C", ...)` |
| 行動項編號 | `A-YYYYMMDD-XX` | ✅ 已實作 | `generate_system_code("A", ...)` |
| **會議編號** | `M-YYYYMMDD-XX` | ❌ 需新增 | 需在 DB 新增欄位並實作生成邏輯 |
## 已確認決策
1. **多條結論處理**:✅ 將多條結論用換行符合併放入 D8 單一儲存格
2. **執行現況欄位**:✅ 直接使用現有 `status` 欄位Open/In Progress/Done/Delayed
## Dify System Prompt
以下是完整的 System Prompt請在 Dify 後台設定:
```
# 角色定義
你是專業的會議記錄助手,負責分析會議逐字稿並提取結構化的會議紀錄。
# 任務說明
請仔細閱讀使用者提供的會議逐字稿,從中提取:
1. 會議結論:會議中達成的決定、共識或重要決議
2. 待辦事項:需要後續執行的具體行動項目
# 輸出格式要求
你必須以 JSON 格式回覆,結構如下:
{
"conclusions": [
"結論內容1",
"結論內容2"
],
"action_items": [
{
"content": "待辦事項的具體描述",
"owner": "負責人姓名",
"due_date": "YYYY-MM-DD"
}
]
}
# 欄位說明
## conclusions結論陣列
- 類型:字串陣列
- 數量:根據實際內容決定,可能是 0 個、1 個或多個
- 內容:每個結論應該是完整的一句話,清楚描述決議內容
- 若逐字稿中沒有明確的結論或決議,回傳空陣列 []
## action_items待辦事項陣列
- 類型:物件陣列
- 數量:根據實際內容決定,可能是 0 個、1 個或多個
- 各欄位說明:
- content必填待辦事項的具體描述應清楚說明要做什麼
- owner選填負責執行此事項的人員姓名。若逐字稿中有提及負責人則填入否則填空字串 ""
- due_date選填預計完成日期格式為 YYYY-MM-DD。若逐字稿中有提及日期則填入否則填 null
# 提取原則
1. **結論提取原則**
- 尋找「決定」、「同意」、「確認」、「通過」、「結論是」等關鍵詞
- 提取具有決策性質的陳述
- 不要將一般討論內容當作結論
- 每個結論應該是獨立且完整的
2. **待辦事項提取原則**
- 尋找「請...」、「需要...」、「負責...」、「跟進...」、「處理...」等動作詞
- 提取有明確執行要求的事項
- 若有提到負責人,填入 owner 欄位
- 若有提到期限如「下週」、「月底前」、「12月15日」轉換為 YYYY-MM-DD 格式
3. **數量彈性原則**
- 結論和待辦事項的數量完全取決於逐字稿內容
- 不要為了填充而虛構內容
- 不要合併應該分開的項目
- 不要拆分應該合併的項目
# 輸出範例
## 範例1有多個結論和待辦事項
{
"conclusions": [
"本季度預算調整為新台幣500萬元",
"新產品發布日期確定為2025年3月1日",
"同意增聘兩名工程師"
],
"action_items": [
{
"content": "準備預算調整報告提交財務部",
"owner": "王小明",
"due_date": "2025-12-20"
},
{
"content": "聯繫供應商確認交貨時程",
"owner": "李小華",
"due_date": "2025-12-15"
},
{
"content": "發布工程師職缺招募公告",
"owner": "",
"due_date": null
}
]
}
## 範例2只有一個結論沒有待辦事項
{
"conclusions": [
"維持現有方案不做調整"
],
"action_items": []
}
## 範例3沒有結論有待辦事項
{
"conclusions": [],
"action_items": [
{
"content": "收集更多市場數據供下次會議討論",
"owner": "張三",
"due_date": "2025-12-25"
}
]
}
## 範例4都沒有
{
"conclusions": [],
"action_items": []
}
# 重要提醒
1. 只輸出 JSON不要有其他文字說明
2. 確保 JSON 格式正確,可被程式解析
3. 使用繁體中文
4. 日期格式必須是 YYYY-MM-DD 或 null
5. owner 欄位若無資料請填空字串 "",不要填 null
```

View File

@@ -0,0 +1,31 @@
## MODIFIED Requirements
### Requirement: Structured Output Format
The AI summarization SHALL return structured data with conclusions and action items.
#### Scenario: Complete structured response
- **WHEN** transcript contains clear decisions and assignments
- **THEN** response SHALL include conclusions array (0 to many items) and action_items array with content, owner, due_date fields
#### Scenario: Partial data extraction
- **WHEN** transcript lacks explicit owner or due_date for action items
- **THEN** those fields SHALL be empty strings or null allowing manual completion
#### Scenario: Variable conclusion count
- **WHEN** transcript has multiple decisions
- **THEN** conclusions array SHALL contain all extracted conclusions without artificial limit
#### Scenario: No conclusions found
- **WHEN** transcript has no clear decisions
- **THEN** conclusions array SHALL be empty []
### Requirement: Dify Prompt Configuration
The Dify workflow SHALL be configured with detailed system prompt for meeting summarization.
#### Scenario: System prompt behavior
- **WHEN** transcript is sent to Dify
- **THEN** Dify SHALL use configured system prompt to extract conclusions and action_items in JSON format
#### Scenario: Flexible output count
- **WHEN** Dify processes transcript
- **THEN** it SHALL return variable number of conclusions and action items based on actual content

View File

@@ -0,0 +1,61 @@
## MODIFIED Requirements
### Requirement: Template-based Generation
The Excel export SHALL use openpyxl with template files and cell coordinate mapping.
#### Scenario: Cell coordinate filling
- **WHEN** Excel is generated from template `meeting_template.xlsx`
- **THEN** data SHALL be filled into specific cells:
- D3: meeting subject
- D4: meeting time
- D5: chairperson
- F4: location
- F5: recorder
- D6: attendees (comma separated)
- C8: meeting number (M-YYYYMMDD-XX format)
- D8: conclusions (newline separated if multiple)
#### Scenario: Dynamic row filling for action items
- **WHEN** meeting has action items
- **THEN** rows SHALL be filled starting from row 10:
- Column C: action item system code (A-YYYYMMDD-XX)
- Column D: action item content
- Column F: owner
- Column G: due date
- Column H: status (Open/In Progress/Done/Delayed)
#### Scenario: Template path resolution
- **WHEN** export is requested
- **THEN** server SHALL load template from `backend/template/meeting_template.xlsx`
### Requirement: Complete Data Inclusion
The exported Excel SHALL include all meeting metadata, conclusions, and action items.
#### Scenario: Full metadata export
- **WHEN** Excel is generated
- **THEN** it SHALL include subject, meeting_time, location, chairperson, recorder, attendees, and meeting_number
#### Scenario: Conclusions export
- **WHEN** Excel is generated with multiple conclusions
- **THEN** all conclusions SHALL be merged with newline separator into cell D8
#### Scenario: Action items export with status
- **WHEN** Excel is generated
- **THEN** all action items SHALL be listed with system_code, content, owner, due_date, and status
## ADDED Requirements
### Requirement: Meeting Number Generation
The system SHALL automatically generate meeting numbers for each meeting record.
#### Scenario: Auto-generate meeting number
- **WHEN** a new meeting is created
- **THEN** system SHALL generate meeting number in format `M-YYYYMMDD-XX` where XX is daily sequence
#### Scenario: Meeting number uniqueness
- **WHEN** multiple meetings are created on same date
- **THEN** each SHALL receive unique sequential number (M-20251211-01, M-20251211-02, etc.)
#### Scenario: Meeting number in export
- **WHEN** Excel is exported
- **THEN** meeting_number SHALL be displayed in cell C8

View File

@@ -0,0 +1,35 @@
## MODIFIED Requirements
### Requirement: Create Meeting
The system SHALL allow users to create meetings with required metadata and auto-generated meeting number.
#### Scenario: Create meeting with all fields
- **WHEN** user submits POST /api/meetings with subject, meeting_time, chairperson, location, recorder, attendees
- **THEN** a new meeting record SHALL be created with auto-generated UUID, meeting_number, and the meeting data SHALL be returned
#### Scenario: Create meeting with missing required fields
- **WHEN** user submits POST /api/meetings without subject or meeting_time
- **THEN** the server SHALL return HTTP 400 with validation error details
#### Scenario: Recorder defaults to current user
- **WHEN** user creates meeting without specifying recorder
- **THEN** the recorder field SHALL default to the logged-in user's email
#### Scenario: Auto-generate meeting number
- **WHEN** a new meeting is created
- **THEN** meeting_number SHALL be auto-generated in format M-YYYYMMDD-XX
### Requirement: System Code Generation
The system SHALL auto-generate unique system codes for meetings, conclusions, and action items.
#### Scenario: Generate meeting number
- **WHEN** a meeting is created on 2025-12-10
- **THEN** the meeting_number SHALL follow format M-20251210-XX where XX is sequence number
#### Scenario: Generate conclusion code
- **WHEN** a conclusion is created for a meeting on 2025-12-10
- **THEN** the system_code SHALL follow format C-20251210-XX where XX is sequence number
#### Scenario: Generate action item code
- **WHEN** an action item is created for a meeting on 2025-12-10
- **THEN** the system_code SHALL follow format A-20251210-XX where XX is sequence number

View File

@@ -0,0 +1,64 @@
# Tasks: Update Excel Template Export
## 1. Database Schema Updates
- [x] 1.1 新增 `meeting_number` 欄位至 `meeting_records`VARCHAR(20),格式 M-YYYYMMDD-XX
- Migration SQL: `backend/migrations/001_add_meeting_number.sql`
## 2. Backend Model Updates
- [x] 2.1 更新 `schemas.py``MeetingResponse` 新增 `meeting_number` 欄位
## 3. Backend API Updates
- [x] 3.1 更新 `meetings.py`:實作會議編號自動生成邏輯(使用現有 `generate_system_code("M", ...)`)
- [x] 3.2 更新 `meetings.py`:在 create_meeting 和 response 中包含 meeting_number
## 4. Excel Export Refactoring
- [x] 4.1 修正 `export.py`:模板路徑從 `templates/` 改為 `template/`
- [x] 4.2 重構 `export.py`改用儲存格座標填充方式D3, D4, D5, F4, F5, D6, C8, D8
- [x] 4.3 實作動態行填充:代辦事項從第 10 行開始往下延伸C, D, F, G, H 列)
- [x] 4.4 實作結論合併邏輯:多條結論用換行符合併至 D8
- [x] 4.5 H 列填入 action.status 作為執行現況
## 5. Testing
- [x] 5.1 手動測試 Excel 導出與模板填充
- [x] 5.2 驗證各欄位正確填入對應儲存格
- 測試腳本:`backend/tests/test_excel_export.py`
- 測試結果:✅ 全部通過
## 6. Documentation
- [x] 6.1 ✅ Dify System Prompt 已提供於 proposal.md
## 測試結果
```
Template exists: True
=== Testing with template ===
D3 (Subject): 專案進度討論會議
D4 (Time): 2025-12-11 14:30
D5 (Chair): 王經理
F4 (Location): 會議室A
F5 (Recorder): 李小明
D6 (Attendees): 張三, 李四, 王五
C8 (Meeting Number): M-20251211-01
D8 (Conclusions): 確認專案時程延後兩週
同意增加測試人力
下次會議改為線上進行
Action Items:
Row 10: C=A-20251211-01, D=更新專案時程表, F=張三, G=2025-12-15, H=Open
Row 11: C=A-20251211-02, D=聯繫人資部門增聘測試人員, F=李四, G=2025-12-20, H=In Progress
Row 12: C=A-20251211-03, D=準備線上會議設備, F=王五, G=, H=Open
✅ Test completed successfully!
```
## 部署前須執行
請在資料庫執行以下 SQL 來新增 `meeting_number` 欄位:
```sql
ALTER TABLE meeting_records
ADD COLUMN meeting_number VARCHAR(20) NULL AFTER uuid;
CREATE INDEX idx_meeting_number ON meeting_records(meeting_number);
```

View File

@@ -23,18 +23,30 @@ The AI summarization SHALL return structured data with conclusions and action it
#### Scenario: Complete structured response #### Scenario: Complete structured response
- **WHEN** transcript contains clear decisions and assignments - **WHEN** transcript contains clear decisions and assignments
- **THEN** response SHALL include conclusions array and action_items array with content, owner, due_date fields - **THEN** response SHALL include conclusions array (0 to many items) and action_items array with content, owner, due_date fields
#### Scenario: Partial data extraction #### Scenario: Partial data extraction
- **WHEN** transcript lacks explicit owner or due_date for action items - **WHEN** transcript lacks explicit owner or due_date for action items
- **THEN** those fields SHALL be empty strings allowing manual completion - **THEN** those fields SHALL be empty strings or null allowing manual completion
#### Scenario: Variable conclusion count
- **WHEN** transcript has multiple decisions
- **THEN** conclusions array SHALL contain all extracted conclusions without artificial limit
#### Scenario: No conclusions found
- **WHEN** transcript has no clear decisions
- **THEN** conclusions array SHALL be empty []
### Requirement: Dify Prompt Configuration ### Requirement: Dify Prompt Configuration
The Dify workflow SHALL be configured with appropriate system prompt for meeting summarization. The Dify workflow SHALL be configured with detailed system prompt for meeting summarization.
#### Scenario: System prompt behavior #### Scenario: System prompt behavior
- **WHEN** transcript is sent to Dify - **WHEN** transcript is sent to Dify
- **THEN** Dify SHALL use configured prompt to extract conclusions and action_items in JSON format - **THEN** Dify SHALL use configured system prompt to extract conclusions and action_items in JSON format
#### Scenario: Flexible output count
- **WHEN** Dify processes transcript
- **THEN** it SHALL return variable number of conclusions and action items based on actual content
### Requirement: Manual Data Completion ### Requirement: Manual Data Completion
The Electron client SHALL allow users to manually complete missing AI-extracted data. The Electron client SHALL allow users to manually complete missing AI-extracted data.

View File

@@ -15,30 +15,47 @@ The middleware server SHALL generate Excel reports from meeting data using templ
- **THEN** server SHALL return HTTP 404 - **THEN** server SHALL return HTTP 404
### Requirement: Template-based Generation ### Requirement: Template-based Generation
The Excel export SHALL use openpyxl with template files. The Excel export SHALL use openpyxl with template files and cell coordinate mapping.
#### Scenario: Placeholder replacement #### Scenario: Cell coordinate filling
- **WHEN** Excel is generated - **WHEN** Excel is generated from template `meeting_template.xlsx`
- **THEN** placeholders ({{subject}}, {{time}}, {{chair}}, etc.) SHALL be replaced with actual meeting data - **THEN** data SHALL be filled into specific cells:
- D3: meeting subject
- D4: meeting time
- D5: chairperson
- F4: location
- F5: recorder
- D6: attendees (comma separated)
- C8: meeting number (M-YYYYMMDD-XX format)
- D8: conclusions (newline separated if multiple)
#### Scenario: Dynamic row insertion #### Scenario: Dynamic row filling for action items
- **WHEN** meeting has multiple conclusions or action items - **WHEN** meeting has action items
- **THEN** rows SHALL be dynamically inserted to accommodate all items - **THEN** rows SHALL be filled starting from row 10:
- Column C: action item system code (A-YYYYMMDD-XX)
- Column D: action item content
- Column F: owner
- Column G: due date
- Column H: status (Open/In Progress/Done/Delayed)
#### Scenario: Template path resolution
- **WHEN** export is requested
- **THEN** server SHALL load template from `backend/template/meeting_template.xlsx`
### Requirement: Complete Data Inclusion ### Requirement: Complete Data Inclusion
The exported Excel SHALL include all meeting metadata and AI-generated content. The exported Excel SHALL include all meeting metadata, conclusions, and action items.
#### Scenario: Full metadata export #### Scenario: Full metadata export
- **WHEN** Excel is generated - **WHEN** Excel is generated
- **THEN** it SHALL include subject, meeting_time, location, chairperson, recorder, and attendees - **THEN** it SHALL include subject, meeting_time, location, chairperson, recorder, attendees, and meeting_number
#### Scenario: Conclusions export #### Scenario: Conclusions export
- **WHEN** Excel is generated - **WHEN** Excel is generated with multiple conclusions
- **THEN** all conclusions SHALL be listed with their system codes - **THEN** all conclusions SHALL be merged with newline separator into cell D8
#### Scenario: Action items export #### Scenario: Action items export with status
- **WHEN** Excel is generated - **WHEN** Excel is generated
- **THEN** all action items SHALL be listed with content, owner, due_date, status, and system code - **THEN** all action items SHALL be listed with system_code, content, owner, due_date, and status
### Requirement: Template Management ### Requirement: Template Management
Admin users SHALL be able to manage Excel templates. Admin users SHALL be able to manage Excel templates.
@@ -47,3 +64,18 @@ Admin users SHALL be able to manage Excel templates.
- **WHEN** admin user accesses template management - **WHEN** admin user accesses template management
- **THEN** they SHALL be able to upload, view, and update Excel templates - **THEN** they SHALL be able to upload, view, and update Excel templates
### Requirement: Meeting Number Generation
The system SHALL automatically generate meeting numbers for each meeting record.
#### Scenario: Auto-generate meeting number
- **WHEN** a new meeting is created
- **THEN** system SHALL generate meeting number in format `M-YYYYMMDD-XX` where XX is daily sequence
#### Scenario: Meeting number uniqueness
- **WHEN** multiple meetings are created on same date
- **THEN** each SHALL receive unique sequential number (M-20251211-01, M-20251211-02, etc.)
#### Scenario: Meeting number in export
- **WHEN** Excel is exported
- **THEN** meeting_number SHALL be displayed in cell C8

View File

@@ -4,11 +4,11 @@
TBD - created by archiving change add-meeting-assistant-mvp. Update Purpose after archive. TBD - created by archiving change add-meeting-assistant-mvp. Update Purpose after archive.
## Requirements ## Requirements
### Requirement: Create Meeting ### Requirement: Create Meeting
The system SHALL allow users to create meetings with required metadata. The system SHALL allow users to create meetings with required metadata and auto-generated meeting number.
#### Scenario: Create meeting with all fields #### Scenario: Create meeting with all fields
- **WHEN** user submits POST /api/meetings with subject, meeting_time, chairperson, location, recorder, attendees - **WHEN** user submits POST /api/meetings with subject, meeting_time, chairperson, location, recorder, attendees
- **THEN** a new meeting record SHALL be created with auto-generated UUID and the meeting data SHALL be returned - **THEN** a new meeting record SHALL be created with auto-generated UUID, meeting_number, and the meeting data SHALL be returned
#### Scenario: Create meeting with missing required fields #### Scenario: Create meeting with missing required fields
- **WHEN** user submits POST /api/meetings without subject or meeting_time - **WHEN** user submits POST /api/meetings without subject or meeting_time
@@ -18,6 +18,10 @@ The system SHALL allow users to create meetings with required metadata.
- **WHEN** user creates meeting without specifying recorder - **WHEN** user creates meeting without specifying recorder
- **THEN** the recorder field SHALL default to the logged-in user's email - **THEN** the recorder field SHALL default to the logged-in user's email
#### Scenario: Auto-generate meeting number
- **WHEN** a new meeting is created
- **THEN** meeting_number SHALL be auto-generated in format M-YYYYMMDD-XX
### Requirement: List Meetings ### Requirement: List Meetings
The system SHALL allow users to retrieve a list of meetings. The system SHALL allow users to retrieve a list of meetings.
@@ -63,7 +67,11 @@ The system SHALL allow authorized users to delete meetings.
- **THEN** the meeting and all related data SHALL be deleted - **THEN** the meeting and all related data SHALL be deleted
### Requirement: System Code Generation ### Requirement: System Code Generation
The system SHALL auto-generate unique system codes for conclusions and action items. The system SHALL auto-generate unique system codes for meetings, conclusions, and action items.
#### Scenario: Generate meeting number
- **WHEN** a meeting is created on 2025-12-10
- **THEN** the meeting_number SHALL follow format M-20251210-XX where XX is sequence number
#### Scenario: Generate conclusion code #### Scenario: Generate conclusion code
- **WHEN** a conclusion is created for a meeting on 2025-12-10 - **WHEN** a conclusion is created for a meeting on 2025-12-10

386
start.sh Executable file
View File

@@ -0,0 +1,386 @@
#!/bin/bash
#
# Meeting Assistant - Startup Script
# 啟動前端、後端及 Sidecar 服務
#
set -e
# 顏色定義
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 專案路徑
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BACKEND_DIR="$PROJECT_DIR/backend"
CLIENT_DIR="$PROJECT_DIR/client"
SIDECAR_DIR="$PROJECT_DIR/sidecar"
# Port 設定
BACKEND_PORT=8000
# PID 檔案
PID_FILE="$PROJECT_DIR/.running_pids"
# 函數:印出訊息
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[OK]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# 函數:檢查 port 是否被佔用
check_port() {
local port=$1
if lsof -i :$port > /dev/null 2>&1; then
return 0 # port 被佔用
else
return 1 # port 可用
fi
}
# 函數:釋放 port
release_port() {
local port=$1
if check_port $port; then
log_warn "Port $port 被佔用,嘗試釋放..."
local pid=$(lsof -t -i :$port 2>/dev/null)
if [ -n "$pid" ]; then
kill -9 $pid 2>/dev/null || true
sleep 1
log_success "Port $port 已釋放"
fi
fi
}
# 函數:檢查環境
check_environment() {
echo ""
echo "=========================================="
echo " Meeting Assistant 環境檢查"
echo "=========================================="
echo ""
local all_ok=true
# 檢查 Python
log_info "檢查 Python..."
if command -v python3 &> /dev/null; then
local py_version=$(python3 --version 2>&1)
log_success "Python: $py_version"
else
log_error "Python3 未安裝"
all_ok=false
fi
# 檢查 Node.js
log_info "檢查 Node.js..."
if command -v node &> /dev/null; then
local node_version=$(node --version)
log_success "Node.js: $node_version"
else
log_error "Node.js 未安裝"
all_ok=false
fi
# 檢查 npm
log_info "檢查 npm..."
if command -v npm &> /dev/null; then
local npm_version=$(npm --version)
log_success "npm: $npm_version"
else
log_error "npm 未安裝"
all_ok=false
fi
# 檢查後端虛擬環境
log_info "檢查後端虛擬環境..."
if [ -d "$BACKEND_DIR/venv" ]; then
log_success "後端虛擬環境: 存在"
else
log_error "後端虛擬環境不存在,請執行: cd backend && python3 -m venv venv"
all_ok=false
fi
# 檢查 Sidecar 虛擬環境
log_info "檢查 Sidecar 虛擬環境..."
if [ -d "$SIDECAR_DIR/venv" ]; then
log_success "Sidecar 虛擬環境: 存在"
else
log_warn "Sidecar 虛擬環境不存在(語音轉寫功能將無法使用)"
fi
# 檢查前端依賴
log_info "檢查前端依賴..."
if [ -d "$CLIENT_DIR/node_modules" ]; then
log_success "前端依賴: 已安裝"
else
log_error "前端依賴未安裝,請執行: cd client && npm install"
all_ok=false
fi
# 檢查後端 .env
log_info "檢查後端環境變數..."
if [ -f "$BACKEND_DIR/.env" ]; then
log_success "後端 .env: 存在"
else
log_warn "後端 .env 不存在,請複製 .env.example 並設定"
fi
# 檢查 Port 狀態
log_info "檢查 Port $BACKEND_PORT..."
if check_port $BACKEND_PORT; then
log_warn "Port $BACKEND_PORT 已被佔用"
else
log_success "Port $BACKEND_PORT: 可用"
fi
echo ""
if [ "$all_ok" = true ]; then
log_success "環境檢查通過!"
return 0
else
log_error "環境檢查失敗,請修正上述問題"
return 1
fi
}
# 函數:啟動後端
start_backend() {
log_info "啟動後端服務..."
# 釋放 port
release_port $BACKEND_PORT
cd "$BACKEND_DIR"
source venv/bin/activate
# 背景啟動 uvicorn
nohup uvicorn app.main:app --host 0.0.0.0 --port $BACKEND_PORT --reload > "$PROJECT_DIR/backend.log" 2>&1 &
local backend_pid=$!
echo "BACKEND_PID=$backend_pid" >> "$PID_FILE"
# 等待啟動
sleep 2
if check_port $BACKEND_PORT; then
log_success "後端服務已啟動 (PID: $backend_pid, Port: $BACKEND_PORT)"
else
log_error "後端服務啟動失敗,請檢查 backend.log"
return 1
fi
}
# 函數:啟動前端
start_frontend() {
log_info "啟動前端應用..."
cd "$CLIENT_DIR"
# 背景啟動 Electron它會自動管理 Sidecar
nohup npm start > "$PROJECT_DIR/frontend.log" 2>&1 &
local frontend_pid=$!
echo "FRONTEND_PID=$frontend_pid" >> "$PID_FILE"
sleep 2
log_success "前端應用已啟動 (PID: $frontend_pid)"
log_info "Sidecar 語音識別引擎將由 Electron 自動管理"
}
# 函數:停止所有服務
stop_all() {
echo ""
log_info "停止所有服務..."
# 讀取 PID 檔案並終止程序
if [ -f "$PID_FILE" ]; then
while IFS='=' read -r key pid; do
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
log_info "停止 $key (PID: $pid)..."
kill -TERM "$pid" 2>/dev/null || true
fi
done < "$PID_FILE"
rm -f "$PID_FILE"
fi
# 釋放 port
release_port $BACKEND_PORT
# 清理可能殘留的程序
pkill -f "uvicorn app.main:app" 2>/dev/null || true
pkill -f "electron ." 2>/dev/null || true
pkill -f "transcriber.py" 2>/dev/null || true
sleep 1
log_success "所有服務已停止"
}
# 函數:顯示狀態
show_status() {
echo ""
echo "=========================================="
echo " Meeting Assistant 服務狀態"
echo "=========================================="
echo ""
# 檢查後端
if check_port $BACKEND_PORT; then
local backend_pid=$(lsof -t -i :$BACKEND_PORT 2>/dev/null | head -1)
log_success "後端服務: 運行中 (PID: $backend_pid, Port: $BACKEND_PORT)"
else
log_warn "後端服務: 未運行"
fi
# 檢查 Electron
local electron_pid=$(pgrep -f "electron ." 2>/dev/null | head -1)
if [ -n "$electron_pid" ]; then
log_success "前端應用: 運行中 (PID: $electron_pid)"
else
log_warn "前端應用: 未運行"
fi
# 檢查 Sidecar
local sidecar_pid=$(pgrep -f "transcriber.py" 2>/dev/null | head -1)
if [ -n "$sidecar_pid" ]; then
log_success "Sidecar: 運行中 (PID: $sidecar_pid)"
else
log_warn "Sidecar: 未運行(將在前端需要時自動啟動)"
fi
echo ""
}
# 函數:顯示幫助
show_help() {
echo ""
echo "Meeting Assistant 啟動腳本"
echo ""
echo "用法: $0 [命令]"
echo ""
echo "命令:"
echo " start 啟動所有服務(後端 + 前端)"
echo " stop 停止所有服務並釋放 port"
echo " restart 重啟所有服務"
echo " status 顯示服務狀態"
echo " check 檢查環境設定"
echo " backend 僅啟動後端服務"
echo " frontend 僅啟動前端應用"
echo " logs 顯示日誌"
echo " help 顯示此幫助訊息"
echo ""
echo "範例:"
echo " $0 start # 啟動所有服務"
echo " $0 stop # 停止所有服務"
echo " $0 status # 查看狀態"
echo ""
}
# 函數:顯示日誌
show_logs() {
echo ""
echo "=========================================="
echo " Meeting Assistant 日誌"
echo "=========================================="
if [ -f "$PROJECT_DIR/backend.log" ]; then
echo ""
echo "--- 後端日誌 (最後 20 行) ---"
tail -20 "$PROJECT_DIR/backend.log"
fi
if [ -f "$PROJECT_DIR/frontend.log" ]; then
echo ""
echo "--- 前端日誌 (最後 20 行) ---"
tail -20 "$PROJECT_DIR/frontend.log"
fi
}
# 主程式
main() {
local command=${1:-help}
case $command in
start)
echo ""
echo "=========================================="
echo " Meeting Assistant 啟動中..."
echo "=========================================="
# 清理舊的 PID 檔案
rm -f "$PID_FILE"
if ! check_environment; then
exit 1
fi
echo ""
start_backend
start_frontend
echo ""
echo "=========================================="
log_success "Meeting Assistant 已啟動!"
echo "=========================================="
echo ""
echo " 後端 API: http://localhost:$BACKEND_PORT"
echo " API 文件: http://localhost:$BACKEND_PORT/docs"
echo ""
echo " 停止服務: $0 stop"
echo " 查看狀態: $0 status"
echo " 查看日誌: $0 logs"
echo ""
;;
stop)
stop_all
;;
restart)
stop_all
sleep 2
$0 start
;;
status)
show_status
;;
check)
check_environment
;;
backend)
release_port $BACKEND_PORT
rm -f "$PID_FILE"
start_backend
;;
frontend)
start_frontend
;;
logs)
show_logs
;;
help|--help|-h)
show_help
;;
*)
log_error "未知命令: $command"
show_help
exit 1
;;
esac
}
# 捕捉中斷信號
trap 'echo ""; log_info "收到中斷信號,停止服務..."; stop_all; exit 0' INT TERM
# 執行主程式
main "$@"