Files
crm-ai/scripts/twenty-bootstrap.js
T
2026-10-23 10:35:28 +08:00

184 lines
7.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
'use strict';
/**
* 在 Twenty 中创建初始用户 + Workspace,并直接在该库里生成 API key(不经过 UI)。
*
* 思路类比旧项目 gen_apikey.js:
* - 不用浏览器注册,脚本调 GraphQL signUp / signUpInNewWorkspace 建好 workspace
* - 再用 Twenty 自带命令 `workspace:generate-api-key` 在库内创建 apiKey + roleTarget,并签出 JWT
* - 结果写回 .env(TWENTY_WORKSPACE_ID / TWENTY_API_KEY),重建 bridge
*
* 用法:node scripts/twenty-bootstrap.js
*/
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { execFileSync } = require('child_process');
const ROOT = path.resolve(__dirname, '..');
const CONTAINER = 'crmnew-twenty-server';
function loadEnv(file) {
const out = {};
for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
const t = line.trim();
if (!t || t.startsWith('#')) continue;
const eq = t.indexOf('=');
if (eq < 0) continue;
let v = t.slice(eq + 1).trim();
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
out[t.slice(0, eq).trim()] = v;
}
return out;
}
function setEnv(file, kv) {
const lines = fs.readFileSync(file, 'utf8').split('\n');
for (const [k, v] of Object.entries(kv)) {
const idx = lines.findIndex((l) => l.startsWith(`${k}=`));
if (idx >= 0) lines[idx] = `${k}=${v}`;
else lines.push(`${k}=${v}`);
}
fs.writeFileSync(file, lines.join('\n'), { mode: 0o600 });
}
async function gql(base, query, variables, token) {
// 认证类 mutation 在 core(/metadata)端点,不在 workspace 数据端点 /graphql
const res = await fetch(`${base}/metadata`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({ query, variables }),
});
const json = await res.json();
if (json.errors && json.errors.length) {
throw new Error(`GraphQL error: ${JSON.stringify(json.errors).slice(0, 400)}`);
}
return json.data;
}
function psql(sql) {
return execFileSync('docker', ['exec', 'crmnew-postgres', 'psql', '-U', 'crmnew', '-d', 'twenty', '-tAc', sql], {
encoding: 'utf8',
}).trim();
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function main() {
const env = loadEnv(path.join(ROOT, '.env'));
const base = (env.TWENTY_SERVER_URL || 'http://192.168.31.61:3100').replace(/\/+$/, '');
const email = env.TWENTY_ADMIN_EMAIL;
const password = env.TWENTY_ADMIN_PASSWORD;
if (!email || !password) throw new Error('缺少 TWENTY_ADMIN_EMAIL / TWENTY_ADMIN_PASSWORD(写入 .env,不要硬编码)');
// 已存在 workspace 就跳过创建
let workspaceId = env.TWENTY_WORKSPACE_ID || '';
if (workspaceId) {
const exists = psql(`select count(*) from core.workspace where id='${workspaceId}';`);
if (exists === '1') console.log(`复用已有 workspace ${workspaceId}`);
else workspaceId = '';
}
if (!workspaceId) {
console.log('1) signUp…');
const signUp = await gql(base, `
mutation($email: String!, $password: String!) {
signUp(email: $email, password: $password) {
tokens { accessOrWorkspaceAgnosticToken { token } }
}
}
`, { email, password });
const agnostic = signUp.signUp.tokens.accessOrWorkspaceAgnosticToken.token;
console.log('2) signUpInNewWorkspace…');
const created = await gql(base, `
mutation($input: SignUpInNewWorkspaceInput) {
signUpInNewWorkspace(input: $input) {
loginToken { token }
workspace { id }
}
}
`, { input: { displayName: 'Yehwang', subdomain: 'yehwang' } }, agnostic);
workspaceId = created.signUpInNewWorkspace.workspace.id;
const loginToken = created.signUpInNewWorkspace.loginToken.token;
const workspaceOrigin = created.signUpInNewWorkspace.workspace.workspaceUrls?.subdomainUrl || base;
console.log(` workspace: ${workspaceId}`);
// 自托管环境没有真实邮件服务:直接标记邮箱已验证,才能换取 workspace token
psql(`update core."user" set "isEmailVerified"=true where email='${email}';`);
console.log('3) 交换 workspace token…');
const exchanged = await gql(base, `
mutation($loginToken: String!, $origin: String!) {
getAuthTokensFromLoginToken(loginToken: $loginToken, origin: $origin) {
tokens { accessOrWorkspaceAgnosticToken { token } }
}
}
`, { loginToken, origin: base });
const accessToken = exchanged.getAuthTokensFromLoginToken.tokens.accessOrWorkspaceAgnosticToken.token;
console.log('4) 激活 workspace(建 schema / 默认角色)…');
await gql(base, `
mutation($data: ActivateWorkspaceInput!) {
activateWorkspace(data: $data) { id }
}
`, { data: { displayName: 'Yehwang' } }, accessToken);
setEnv(path.join(ROOT, '.env'), {
TWENTY_WORKSPACE_ID: workspaceId,
TWENTY_ADMIN_EMAIL: email,
TWENTY_ADMIN_PASSWORD: password,
});
}
// 等 workspace 初始化完成(PENDING_CREATION -> ACTIVE/CREATED)
for (let i = 0; i < 60; i += 1) {
const status = psql(`select "activationStatus" from core.workspace where id='${workspaceId}';`);
if (status === 'ACTIVE' || status === 'CREATED') {
console.log(` workspace 状态: ${status}`);
break;
}
if (i === 59) throw new Error(`workspace 未就绪,状态=${status}`);
await sleep(2000);
}
// 3) 在库内生成 API key(Twenty 官方命令,dev/test 环境限制)
console.log('5) 生成 API key(workspace:generate-api-key)…');
const out = execFileSync('docker', [
'exec', '-e', 'NODE_ENV=development', CONTAINER, 'sh', '-lc',
`cd /app/packages/twenty-server && yarn command:prod workspace:generate-api-key -w ${workspaceId} -n crm-new`,
], { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 });
const m = out.match(/TOKEN:(\S+)/);
if (!m) throw new Error(`未解析到 TOKEN。输出尾部:\n${out.slice(-1200)}`);
const apiKey = m[1];
setEnv(path.join(ROOT, '.env'), { TWENTY_API_KEY: apiKey });
console.log(` API key 已写入 .env(${apiKey.slice(0, 24)}…)`);
// 4) 重建 bridge
console.log('6) 重建 bridge…');
execFileSync('docker', ['compose', 'up', '-d', '--force-recreate', 'bridge'], { cwd: ROOT, stdio: 'inherit' });
await sleep(3000);
// 5) 验证
console.log('7) 验证 API key…');
const check = await fetch(`${base}/graphql`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
body: JSON.stringify({ query: '{ companies(first: 1) { edges { node { id } } } }' }),
});
const checkJson = await check.json();
if (!check.ok || checkJson.errors) throw new Error(`API key 验证失败:${JSON.stringify(checkJson).slice(0, 400)}`);
console.log(' ✓ API key 可用(companies 查询成功)');
console.log('\n完成。TWENTY_WORKSPACE_ID / TWENTY_API_KEY 已写入 .env');
}
main().catch((err) => {
console.error('[twenty-bootstrap]', err.message);
process.exit(1);
});