Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """ | |
| B-11 migration: 清测试数据 + 导入 0603.md 到 thread='main' | |
| 跑法(dry-run、不真动 DB): | |
| DATABASE_URL=postgresql://... python3 scripts/migrate_to_main.py | |
| 跑法(真执行): | |
| DATABASE_URL=postgresql://... python3 scripts/migrate_to_main.py --commit | |
| 数据清理范围([[zhiyu-migration-project]] B-11 决议): | |
| - conversations 表 thread_id='test' 全删 | |
| - memories 表 created_at >= '2026-05-29' 全删(保留 chat 端真历史) | |
| - mido_messages / dreams / messages_board / diary_entries / images 全删(全是测试期产生) | |
| 0603.md 导入: | |
| - thread_id='main' | |
| - created_at 从 2026-06-03 06:00 起、每 turn +1 分钟(合并算作"一天") | |
| - session_id='b11-migration-0603' 统一 | |
| - 思维链入 metadata.thinking JSON 字段 | |
| - 用 `|` 分隔思维链和正文 | |
| """ | |
| import asyncio | |
| import asyncpg | |
| import os | |
| import sys | |
| import re | |
| import json | |
| from datetime import datetime, timezone, timedelta | |
| from pathlib import Path | |
| DATABASE_URL = os.environ.get('DATABASE_URL', '') | |
| ZERO3_PATH = Path(__file__).parent.parent.parent / 'memory' / 'chat_history' / '0603.md' | |
| DRY_RUN = '--commit' not in sys.argv | |
| BJ_TZ = timezone(timedelta(hours=8)) | |
| BASE_TIME = datetime(2026, 6, 3, 6, 0, 0, tzinfo=BJ_TZ) | |
| TURN_INTERVAL_MIN = 1 | |
| SESSION_ID = 'b11-migration-0603' | |
| def parse_0603(text: str) -> list: | |
| """解析 0603.md、返回 [{role, content, thinking}, ...] | |
| 格式: | |
| 2026.06.03 | |
| **User:** | |
| ... | |
| --- | |
| **Claude:** | |
| 思维链多段... | |
| 最后一行思维链 | | |
| 正文多段... | |
| --- | |
| """ | |
| text = text.lstrip() | |
| if text.startswith('2026.06.03'): | |
| text = text[len('2026.06.03'):].lstrip() | |
| raw_turns = re.split(r'\n---\n', text) | |
| turns = [] | |
| for raw in raw_turns: | |
| raw = raw.strip() | |
| if not raw: | |
| continue | |
| if raw.startswith('**User:**'): | |
| content = raw[len('**User:**'):].strip() | |
| turns.append({'role': 'user', 'content': content, 'thinking': None}) | |
| elif raw.startswith('**Claude:**'): | |
| content = raw[len('**Claude:**'):].strip() | |
| if '|' in content: | |
| idx = content.rfind('|') | |
| thinking = content[:idx].rstrip() | |
| body = content[idx + 1:].lstrip() | |
| else: | |
| thinking = None | |
| body = content | |
| turns.append({'role': 'assistant', 'content': body, 'thinking': thinking}) | |
| return turns | |
| async def clean_test_data(conn, dry_run=True): | |
| operations = [ | |
| ("conversations.thread_id='test'", | |
| "SELECT COUNT(*) FROM conversations WHERE thread_id = 'test'", | |
| "DELETE FROM conversations WHERE thread_id = 'test'"), | |
| ("memories created_at >= '2026-05-29'", | |
| "SELECT COUNT(*) FROM memories WHERE created_at >= '2026-05-29'", | |
| "DELETE FROM memories WHERE created_at >= '2026-05-29'"), | |
| ("mido_messages 全部", | |
| "SELECT COUNT(*) FROM mido_messages", | |
| "DELETE FROM mido_messages"), | |
| ("dreams 全部", | |
| "SELECT COUNT(*) FROM dreams", | |
| "DELETE FROM dreams"), | |
| ("messages_board 全部", | |
| "SELECT COUNT(*) FROM messages_board", | |
| "DELETE FROM messages_board"), | |
| ("diary_entries 全部", | |
| "SELECT COUNT(*) FROM diary_entries", | |
| "DELETE FROM diary_entries"), | |
| ("images 全部", | |
| "SELECT COUNT(*) FROM images", | |
| "DELETE FROM images"), | |
| ] | |
| for label, count_sql, delete_sql in operations: | |
| try: | |
| count = await conn.fetchval(count_sql) | |
| except Exception as e: | |
| print(f" [SKIP] {label}: {e!r}") | |
| continue | |
| if dry_run: | |
| print(f" [DRY] {label}: 会删 {count} 条") | |
| else: | |
| res = await conn.execute(delete_sql) | |
| print(f" [DEL] {label}: {res}(之前 {count} 条)") | |
| async def import_0603(conn, turns, dry_run=True): | |
| n_imported = 0 | |
| for i, turn in enumerate(turns): | |
| created_at = BASE_TIME + timedelta(minutes=i * TURN_INTERVAL_MIN) | |
| metadata = json.dumps({'thinking': turn['thinking']}, ensure_ascii=False) if turn['thinking'] else None | |
| if dry_run: | |
| preview = turn['content'][:80].replace('\n', ' ') | |
| if len(turn['content']) > 80: | |
| preview += '...' | |
| t_mark = '✦' if turn['thinking'] else ' ' | |
| print(f" [DRY] {created_at.strftime('%H:%M')} {t_mark} {turn['role']:>9}: {preview}") | |
| else: | |
| await conn.execute(""" | |
| INSERT INTO conversations (session_id, role, content, model, metadata, thread_id, created_at) | |
| VALUES ($1, $2, $3, $4, $5, $6, $7) | |
| """, SESSION_ID, turn['role'], turn['content'], 'claude.ai-export', metadata, 'main', created_at) | |
| n_imported += 1 | |
| return n_imported | |
| async def main(): | |
| if not DATABASE_URL: | |
| print("⚠️ DATABASE_URL 没设、跑不了。export DATABASE_URL=postgresql://... 再来", file=sys.stderr) | |
| sys.exit(1) | |
| if not ZERO3_PATH.exists(): | |
| print(f"⚠️ 找不到 {ZERO3_PATH}", file=sys.stderr) | |
| sys.exit(1) | |
| text = ZERO3_PATH.read_text(encoding='utf-8') | |
| turns = parse_0603(text) | |
| n_user = sum(1 for t in turns if t['role'] == 'user') | |
| n_asst = sum(1 for t in turns if t['role'] == 'assistant') | |
| n_with_thinking = sum(1 for t in turns if t.get('thinking')) | |
| print(f"=== 解析 0603.md ===") | |
| print(f" 总 turn 数:{len(turns)}(user {n_user} / assistant {n_asst})") | |
| print(f" 带思维链:{n_with_thinking} 条") | |
| if turns: | |
| print(f" 第一条:{turns[0]['role']} - {turns[0]['content'][:60]!r}") | |
| print(f" 最后一条:{turns[-1]['role']} - {turns[-1]['content'][:60]!r}") | |
| print(f" 时间戳:{BASE_TIME.strftime('%Y-%m-%d %H:%M')} 起、每 turn +{TURN_INTERVAL_MIN} 分钟") | |
| print(f" 最后一条会落在 {(BASE_TIME + timedelta(minutes=(len(turns)-1) * TURN_INTERVAL_MIN)).strftime('%H:%M')}") | |
| print() | |
| if DRY_RUN: | |
| print("=== DRY RUN(不会真删 / 真灌)===\n") | |
| else: | |
| print("=== COMMIT MODE(实际执行 + 不可逆)===\n") | |
| confirm = input("确认要真执行吗?输入 'YES' 继续: ") | |
| if confirm != 'YES': | |
| print("取消") | |
| return | |
| pool = await asyncpg.create_pool(DATABASE_URL, min_size=1, max_size=2, statement_cache_size=0) | |
| try: | |
| async with pool.acquire() as conn: | |
| print("[1/2] 清测试数据") | |
| await clean_test_data(conn, dry_run=DRY_RUN) | |
| print(f"\n[2/2] 导入 0603.md 到 thread='main'") | |
| n = await import_0603(conn, turns, dry_run=DRY_RUN) | |
| if not DRY_RUN: | |
| print(f" 导入 {n} 条") | |
| finally: | |
| await pool.close() | |
| print(f"\n{'(dry-run、没动 DB)' if DRY_RUN else 'Done、DB 已变更'}") | |
| if __name__ == '__main__': | |
| asyncio.run(main()) | |