295 lines
8.3 KiB
Bash
295 lines
8.3 KiB
Bash
|
|
#!/bin/bash
|
|||
|
|
#
|
|||
|
|
# artifact_postgres_export.sh - Export code artifacts from Open WebUI PostgreSQL database
|
|||
|
|
#
|
|||
|
|
# Usage: artifact_postgres_export.sh [OPTIONS] <chat_id> <output_dir>
|
|||
|
|
#
|
|||
|
|
# Arguments:
|
|||
|
|
# chat_id Chat ID from Open WebUI URL (e.g., e135d74e-5b43-4b24-a651-e999f103942b)
|
|||
|
|
# output_dir Directory to save extracted code files
|
|||
|
|
#
|
|||
|
|
# Options:
|
|||
|
|
# -h, --help Show this help message
|
|||
|
|
# -H, --host HOST PostgreSQL host (default: ai_postgres via Docker)
|
|||
|
|
# -u, --user USER PostgreSQL user (default: ai)
|
|||
|
|
# -d, --db DATABASE PostgreSQL database (default: openwebui)
|
|||
|
|
# -v, --verbose Verbose output
|
|||
|
|
# --remote HOST SSH remote host for Docker access (default: vps)
|
|||
|
|
# -f, --force Force export even if output directory is not empty
|
|||
|
|
#
|
|||
|
|
# Examples:
|
|||
|
|
# artifact_postgres_export.sh e135d74e-5b43-4b24-a651-e999f103942b ~/Projects/rust/piglet
|
|||
|
|
# artifact_postgres_export.sh --remote vps abc123def456 ./output
|
|||
|
|
#
|
|||
|
|
|
|||
|
|
set -euo pipefail
|
|||
|
|
|
|||
|
|
# Colors
|
|||
|
|
RED='\033[0;31m'
|
|||
|
|
GREEN='\033[0;32m'
|
|||
|
|
YELLOW='\033[1;33m'
|
|||
|
|
BLUE='\033[0;34m'
|
|||
|
|
MAGENTA='\033[0;35m'
|
|||
|
|
CYAN='\033[0;36m'
|
|||
|
|
RESET='\033[0m'
|
|||
|
|
|
|||
|
|
# Default values
|
|||
|
|
PG_CONTAINER="ai_postgres"
|
|||
|
|
PG_USER="ai"
|
|||
|
|
PG_DATABASE="openwebui"
|
|||
|
|
REMOTE_HOST="vps"
|
|||
|
|
VERBOSE=false
|
|||
|
|
FORCE=false
|
|||
|
|
|
|||
|
|
# Functions
|
|||
|
|
print_error() {
|
|||
|
|
echo -e "${RED}✗ Error:${RESET} $1" >&2
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print_success() {
|
|||
|
|
echo -e "${GREEN}✓${RESET} $1"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print_info() {
|
|||
|
|
echo -e "${BLUE}ℹ${RESET} $1"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print_warning() {
|
|||
|
|
echo -e "${YELLOW}⚠${RESET} $1"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print_verbose() {
|
|||
|
|
if [[ "$VERBOSE" == "true" ]]; then
|
|||
|
|
echo -e "${CYAN}→${RESET} $1"
|
|||
|
|
fi
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
show_help() {
|
|||
|
|
sed -n '2,/^$/p' "$0" | sed 's/^# \?//' | head -n -1
|
|||
|
|
exit 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Parse options
|
|||
|
|
POSITIONAL_ARGS=()
|
|||
|
|
while [[ $# -gt 0 ]]; do
|
|||
|
|
case $1 in
|
|||
|
|
-h|--help)
|
|||
|
|
show_help
|
|||
|
|
;;
|
|||
|
|
-H|--host)
|
|||
|
|
PG_CONTAINER="$2"
|
|||
|
|
shift 2
|
|||
|
|
;;
|
|||
|
|
-u|--user)
|
|||
|
|
PG_USER="$2"
|
|||
|
|
shift 2
|
|||
|
|
;;
|
|||
|
|
-d|--db)
|
|||
|
|
PG_DATABASE="$2"
|
|||
|
|
shift 2
|
|||
|
|
;;
|
|||
|
|
-v|--verbose)
|
|||
|
|
VERBOSE=true
|
|||
|
|
shift
|
|||
|
|
;;
|
|||
|
|
-f|--force)
|
|||
|
|
FORCE=true
|
|||
|
|
shift
|
|||
|
|
;;
|
|||
|
|
--remote)
|
|||
|
|
REMOTE_HOST="$2"
|
|||
|
|
shift 2
|
|||
|
|
;;
|
|||
|
|
-*)
|
|||
|
|
print_error "Unknown option: $1"
|
|||
|
|
echo "Use --help for usage information"
|
|||
|
|
exit 1
|
|||
|
|
;;
|
|||
|
|
*)
|
|||
|
|
POSITIONAL_ARGS+=("$1")
|
|||
|
|
shift
|
|||
|
|
;;
|
|||
|
|
esac
|
|||
|
|
done
|
|||
|
|
|
|||
|
|
# Restore positional parameters
|
|||
|
|
set -- "${POSITIONAL_ARGS[@]}"
|
|||
|
|
|
|||
|
|
# Validate arguments
|
|||
|
|
if [[ $# -lt 1 ]]; then
|
|||
|
|
print_error "Missing required argument: chat_id"
|
|||
|
|
echo "Usage: $(basename "$0") [OPTIONS] <chat_id> [output_dir]"
|
|||
|
|
echo "Use --help for more information"
|
|||
|
|
exit 1
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
CHAT_ID="$1"
|
|||
|
|
OUTPUT_DIR="${2:-.}" # Default to current directory if not specified
|
|||
|
|
|
|||
|
|
# Validate chat ID format (UUID)
|
|||
|
|
if [[ ! "$CHAT_ID" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then
|
|||
|
|
print_error "Invalid chat ID format. Expected UUID format."
|
|||
|
|
exit 1
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# Create output directory
|
|||
|
|
mkdir -p "$OUTPUT_DIR"
|
|||
|
|
|
|||
|
|
# Check if output directory is empty (unless --force)
|
|||
|
|
if [[ "$FORCE" == "false" ]]; then
|
|||
|
|
if [[ -n "$(ls -A "$OUTPUT_DIR" 2>/dev/null)" ]]; then
|
|||
|
|
print_error "Output directory is not empty: $OUTPUT_DIR"
|
|||
|
|
echo "Use --force to export anyway, or choose an empty directory"
|
|||
|
|
exit 1
|
|||
|
|
fi
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
print_verbose "Output directory: $OUTPUT_DIR"
|
|||
|
|
|
|||
|
|
# Function to run PostgreSQL queries
|
|||
|
|
run_psql() {
|
|||
|
|
local query="$1"
|
|||
|
|
if [[ -n "$REMOTE_HOST" ]]; then
|
|||
|
|
ssh -A "$REMOTE_HOST" "docker exec $PG_CONTAINER psql -U $PG_USER -d $PG_DATABASE -t -c \"$query\""
|
|||
|
|
else
|
|||
|
|
docker exec "$PG_CONTAINER" psql -U "$PG_USER" -d "$PG_DATABASE" -t -c "$query"
|
|||
|
|
fi
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print_info "Fetching chat from database..."
|
|||
|
|
|
|||
|
|
# Check if chat exists
|
|||
|
|
CHAT_EXISTS=$(run_psql "SELECT COUNT(*) FROM chat WHERE id = '$CHAT_ID';" | tr -d '[:space:]')
|
|||
|
|
if [[ "$CHAT_EXISTS" != "1" ]]; then
|
|||
|
|
print_error "Chat ID not found in database"
|
|||
|
|
exit 1
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# Get chat title
|
|||
|
|
CHAT_TITLE=$(run_psql "SELECT title FROM chat WHERE id = '$CHAT_ID';" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|||
|
|
print_success "Found chat: ${MAGENTA}${CHAT_TITLE}${RESET}"
|
|||
|
|
|
|||
|
|
# Export chat data to temporary file
|
|||
|
|
TEMP_FILE=$(mktemp)
|
|||
|
|
trap 'rm -f "$TEMP_FILE"' EXIT
|
|||
|
|
|
|||
|
|
print_verbose "Exporting chat data to temporary file..."
|
|||
|
|
run_psql "SELECT chat FROM chat WHERE id = '$CHAT_ID';" > "$TEMP_FILE"
|
|||
|
|
|
|||
|
|
# Parse JSON and extract code blocks using Python
|
|||
|
|
print_info "Extracting code blocks..."
|
|||
|
|
|
|||
|
|
python3 - "$TEMP_FILE" "$OUTPUT_DIR" "$CHAT_ID" "$CHAT_TITLE" "$GREEN" "$RED" "$YELLOW" "$BLUE" "$MAGENTA" "$CYAN" "$RESET" <<'PYTHON_SCRIPT'
|
|||
|
|
import json
|
|||
|
|
import re
|
|||
|
|
import sys
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
# Get arguments
|
|||
|
|
temp_file = sys.argv[1]
|
|||
|
|
output_dir = Path(sys.argv[2])
|
|||
|
|
chat_id = sys.argv[3]
|
|||
|
|
chat_title = sys.argv[4]
|
|||
|
|
GREEN, RED, YELLOW, BLUE, MAGENTA, CYAN, RESET = sys.argv[5:12]
|
|||
|
|
|
|||
|
|
# Read JSON
|
|||
|
|
with open(temp_file, 'r') as f:
|
|||
|
|
content = f.read().strip()
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
chat_data = json.loads(content)
|
|||
|
|
except json.JSONDecodeError as e:
|
|||
|
|
print(f"{RED}✗ Error:{RESET} Failed to parse JSON: {e}", file=sys.stderr)
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
messages = chat_data.get('messages', [])
|
|||
|
|
if isinstance(messages, dict):
|
|||
|
|
messages = list(messages.values())
|
|||
|
|
if not messages:
|
|||
|
|
print(f"{YELLOW}⚠ Warning:{RESET} No messages found in chat")
|
|||
|
|
sys.exit(0)
|
|||
|
|
|
|||
|
|
code_blocks_found = 0
|
|||
|
|
|
|||
|
|
# Extension mapping
|
|||
|
|
extensions = {
|
|||
|
|
'rust': '.rs', 'python': '.py', 'javascript': '.js', 'typescript': '.ts',
|
|||
|
|
'bash': '.sh', 'shell': '.sh', 'sh': '.sh', 'toml': '.toml',
|
|||
|
|
'yaml': '.yaml', 'yml': '.yml', 'json': '.json', 'md': '.md',
|
|||
|
|
'markdown': '.md', 'c': '.c', 'cpp': '.cpp', 'java': '.java',
|
|||
|
|
'go': '.go', 'html': '.html', 'css': '.css',
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for idx, message in enumerate(messages):
|
|||
|
|
role = message.get('role', 'unknown')
|
|||
|
|
content = message.get('content', '')
|
|||
|
|
|
|||
|
|
# Split content by code blocks
|
|||
|
|
parts = re.split(r'```', content)
|
|||
|
|
|
|||
|
|
block_idx = 0
|
|||
|
|
for i in range(1, len(parts), 2):
|
|||
|
|
if i >= len(parts):
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
# Get the code block
|
|||
|
|
code_block = parts[i]
|
|||
|
|
lines = code_block.split('\n', 1)
|
|||
|
|
language = lines[0].strip().lower() if lines else 'txt'
|
|||
|
|
code = lines[1] if len(lines) > 1 else code_block
|
|||
|
|
|
|||
|
|
# Look for filename in the text before this code block
|
|||
|
|
before_block = parts[i - 1]
|
|||
|
|
before_lines = before_block.strip().split('\n')
|
|||
|
|
|
|||
|
|
filename = None
|
|||
|
|
original_filename = None
|
|||
|
|
|
|||
|
|
# Check last few lines before code block for filenames
|
|||
|
|
for line in reversed(before_lines[-3:]):
|
|||
|
|
line = line.strip()
|
|||
|
|
# Pattern 1: ### `src/main.rs` or ### src/main.rs
|
|||
|
|
match = re.search(r'###?\s*`?([^\s`]+\.(rs|toml|py|js|sh|md|json|yaml|yml|html|css|c|cpp|java|go))`?', line, re.IGNORECASE)
|
|||
|
|
if match:
|
|||
|
|
original_filename = match.group(1)
|
|||
|
|
filename = original_filename # Keep original path structure
|
|||
|
|
break
|
|||
|
|
# Pattern 2: Look for step indicators with filenames
|
|||
|
|
match = re.search(r'[:#]\s*`?([^\s`]+\.(rs|toml|py|js|sh|md|json|yaml|yml|html|css|c|cpp|java|go))`?', line, re.IGNORECASE)
|
|||
|
|
if match:
|
|||
|
|
original_filename = match.group(1)
|
|||
|
|
filename = original_filename # Keep original path structure
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
# If no filename found, generate one
|
|||
|
|
if not filename:
|
|||
|
|
ext = extensions.get(language, '.txt')
|
|||
|
|
filename = f"{idx:03d}-{block_idx:02d}{ext}"
|
|||
|
|
|
|||
|
|
# Strip leading slashes/dots to prevent absolute path issues
|
|||
|
|
filename = filename.lstrip('/')
|
|||
|
|
|
|||
|
|
filepath = output_dir / filename
|
|||
|
|
|
|||
|
|
# Create parent directories if needed
|
|||
|
|
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|||
|
|
|
|||
|
|
# Save code
|
|||
|
|
with open(filepath, 'w') as f:
|
|||
|
|
f.write(code.strip())
|
|||
|
|
|
|||
|
|
if original_filename:
|
|||
|
|
print(f"{GREEN}✓{RESET} Saved: {filename} ({CYAN}{language}{RESET})")
|
|||
|
|
else:
|
|||
|
|
print(f"{GREEN}✓{RESET} Saved: {filename} ({CYAN}{language}{RESET})")
|
|||
|
|
code_blocks_found += 1
|
|||
|
|
block_idx += 1
|
|||
|
|
|
|||
|
|
if code_blocks_found == 0:
|
|||
|
|
print(f"{YELLOW}⚠ Warning:{RESET} No code blocks found in chat")
|
|||
|
|
else:
|
|||
|
|
print(f"\n{GREEN}✓{RESET} Exported {MAGENTA}{code_blocks_found}{RESET} code blocks to {BLUE}{output_dir}{RESET}")
|
|||
|
|
PYTHON_SCRIPT
|
|||
|
|
|
|||
|
|
exit 0
|