chore: CRM-NEW baseline
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
#!/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);
|
||||
});
|
||||
Reference in New Issue
Block a user