| #!/usr/bin/env node |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import pg from "pg"; |
| import bcrypt from "bcrypt"; |
| import { ulid } from "ulid"; |
|
|
| function parseArgs(argv) { |
| const out = {}; |
| for (let i = 2; i < argv.length; i++) { |
| const a = argv[i]; |
| if (a === "--invite-codes") out.inviteCodes = argv[++i]; |
| else if (a.startsWith("--invite-codes=")) out.inviteCodes = a.slice("--invite-codes=".length); |
| else if (a === "--help" || a === "-h") out.help = true; |
| } |
| return out; |
| } |
|
|
| function parseList(raw) { |
| return String(raw || "") |
| .split(",") |
| .map((s) => s.trim()) |
| .filter(Boolean); |
| } |
|
|
| function defaultPeriodEnd(start = new Date()) { |
| return new Date(start.getTime() + 30 * 24 * 60 * 60 * 1000); |
| } |
|
|
| function defaultSettingsBlob() { |
| return { |
| appearance: { |
| theme: "system", |
| density: "comfortable", |
| font_scale: 1.0, |
| code_theme: "github-dark", |
| }, |
| language: "zh-CN", |
| default_model_id: "mdl_claude-sonnet-4-6", |
| send_on_enter: true, |
| show_token_count: true, |
| spellcheck: true, |
| voice: { enabled: false, voice_id: null, speed: 1.0 }, |
| data_controls: { |
| chat_history_enabled: true, |
| improve_model_with_my_data: false, |
| auto_archive_after_days: 0, |
| }, |
| beta_features: { streaming: true, code_interpreter: false }, |
| notifications: { job_completed: true, weekly_digest: false }, |
| long_term_memory: { |
| enabled: false, |
| auto_extract: true, |
| max_facts: 100, |
| max_tokens_per_turn: 2000, |
| }, |
| }; |
| } |
|
|
| async function main() { |
| const args = parseArgs(process.argv); |
| if (args.help) { |
| process.stdout.write( |
| "Usage: node artifacts/api-server/scripts/seed-prod.mjs [--invite-codes A,B,C]\n", |
| ); |
| return; |
| } |
|
|
| const url = process.env.DATABASE_URL; |
| if (!url) { |
| console.error("seed-prod: DATABASE_URL is required"); |
| process.exit(1); |
| } |
|
|
| const codes = parseList( |
| args.inviteCodes || |
| process.env.INITIAL_INVITE_CODES || |
| process.env.INVITE_CODES, |
| ); |
|
|
| const adminEmail = String(process.env.INITIAL_ADMIN_EMAIL || "").trim().toLowerCase(); |
| const adminUsername = String(process.env.INITIAL_ADMIN_USERNAME || "").trim().toLowerCase(); |
| const adminPassword = String(process.env.INITIAL_ADMIN_PASSWORD || ""); |
| const wantsAdmin = Boolean(adminEmail || adminUsername || adminPassword); |
|
|
| if (!codes.length && !wantsAdmin) { |
| console.warn( |
| "seed-prod: no INITIAL_INVITE_CODES / INVITE_CODES and no INITIAL_ADMIN_* set; nothing to do.", |
| ); |
| return; |
| } |
|
|
| const pool = new pg.Pool({ connectionString: url }); |
| const client = await pool.connect(); |
| try { |
| |
| if (codes.length) { |
| let inserted = 0; |
| let skipped = 0; |
| for (const code of codes) { |
| const r = await client.query( |
| `INSERT INTO invite_codes (code) VALUES ($1) |
| ON CONFLICT (code) DO NOTHING |
| RETURNING code`, |
| [code], |
| ); |
| if (r.rowCount === 1) inserted++; |
| else skipped++; |
| } |
| console.log( |
| `seed-prod: invite_codes — inserted=${inserted} skipped=${skipped} total=${codes.length}`, |
| ); |
| } else { |
| console.log("seed-prod: no invite codes provided; skipping invite seed"); |
| } |
|
|
| |
| if (wantsAdmin) { |
| if (!adminEmail || !adminUsername || !adminPassword) { |
| console.error( |
| "seed-prod: INITIAL_ADMIN_EMAIL, INITIAL_ADMIN_USERNAME and INITIAL_ADMIN_PASSWORD must all be set together.", |
| ); |
| process.exit(1); |
| } |
| if (adminPassword.length < 8) { |
| console.error("seed-prod: INITIAL_ADMIN_PASSWORD must be at least 8 characters."); |
| process.exit(1); |
| } |
|
|
| const existing = await client.query( |
| `SELECT id, email, username FROM users WHERE email = $1 OR username = $2 LIMIT 1`, |
| [adminEmail, adminUsername], |
| ); |
| if (existing.rowCount && existing.rowCount > 0) { |
| const row = existing.rows[0]; |
| console.log( |
| `seed-prod: admin user already exists (id=${row.id} email=${row.email} username=${row.username}); not overwriting`, |
| ); |
| } else { |
| const id = `usr_${ulid()}`; |
| const now = new Date(); |
| const passwordHash = await bcrypt.hash(adminPassword, 12); |
| await client.query("BEGIN"); |
| try { |
| await client.query( |
| `INSERT INTO users |
| (id, email, username, password_hash, display_name, role, plan, |
| tokens_limit, tokens_used, period_start, period_end, created_at) |
| VALUES ($1,$2,$3,$4,$5,'admin','free',5000000,0,$6,$7,$6)`, |
| [ |
| id, |
| adminEmail, |
| adminUsername, |
| passwordHash, |
| adminUsername, |
| now, |
| defaultPeriodEnd(now), |
| ], |
| ); |
| await client.query( |
| `INSERT INTO user_settings (user_id, data) VALUES ($1, $2::jsonb) |
| ON CONFLICT (user_id) DO NOTHING`, |
| [id, JSON.stringify(defaultSettingsBlob())], |
| ); |
| await client.query( |
| `INSERT INTO custom_instructions (user_id, about_me, response_style, enabled) |
| VALUES ($1, '', '', true) |
| ON CONFLICT (user_id) DO NOTHING`, |
| [id], |
| ); |
| await client.query("COMMIT"); |
| console.log( |
| `seed-prod: created admin user id=${id} email=${adminEmail} username=${adminUsername}`, |
| ); |
| } catch (err) { |
| await client.query("ROLLBACK"); |
| throw err; |
| } |
| } |
| } else { |
| console.log("seed-prod: no INITIAL_ADMIN_* provided; skipping admin user"); |
| } |
| } finally { |
| client.release(); |
| await pool.end(); |
| } |
| } |
|
|
| main().catch((err) => { |
| console.error("seed-prod: failed:", err?.stack || err?.message || err); |
| process.exit(1); |
| }); |
|
|