zhaozhao_memory / scripts /migrate_to_main.py
junyue1002
B-11 准备:填 system_prompt.txt(CLAUDE.md + 工具段)+ 日期白名单注入支持 + 数据迁移脚本
e755b65
Raw
History Blame Contribute Delete
7.03 kB
#!/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())