feat: content generation engine - spintax resolver, API endpoints, BullMQ worker
This commit is contained in:
122
src/pages/api/god/campaigns/create.ts
Normal file
122
src/pages/api/god/campaigns/create.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
// API Endpoint: POST /api/god/campaigns/create
|
||||
import type { APIRoute } from 'astro';
|
||||
import { pool } from '../../../../lib/db/db';
|
||||
import crypto from 'crypto';
|
||||
|
||||
interface CampaignBlueprint {
|
||||
asset_name: string;
|
||||
deployment_target: string;
|
||||
variables: Record<string, string>;
|
||||
content: {
|
||||
url_path: string;
|
||||
meta_description: string;
|
||||
body: Array<{
|
||||
block_type: string;
|
||||
content: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
// Auth check
|
||||
const godToken = request.headers.get('X-God-Token');
|
||||
if (!godToken || godToken !== import.meta.env.GOD_TOKEN) {
|
||||
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { name, blueprint } = body as { name?: string; blueprint: CampaignBlueprint };
|
||||
|
||||
if (!blueprint || !blueprint.content || !blueprint.variables) {
|
||||
return new Response(JSON.stringify({ error: 'Invalid blueprint structure' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Get admin site ID
|
||||
const siteResult = await client.query(
|
||||
`SELECT id FROM sites WHERE domain = 'spark.jumpstartscaling.com' LIMIT 1`
|
||||
);
|
||||
|
||||
if (siteResult.rows.length === 0) {
|
||||
throw new Error('Admin site not found');
|
||||
}
|
||||
|
||||
const siteId = siteResult.rows[0].id;
|
||||
|
||||
// Insert campaign
|
||||
const campaignName = name || blueprint.asset_name;
|
||||
const campaignResult = await client.query(
|
||||
`INSERT INTO campaign_masters (site_id, name, blueprint_json, status)
|
||||
VALUES ($1, $2, $3, 'pending')
|
||||
RETURNING id`,
|
||||
[siteId, campaignName, JSON.stringify(blueprint)]
|
||||
);
|
||||
|
||||
const campaignId = campaignResult.rows[0].id;
|
||||
|
||||
// Insert content fragments (blocks)
|
||||
for (const block of blueprint.content.body) {
|
||||
const contentHash = crypto.createHash('sha256')
|
||||
.update(block.content)
|
||||
.digest('hex')
|
||||
.substring(0, 32);
|
||||
|
||||
await client.query(
|
||||
`INSERT INTO content_fragments (
|
||||
site_id, campaign_id, block_type, blueprint_name, content, content_hash, use_count
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, 0)
|
||||
ON CONFLICT (content_hash) DO UPDATE SET
|
||||
campaign_id = EXCLUDED.campaign_id,
|
||||
use_count = content_fragments.use_count
|
||||
`,
|
||||
[
|
||||
siteId,
|
||||
campaignId,
|
||||
block.block_type.replace(/ \(\d+\)$/, ''),
|
||||
campaignName,
|
||||
block.content,
|
||||
contentHash
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
campaignId,
|
||||
message: `Campaign "${campaignName}" created successfully. Use /launch/${campaignId} to generate content.`
|
||||
}), {
|
||||
status: 201,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
await client.query('ROLLBACK');
|
||||
throw err;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Campaign creation error:', error);
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Campaign creation failed',
|
||||
details: error.message
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
};
|
||||
71
src/pages/api/god/campaigns/launch/[id].ts
Normal file
71
src/pages/api/god/campaigns/launch/[id].ts
Normal file
@@ -0,0 +1,71 @@
|
||||
// API Endpoint: POST /api/god/campaigns/launch/[id]
|
||||
import type { APIRoute } from 'astro';
|
||||
import { pool } from '../../../../../lib/db/db';
|
||||
import { batchQueue } from '../../../../../lib/queue/config';
|
||||
|
||||
export const POST: APIRoute = async ({ params, request }) => {
|
||||
try {
|
||||
const godToken = request.headers.get('X-God-Token');
|
||||
if (!godToken || godToken !== import.meta.env.GOD_TOKEN) {
|
||||
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const { id } = params;
|
||||
if (!id) {
|
||||
return new Response(JSON.stringify({ error: 'Campaign ID required' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch campaign
|
||||
const result = await pool.query(
|
||||
`SELECT id, name, blueprint_json FROM campaign_masters WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return new Response(JSON.stringify({ error: 'Campaign not found' }), {
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const campaign = result.rows[0];
|
||||
|
||||
// Queue the job
|
||||
await batchQueue.add('generate_campaign_content', {
|
||||
campaignId: id,
|
||||
campaignName: campaign.name
|
||||
});
|
||||
|
||||
// Update status
|
||||
await pool.query(
|
||||
`UPDATE campaign_masters SET status = 'processing', updated_at = NOW() WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
campaignId: id,
|
||||
message: 'Campaign queued for generation',
|
||||
status: 'processing'
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Campaign launch error:', error);
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Campaign launch failed',
|
||||
details: error.message
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
};
|
||||
75
src/pages/api/god/campaigns/status/[id].ts
Normal file
75
src/pages/api/god/campaigns/status/[id].ts
Normal file
@@ -0,0 +1,75 @@
|
||||
// API Endpoint: GET /api/god/campaigns/status/[id]
|
||||
import type { APIRoute } from 'astro';
|
||||
import { pool } from '../../../../../lib/db/db';
|
||||
|
||||
export const GET: APIRoute = async ({ params, request }) => {
|
||||
try {
|
||||
const godToken = request.headers.get('X-God-Token');
|
||||
if (!godToken || godToken !== import.meta.env.GOD_TOKEN) {
|
||||
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const { id } = params;
|
||||
|
||||
// Get campaign details
|
||||
const campaignResult = await pool.query(
|
||||
`SELECT id, name, status, created_at, updated_at FROM campaign_masters WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (campaignResult.rows.length === 0) {
|
||||
return new Response(JSON.stringify({ error: 'Campaign not found' }), {
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const campaign = campaignResult.rows[0];
|
||||
|
||||
// Count generated posts
|
||||
const postsResult = await pool.query(
|
||||
`SELECT COUNT(*) as count FROM variation_registry WHERE campaign_id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
const postsCreated = parseInt(postsResult.rows[0].count);
|
||||
|
||||
// Get block usage stats
|
||||
const blockStats = await pool.query(
|
||||
`SELECT block_type, total_uses
|
||||
FROM block_usage_stats
|
||||
WHERE content_fragment_id IN (
|
||||
SELECT id FROM content_fragments WHERE campaign_id = $1
|
||||
)
|
||||
ORDER BY total_uses DESC
|
||||
LIMIT 10`,
|
||||
[id]
|
||||
);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
campaignId: campaign.id,
|
||||
name: campaign.name,
|
||||
status: campaign.status,
|
||||
postsCreated,
|
||||
createdAt: campaign.created_at,
|
||||
updatedAt: campaign.updated_at,
|
||||
blockUsage: blockStats.rows
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Status check error:', error);
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Status check failed',
|
||||
details: error.message
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user