@sulala/agent 0.1.39 → 0.1.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/bin/sulala copy.mjs +7 -0
- package/bin/sulala.mjs +10 -4
- package/dashboard/dist/assets/index-CpWzBXHV.js +88 -0
- package/dashboard/dist/assets/index-DUAWeMLm.css +1 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/agent/content-writer-agent.d.ts +52 -0
- package/dist/agent/content-writer-agent.d.ts.map +1 -0
- package/dist/agent/content-writer-agent.js +271 -0
- package/dist/agent/content-writer-agent.js.map +1 -0
- package/dist/agent/execution-context.d.ts +45 -0
- package/dist/agent/execution-context.d.ts.map +1 -0
- package/dist/agent/execution-context.js +31 -0
- package/dist/agent/execution-context.js.map +1 -0
- package/dist/agent/lead-agent-schedule.d.ts +10 -0
- package/dist/agent/lead-agent-schedule.d.ts.map +1 -0
- package/dist/agent/lead-agent-schedule.js +49 -0
- package/dist/agent/lead-agent-schedule.js.map +1 -0
- package/dist/agent/lead-agent.d.ts +60 -0
- package/dist/agent/lead-agent.d.ts.map +1 -0
- package/dist/agent/lead-agent.js +205 -0
- package/dist/agent/lead-agent.js.map +1 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +66 -21
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/news-social-agent-schedule.d.ts +5 -0
- package/dist/agent/news-social-agent-schedule.d.ts.map +1 -0
- package/dist/agent/news-social-agent-schedule.js +42 -0
- package/dist/agent/news-social-agent-schedule.js.map +1 -0
- package/dist/agent/news-social-agent.d.ts +52 -0
- package/dist/agent/news-social-agent.d.ts.map +1 -0
- package/dist/agent/news-social-agent.js +199 -0
- package/dist/agent/news-social-agent.js.map +1 -0
- package/dist/agent/run-cancel.d.ts +20 -0
- package/dist/agent/run-cancel.d.ts.map +1 -0
- package/dist/agent/run-cancel.js +38 -0
- package/dist/agent/run-cancel.js.map +1 -0
- package/dist/agent/skills.d.ts.map +1 -1
- package/dist/agent/skills.js +97 -118
- package/dist/agent/skills.js.map +1 -1
- package/dist/agent/social-agent.d.ts +46 -0
- package/dist/agent/social-agent.d.ts.map +1 -0
- package/dist/agent/social-agent.js +199 -0
- package/dist/agent/social-agent.js.map +1 -0
- package/dist/agent/tool/spec-loader.d.ts.map +1 -1
- package/dist/agent/tool/spec-loader.js +62 -8
- package/dist/agent/tool/spec-loader.js.map +1 -1
- package/dist/agent/tools.d.ts.map +1 -1
- package/dist/agent/tools.js +204 -4
- package/dist/agent/tools.js.map +1 -1
- package/dist/agent/workflow-agents-schedule.d.ts +14 -0
- package/dist/agent/workflow-agents-schedule.d.ts.map +1 -0
- package/dist/agent/workflow-agents-schedule.js +46 -0
- package/dist/agent/workflow-agents-schedule.js.map +1 -0
- package/dist/agent/workflow-agents.d.ts +38 -0
- package/dist/agent/workflow-agents.d.ts.map +1 -0
- package/dist/agent/workflow-agents.js +155 -0
- package/dist/agent/workflow-agents.js.map +1 -0
- package/dist/agent/workflow-folder.d.ts +18 -0
- package/dist/agent/workflow-folder.d.ts.map +1 -0
- package/dist/agent/workflow-folder.js +57 -0
- package/dist/agent/workflow-folder.js.map +1 -0
- package/dist/agent/workflow-runner.d.ts +16 -0
- package/dist/agent/workflow-runner.d.ts.map +1 -0
- package/dist/agent/workflow-runner.js +163 -0
- package/dist/agent/workflow-runner.js.map +1 -0
- package/dist/channels/telegram.d.ts +15 -0
- package/dist/channels/telegram.d.ts.map +1 -1
- package/dist/channels/telegram.js +100 -0
- package/dist/channels/telegram.js.map +1 -1
- package/dist/config.d.ts +11 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +74 -2
- package/dist/config.js.map +1 -1
- package/dist/db/index.d.ts +78 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +279 -0
- package/dist/db/index.js.map +1 -1
- package/dist/db/schema.sql +47 -0
- package/dist/gateway/server.d.ts.map +1 -1
- package/dist/gateway/server.js +1057 -13
- package/dist/gateway/server.js.map +1 -1
- package/dist/index.js +315 -2
- package/dist/index.js.map +1 -1
- package/dist/media/upload-handlers/dropbox.d.ts +2 -0
- package/dist/media/upload-handlers/dropbox.d.ts.map +1 -0
- package/dist/media/upload-handlers/dropbox.js +67 -0
- package/dist/media/upload-handlers/dropbox.js.map +1 -0
- package/dist/onboard-env.d.ts +9 -1
- package/dist/onboard-env.d.ts.map +1 -1
- package/dist/onboard-env.js +15 -1
- package/dist/onboard-env.js.map +1 -1
- package/dist/onboard.d.ts.map +1 -1
- package/dist/onboard.js +7 -0
- package/dist/onboard.js.map +1 -1
- package/dist/tailscale.d.ts +5 -0
- package/dist/tailscale.d.ts.map +1 -0
- package/dist/tailscale.js +36 -0
- package/dist/tailscale.js.map +1 -0
- package/dist/types.d.ts +16 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +16 -2
- package/src/db/schema.sql +47 -0
- package/src/index.ts +326 -2
- package/dashboard/dist/assets/index-C-H2KUVU.js +0 -88
- package/dashboard/dist/assets/index-CG6dieHw.css +0 -1
package/dist/gateway/server.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import { createServer } from 'http';
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'fs';
|
|
4
|
+
import { homedir } from 'os';
|
|
4
5
|
import { join, resolve, sep } from 'path';
|
|
5
6
|
import { randomBytes } from 'crypto';
|
|
6
7
|
import { WebSocketServer } from 'ws';
|
|
7
8
|
import cors from 'cors';
|
|
8
|
-
import { config, getSulalaEnvPath, getSulalaEnvKey, getPortalGatewayBase, getEffectivePortalApiKey } from '../config.js';
|
|
9
|
+
import { config, ensureProtectedBindOrExit, getSulalaEnvPath, getSulalaEnvKey, getPortalGatewayBase, getEffectivePortalApiKey, isLocalRequest } from '../config.js';
|
|
10
|
+
import { startTailscaleServe } from '../tailscale.js';
|
|
9
11
|
import { readOnboardEnvKeys, writeOnboardEnvKeys } from '../onboard-env.js';
|
|
10
|
-
import { initDb, getDb, log, insertTask, getFileStates, updateTaskStatus, setTaskPendingForRetry, getOrCreateAgentSession, getAgentSessionById, getAgentMessages, appendAgentMessage, updateAgentMessageToolResult, listAgentMemories, listAgentMemoryScopeKeys, listAgentSessions, listScheduledJobs, getScheduledJob, insertScheduledJob, updateScheduledJob, deleteScheduledJob, getTasksForJob, getChannelConfig, setChannelConfig, } from '../db/index.js';
|
|
12
|
+
import { initDb, getDb, log, insertTask, getFileStates, updateTaskStatus, setTaskPendingForRetry, getOrCreateAgentSession, getAgentSessionById, getAgentMessages, appendAgentMessage, updateAgentMessageToolResult, listAgentMemories, listAgentMemoryScopeKeys, listAgentSessions, listScheduledJobs, getScheduledJob, insertScheduledJob, updateScheduledJob, deleteScheduledJob, getTasksForJob, getChannelConfig, setChannelConfig, getUsageSummary, listAgents, getAgentById, getAgentBySlug, getLatestAgentMetrics, listWorkflows, getWorkflow, insertWorkflow, updateWorkflow, deleteWorkflow, getScheduledJobIdForWorkflow, } from '../db/index.js';
|
|
11
13
|
import { scheduleCronById, unscheduleJob } from '../scheduler/cron.js';
|
|
12
14
|
import { getQueueLength } from '../scheduler/queue.js';
|
|
13
15
|
import { getTelegramChannelState, setTelegramChannelConfig } from '../channels/telegram.js';
|
|
@@ -15,6 +17,10 @@ import { getDiscordChannelState, setDiscordChannelConfig } from '../channels/dis
|
|
|
15
17
|
import { reloadProviders } from '../ai/orchestrator.js';
|
|
16
18
|
import { runAgentTurn, runAgentTurnStream } from '../agent/loop.js';
|
|
17
19
|
import { runAgentTurnWithPi, isPiAvailable } from '../agent/pi-runner.js';
|
|
20
|
+
import { requestCancel, registerCancelController } from '../agent/run-cancel.js';
|
|
21
|
+
import { isWorkflowAgent, SLUG_TO_TASK_TYPE } from '../agent/workflow-agents.js';
|
|
22
|
+
import { ensureWorkflowFolder } from '../agent/workflow-folder.js';
|
|
23
|
+
import { WORKFLOW_AGENT_CONFIG } from '../agent/workflow-agents-schedule.js';
|
|
18
24
|
import { listTools, executeTool, refreshSpecTools } from '../agent/tools.js';
|
|
19
25
|
import { listSkills, getAllRequiredBins } from '../agent/skills.js';
|
|
20
26
|
import { getRegistrySkills, getAvailableUpdates, installSkill, uninstallSkill, updateSkillsAll, installAllSystemSkills, uploadSkill, getInstallSourceMap } from '../agent/skill-install.js';
|
|
@@ -93,6 +99,9 @@ function getStorePublishBaseUrl() {
|
|
|
93
99
|
}
|
|
94
100
|
export function createGateway(appMount = null) {
|
|
95
101
|
const app = appMount || express();
|
|
102
|
+
if (process.env.SULALA_TAILSCALE_SERVE === '1') {
|
|
103
|
+
app.set('trust proxy', 1);
|
|
104
|
+
}
|
|
96
105
|
app.use(cors());
|
|
97
106
|
// Skip default JSON body parser for /api/upload so the route can use UPLOAD_EXPRESS_LIMIT for video uploads
|
|
98
107
|
app.use((req, res, next) => {
|
|
@@ -116,8 +125,13 @@ export function createGateway(appMount = null) {
|
|
|
116
125
|
return next();
|
|
117
126
|
if (req.path === '/onboard' || req.path.startsWith('/api/onboard') || req.path.startsWith('/api/ollama'))
|
|
118
127
|
return next();
|
|
128
|
+
if (req.path === '/api/settings/gateway-api-key')
|
|
129
|
+
return next();
|
|
119
130
|
if (req.path.startsWith('/api/oauth/') && (req.path.endsWith('/authorize') || req.path.endsWith('/callback')))
|
|
120
131
|
return next();
|
|
132
|
+
const remote = (req.socket?.remoteAddress ?? req.ip ?? '').trim();
|
|
133
|
+
if (isLocalRequest(remote))
|
|
134
|
+
return next();
|
|
121
135
|
const key = req.headers['x-api-key'] || req.query.api_key;
|
|
122
136
|
if (key === config.gatewayApiKey)
|
|
123
137
|
return next();
|
|
@@ -384,6 +398,33 @@ export function createGateway(appMount = null) {
|
|
|
384
398
|
sendGatewayError(res, e);
|
|
385
399
|
}
|
|
386
400
|
});
|
|
401
|
+
/** Aggregate token usage and cost over a time window. Defaults to last 7 days. */
|
|
402
|
+
app.get('/api/agent/usage', (req, res) => {
|
|
403
|
+
try {
|
|
404
|
+
const now = Date.now();
|
|
405
|
+
const daysRaw = typeof req.query.days === 'string' ? parseInt(req.query.days, 10) : NaN;
|
|
406
|
+
const days = Number.isFinite(daysRaw) && daysRaw > 0 ? daysRaw : 7;
|
|
407
|
+
const toMsRaw = typeof req.query.to === 'string' ? parseInt(req.query.to, 10) : NaN;
|
|
408
|
+
const toMs = Number.isFinite(toMsRaw) && toMsRaw > 0 ? toMsRaw : now;
|
|
409
|
+
const fromMsRaw = typeof req.query.from === 'string' ? parseInt(req.query.from, 10) : NaN;
|
|
410
|
+
const fromMs = Number.isFinite(fromMsRaw) && fromMsRaw > 0
|
|
411
|
+
? fromMsRaw
|
|
412
|
+
: toMs - days * 24 * 60 * 60 * 1000;
|
|
413
|
+
const summary = getUsageSummary({ fromMs, toMs });
|
|
414
|
+
res.json({
|
|
415
|
+
from: summary.fromMs,
|
|
416
|
+
to: summary.toMs,
|
|
417
|
+
total_messages: summary.totalMessages,
|
|
418
|
+
prompt_tokens: summary.promptTokens,
|
|
419
|
+
completion_tokens: summary.completionTokens,
|
|
420
|
+
total_tokens: summary.totalTokens,
|
|
421
|
+
cost_usd: summary.costUsd,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
catch (e) {
|
|
425
|
+
sendGatewayError(res, e);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
387
428
|
app.post('/api/tasks', (req, res) => {
|
|
388
429
|
try {
|
|
389
430
|
const { type, payload, scheduled_at } = req.body || {};
|
|
@@ -489,6 +530,23 @@ export function createGateway(appMount = null) {
|
|
|
489
530
|
sendGatewayError(res, e);
|
|
490
531
|
}
|
|
491
532
|
});
|
|
533
|
+
/** GET /api/settings/gateway-api-key — Return API key only when request is from loopback (for Settings display). Remote: returns hasKey only. */
|
|
534
|
+
app.get('/api/settings/gateway-api-key', (_req, res) => {
|
|
535
|
+
try {
|
|
536
|
+
const remote = (_req.socket?.remoteAddress ?? _req.ip ?? '').trim();
|
|
537
|
+
const fromLocal = isLocalRequest(remote);
|
|
538
|
+
const key = config.gatewayApiKey?.trim();
|
|
539
|
+
if (fromLocal && key) {
|
|
540
|
+
res.json({ apiKey: key, hasKey: true });
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
res.json({ hasKey: !!key });
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
catch (e) {
|
|
547
|
+
sendGatewayError(res, e);
|
|
548
|
+
}
|
|
549
|
+
});
|
|
492
550
|
/** POST /api/mcp/reload — Re-read mcp.json (or MCP_SERVERS env) and reload MCP tools. Use after editing ~/.sulala/mcp.json by hand so changes take effect without restarting the agent. */
|
|
493
551
|
app.post('/api/mcp/reload', async (_req, res) => {
|
|
494
552
|
try {
|
|
@@ -775,6 +833,579 @@ export function createGateway(appMount = null) {
|
|
|
775
833
|
sendGatewayError(res, e);
|
|
776
834
|
}
|
|
777
835
|
});
|
|
836
|
+
const LEAD_AGENT_SETUP_KEY = 'lead_agent_setup';
|
|
837
|
+
app.get('/api/settings/lead-agent-setup', (_req, res) => {
|
|
838
|
+
try {
|
|
839
|
+
const raw = getChannelConfig(LEAD_AGENT_SETUP_KEY);
|
|
840
|
+
if (!raw?.trim()) {
|
|
841
|
+
res.json({
|
|
842
|
+
targetIndustry: '',
|
|
843
|
+
targetRole: '',
|
|
844
|
+
companySize: 'any',
|
|
845
|
+
leadSource: 'web_search',
|
|
846
|
+
numLeads: 10,
|
|
847
|
+
geographicFocus: '',
|
|
848
|
+
enrichmentDepth: 'standard',
|
|
849
|
+
});
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
const o = JSON.parse(raw);
|
|
853
|
+
res.json({
|
|
854
|
+
targetIndustry: typeof o.targetIndustry === 'string' ? o.targetIndustry : '',
|
|
855
|
+
targetRole: typeof o.targetRole === 'string' ? o.targetRole : '',
|
|
856
|
+
companySize: ['any', 'startup', 'smb', 'enterprise'].includes(String(o.companySize)) ? o.companySize : 'any',
|
|
857
|
+
leadSource: ['web_search', 'custom'].includes(String(o.leadSource)) ? o.leadSource : 'web_search',
|
|
858
|
+
numLeads: typeof o.numLeads === 'number' && o.numLeads >= 1 ? Math.min(100, Math.round(o.numLeads)) : 10,
|
|
859
|
+
geographicFocus: typeof o.geographicFocus === 'string' ? o.geographicFocus : '',
|
|
860
|
+
enrichmentDepth: ['basic', 'standard', 'deep'].includes(String(o.enrichmentDepth)) ? o.enrichmentDepth : 'standard',
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
catch (e) {
|
|
864
|
+
sendGatewayError(res, e);
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
app.put('/api/settings/lead-agent-setup', (req, res) => {
|
|
868
|
+
try {
|
|
869
|
+
const body = (req.body || {});
|
|
870
|
+
const targetIndustry = typeof body.targetIndustry === 'string' ? body.targetIndustry.trim() : '';
|
|
871
|
+
const targetRole = typeof body.targetRole === 'string' ? body.targetRole.trim() : '';
|
|
872
|
+
const companySize = ['any', 'startup', 'smb', 'enterprise'].includes(String(body.companySize)) ? body.companySize : 'any';
|
|
873
|
+
const leadSource = ['web_search', 'custom'].includes(String(body.leadSource)) ? body.leadSource : 'web_search';
|
|
874
|
+
const numLeads = typeof body.numLeads === 'number' && body.numLeads >= 1 ? Math.min(100, Math.round(body.numLeads)) : 10;
|
|
875
|
+
const geographicFocus = typeof body.geographicFocus === 'string' ? body.geographicFocus.trim() : '';
|
|
876
|
+
const enrichmentDepth = ['basic', 'standard', 'deep'].includes(String(body.enrichmentDepth)) ? body.enrichmentDepth : 'standard';
|
|
877
|
+
setChannelConfig(LEAD_AGENT_SETUP_KEY, JSON.stringify({
|
|
878
|
+
targetIndustry,
|
|
879
|
+
targetRole,
|
|
880
|
+
companySize,
|
|
881
|
+
leadSource,
|
|
882
|
+
numLeads,
|
|
883
|
+
geographicFocus,
|
|
884
|
+
enrichmentDepth,
|
|
885
|
+
}));
|
|
886
|
+
res.json({ ok: true });
|
|
887
|
+
}
|
|
888
|
+
catch (e) {
|
|
889
|
+
sendGatewayError(res, e);
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
app.delete('/api/settings/lead-agent-setup', (_req, res) => {
|
|
893
|
+
try {
|
|
894
|
+
setChannelConfig(LEAD_AGENT_SETUP_KEY, '');
|
|
895
|
+
res.json({ ok: true });
|
|
896
|
+
}
|
|
897
|
+
catch (e) {
|
|
898
|
+
sendGatewayError(res, e);
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
const LEAD_AGENT_SCHEDULE_ID = 'lead-agent-daily';
|
|
902
|
+
const DEFAULT_LEAD_AGENT_CRON = '0 8 * * *';
|
|
903
|
+
app.get('/api/settings/lead-agent-schedule', (_req, res) => {
|
|
904
|
+
try {
|
|
905
|
+
const row = getScheduledJob(LEAD_AGENT_SCHEDULE_ID)
|
|
906
|
+
?? listScheduledJobs(false).find((j) => j.task_type === 'lead_agent_run');
|
|
907
|
+
if (!row) {
|
|
908
|
+
res.status(404).json({ error: 'Lead Agent schedule not found' });
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
res.json(row);
|
|
912
|
+
}
|
|
913
|
+
catch (e) {
|
|
914
|
+
sendGatewayError(res, e);
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
app.put('/api/settings/lead-agent-schedule', (req, res) => {
|
|
918
|
+
try {
|
|
919
|
+
const body = (req.body || {});
|
|
920
|
+
const cronExpression = typeof body.cron_expression === 'string' && body.cron_expression.trim()
|
|
921
|
+
? body.cron_expression.trim()
|
|
922
|
+
: DEFAULT_LEAD_AGENT_CRON;
|
|
923
|
+
const enabled = body.enabled !== false ? 1 : 0;
|
|
924
|
+
const agent = getAgentBySlug('lead-agent');
|
|
925
|
+
if (!agent) {
|
|
926
|
+
res.status(400).json({ error: 'Lead Agent not found' });
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
let row = getScheduledJob(LEAD_AGENT_SCHEDULE_ID)
|
|
930
|
+
?? listScheduledJobs(false).find((j) => j.task_type === 'lead_agent_run');
|
|
931
|
+
if (!row) {
|
|
932
|
+
const payloadStr = JSON.stringify({
|
|
933
|
+
agentId: agent.id,
|
|
934
|
+
provider: agent.default_provider || 'openai',
|
|
935
|
+
model: agent.default_model || 'gpt-4o-mini',
|
|
936
|
+
});
|
|
937
|
+
insertScheduledJob({
|
|
938
|
+
id: LEAD_AGENT_SCHEDULE_ID,
|
|
939
|
+
name: 'Lead Agent Daily Run',
|
|
940
|
+
description: 'Autonomous lead generation and reporting',
|
|
941
|
+
cron_expression: cronExpression,
|
|
942
|
+
task_type: 'lead_agent_run',
|
|
943
|
+
payload: payloadStr,
|
|
944
|
+
enabled,
|
|
945
|
+
});
|
|
946
|
+
row = getScheduledJob(LEAD_AGENT_SCHEDULE_ID);
|
|
947
|
+
}
|
|
948
|
+
else {
|
|
949
|
+
updateScheduledJob(row.id, { cron_expression: cronExpression, enabled });
|
|
950
|
+
unscheduleJob(row.id);
|
|
951
|
+
row = getScheduledJob(row.id);
|
|
952
|
+
}
|
|
953
|
+
if (row.enabled) {
|
|
954
|
+
const payload = row.payload ? JSON.parse(row.payload) : { agentId: agent.id };
|
|
955
|
+
scheduleCronById(row.id, row.cron_expression, 'lead_agent_run', payload);
|
|
956
|
+
}
|
|
957
|
+
res.json(getScheduledJob(row.id));
|
|
958
|
+
}
|
|
959
|
+
catch (e) {
|
|
960
|
+
sendGatewayError(res, e);
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
// News & Social Agent setup and schedule
|
|
964
|
+
const NEWS_SOCIAL_AGENT_SETUP_KEY = 'news_social_agent_setup';
|
|
965
|
+
const NEWS_SOCIAL_AGENT_SCHEDULE_ID = 'news-social-agent-daily';
|
|
966
|
+
const DEFAULT_NEWS_SOCIAL_CRON = '0 9 * * *';
|
|
967
|
+
app.get('/api/settings/news-social-agent-setup', (_req, res) => {
|
|
968
|
+
try {
|
|
969
|
+
const raw = getChannelConfig(NEWS_SOCIAL_AGENT_SETUP_KEY);
|
|
970
|
+
if (!raw?.trim()) {
|
|
971
|
+
res.json({
|
|
972
|
+
newsTopics: '', numStories: 5, confirmBeforePost: false, socialSkills: ['bluesky'], customPrompt: '',
|
|
973
|
+
contentSourceType: 'web_search',
|
|
974
|
+
});
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
const o = JSON.parse(raw);
|
|
978
|
+
const rawSkills = o.socialSkills;
|
|
979
|
+
const socialSkills = Array.isArray(rawSkills)
|
|
980
|
+
? rawSkills.map((s) => String(s).toLowerCase().trim()).filter(Boolean)
|
|
981
|
+
: ['bluesky'];
|
|
982
|
+
if (socialSkills.length === 0)
|
|
983
|
+
socialSkills.push('bluesky');
|
|
984
|
+
const contentSourceType = ['web_search', 'google_doc', 'google_sheet', 'file'].includes(String(o.contentSourceType)) ? o.contentSourceType : 'web_search';
|
|
985
|
+
res.json({
|
|
986
|
+
newsTopics: typeof o.newsTopics === 'string' ? o.newsTopics : '',
|
|
987
|
+
numStories: typeof o.numStories === 'number' && o.numStories >= 1 ? Math.min(20, Math.round(o.numStories)) : 5,
|
|
988
|
+
confirmBeforePost: o.confirmBeforePost === true,
|
|
989
|
+
socialSkills,
|
|
990
|
+
customPrompt: typeof o.customPrompt === 'string' ? o.customPrompt.trim() : '',
|
|
991
|
+
contentSourceType,
|
|
992
|
+
contentSourceId: typeof o.contentSourceId === 'string' ? o.contentSourceId.trim() || undefined : undefined,
|
|
993
|
+
contentSourceSheetRange: typeof o.contentSourceSheetRange === 'string' ? o.contentSourceSheetRange.trim() || undefined : undefined,
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
catch (e) {
|
|
997
|
+
sendGatewayError(res, e);
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
app.put('/api/settings/news-social-agent-setup', (req, res) => {
|
|
1001
|
+
try {
|
|
1002
|
+
const body = (req.body || {});
|
|
1003
|
+
const newsTopics = typeof body.newsTopics === 'string' ? body.newsTopics.trim() : '';
|
|
1004
|
+
const numStories = typeof body.numStories === 'number' && body.numStories >= 1 ? Math.min(20, Math.round(body.numStories)) : 5;
|
|
1005
|
+
const confirmBeforePost = body.confirmBeforePost === true;
|
|
1006
|
+
const rawSkills = body.socialSkills;
|
|
1007
|
+
const socialSkills = Array.isArray(rawSkills)
|
|
1008
|
+
? rawSkills.map((s) => String(s).toLowerCase().trim()).filter(Boolean)
|
|
1009
|
+
: ['bluesky'];
|
|
1010
|
+
const finalSkills = socialSkills.length > 0 ? socialSkills : ['bluesky'];
|
|
1011
|
+
const customPrompt = typeof body.customPrompt === 'string' ? body.customPrompt.trim() : '';
|
|
1012
|
+
const contentSourceType = ['web_search', 'google_doc', 'google_sheet', 'file'].includes(String(body.contentSourceType)) ? body.contentSourceType : 'web_search';
|
|
1013
|
+
const contentSourceId = typeof body.contentSourceId === 'string' ? body.contentSourceId.trim() || undefined : undefined;
|
|
1014
|
+
const contentSourceSheetRange = typeof body.contentSourceSheetRange === 'string' ? body.contentSourceSheetRange.trim() || undefined : undefined;
|
|
1015
|
+
setChannelConfig(NEWS_SOCIAL_AGENT_SETUP_KEY, JSON.stringify({
|
|
1016
|
+
newsTopics, numStories, confirmBeforePost, socialSkills: finalSkills, customPrompt,
|
|
1017
|
+
contentSourceType, contentSourceId, contentSourceSheetRange,
|
|
1018
|
+
}));
|
|
1019
|
+
res.json({ ok: true });
|
|
1020
|
+
}
|
|
1021
|
+
catch (e) {
|
|
1022
|
+
sendGatewayError(res, e);
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
1025
|
+
app.delete('/api/settings/news-social-agent-setup', (_req, res) => {
|
|
1026
|
+
try {
|
|
1027
|
+
setChannelConfig(NEWS_SOCIAL_AGENT_SETUP_KEY, '');
|
|
1028
|
+
res.json({ ok: true });
|
|
1029
|
+
}
|
|
1030
|
+
catch (e) {
|
|
1031
|
+
sendGatewayError(res, e);
|
|
1032
|
+
}
|
|
1033
|
+
});
|
|
1034
|
+
app.get('/api/settings/news-social-agent-schedule', (_req, res) => {
|
|
1035
|
+
try {
|
|
1036
|
+
const row = getScheduledJob(NEWS_SOCIAL_AGENT_SCHEDULE_ID)
|
|
1037
|
+
?? listScheduledJobs(false).find((j) => j.task_type === 'news_social_agent_run');
|
|
1038
|
+
if (!row) {
|
|
1039
|
+
res.status(404).json({ error: 'News & Social Agent schedule not found' });
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
res.json(row);
|
|
1043
|
+
}
|
|
1044
|
+
catch (e) {
|
|
1045
|
+
sendGatewayError(res, e);
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
app.put('/api/settings/news-social-agent-schedule', (req, res) => {
|
|
1049
|
+
try {
|
|
1050
|
+
const body = (req.body || {});
|
|
1051
|
+
const cronExpression = typeof body.cron_expression === 'string' && body.cron_expression.trim()
|
|
1052
|
+
? body.cron_expression.trim()
|
|
1053
|
+
: DEFAULT_NEWS_SOCIAL_CRON;
|
|
1054
|
+
const enabled = body.enabled !== false ? 1 : 0;
|
|
1055
|
+
const agent = getAgentBySlug('news-social-agent');
|
|
1056
|
+
if (!agent) {
|
|
1057
|
+
res.status(400).json({ error: 'News & Social Agent not found' });
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
let row = getScheduledJob(NEWS_SOCIAL_AGENT_SCHEDULE_ID)
|
|
1061
|
+
?? listScheduledJobs(false).find((j) => j.task_type === 'news_social_agent_run');
|
|
1062
|
+
if (!row) {
|
|
1063
|
+
const payloadStr = JSON.stringify({
|
|
1064
|
+
agentId: agent.id,
|
|
1065
|
+
provider: agent.default_provider || 'openai',
|
|
1066
|
+
model: agent.default_model || 'gpt-4o-mini',
|
|
1067
|
+
});
|
|
1068
|
+
insertScheduledJob({
|
|
1069
|
+
id: NEWS_SOCIAL_AGENT_SCHEDULE_ID,
|
|
1070
|
+
name: 'News & Social Agent Daily Run',
|
|
1071
|
+
description: 'Find hot news, post to social media, report to Telegram',
|
|
1072
|
+
cron_expression: cronExpression,
|
|
1073
|
+
task_type: 'news_social_agent_run',
|
|
1074
|
+
payload: payloadStr,
|
|
1075
|
+
enabled,
|
|
1076
|
+
});
|
|
1077
|
+
row = getScheduledJob(NEWS_SOCIAL_AGENT_SCHEDULE_ID);
|
|
1078
|
+
}
|
|
1079
|
+
else {
|
|
1080
|
+
updateScheduledJob(row.id, { cron_expression: cronExpression, enabled });
|
|
1081
|
+
unscheduleJob(row.id);
|
|
1082
|
+
row = getScheduledJob(row.id);
|
|
1083
|
+
}
|
|
1084
|
+
if (row.enabled) {
|
|
1085
|
+
const payload = row.payload ? JSON.parse(row.payload) : { agentId: agent.id };
|
|
1086
|
+
scheduleCronById(row.id, row.cron_expression, 'news_social_agent_run', payload);
|
|
1087
|
+
}
|
|
1088
|
+
res.json(getScheduledJob(row.id));
|
|
1089
|
+
}
|
|
1090
|
+
catch (e) {
|
|
1091
|
+
sendGatewayError(res, e);
|
|
1092
|
+
}
|
|
1093
|
+
});
|
|
1094
|
+
// News Agent setup (personalized topics and story count)
|
|
1095
|
+
const NEWS_AGENT_SETUP_KEY = 'news_agent_setup';
|
|
1096
|
+
app.get('/api/settings/news-agent-setup', (_req, res) => {
|
|
1097
|
+
try {
|
|
1098
|
+
const raw = getChannelConfig(NEWS_AGENT_SETUP_KEY);
|
|
1099
|
+
if (!raw?.trim()) {
|
|
1100
|
+
res.json({ newsTopics: '', numStories: 10, customPrompt: '' });
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
const o = JSON.parse(raw);
|
|
1104
|
+
const numStories = typeof o.numStories === 'number' && o.numStories >= 1
|
|
1105
|
+
? Math.min(20, Math.round(o.numStories))
|
|
1106
|
+
: 10;
|
|
1107
|
+
res.json({
|
|
1108
|
+
newsTopics: typeof o.newsTopics === 'string' ? o.newsTopics.trim() : '',
|
|
1109
|
+
numStories,
|
|
1110
|
+
customPrompt: typeof o.customPrompt === 'string' ? o.customPrompt.trim() : '',
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
catch (e) {
|
|
1114
|
+
sendGatewayError(res, e);
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
app.put('/api/settings/news-agent-setup', (req, res) => {
|
|
1118
|
+
try {
|
|
1119
|
+
const body = (req.body || {});
|
|
1120
|
+
const newsTopics = typeof body.newsTopics === 'string' ? body.newsTopics.trim() : '';
|
|
1121
|
+
const numStories = typeof body.numStories === 'number' && body.numStories >= 1
|
|
1122
|
+
? Math.min(20, Math.round(body.numStories))
|
|
1123
|
+
: 10;
|
|
1124
|
+
const customPrompt = typeof body.customPrompt === 'string' ? body.customPrompt.trim() : '';
|
|
1125
|
+
setChannelConfig(NEWS_AGENT_SETUP_KEY, JSON.stringify({ newsTopics, numStories, customPrompt }));
|
|
1126
|
+
res.json({ ok: true });
|
|
1127
|
+
}
|
|
1128
|
+
catch (e) {
|
|
1129
|
+
sendGatewayError(res, e);
|
|
1130
|
+
}
|
|
1131
|
+
});
|
|
1132
|
+
app.delete('/api/settings/news-agent-setup', (_req, res) => {
|
|
1133
|
+
try {
|
|
1134
|
+
setChannelConfig(NEWS_AGENT_SETUP_KEY, '');
|
|
1135
|
+
res.json({ ok: true });
|
|
1136
|
+
}
|
|
1137
|
+
catch (e) {
|
|
1138
|
+
sendGatewayError(res, e);
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
// Social Agent setup (reads from Doc/Sheet, posts to Bluesky/X)
|
|
1142
|
+
const SOCIAL_AGENT_SETUP_KEY = 'social_agent_setup';
|
|
1143
|
+
app.get('/api/settings/social-agent-setup', (_req, res) => {
|
|
1144
|
+
try {
|
|
1145
|
+
const raw = getChannelConfig(SOCIAL_AGENT_SETUP_KEY);
|
|
1146
|
+
if (!raw?.trim()) {
|
|
1147
|
+
res.json({
|
|
1148
|
+
contentSourceType: 'workflow_folder',
|
|
1149
|
+
contentSourceId: undefined,
|
|
1150
|
+
contentSourceDocName: undefined,
|
|
1151
|
+
contentSourceSheetRange: undefined,
|
|
1152
|
+
socialSkills: ['bluesky'],
|
|
1153
|
+
confirmBeforePost: false,
|
|
1154
|
+
customPrompt: '',
|
|
1155
|
+
});
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
const o = JSON.parse(raw);
|
|
1159
|
+
const rawSkills = o.socialSkills;
|
|
1160
|
+
const socialSkills = Array.isArray(rawSkills)
|
|
1161
|
+
? rawSkills.map((s) => String(s).toLowerCase().trim()).filter(Boolean)
|
|
1162
|
+
: ['bluesky'];
|
|
1163
|
+
if (socialSkills.length === 0)
|
|
1164
|
+
socialSkills.push('bluesky');
|
|
1165
|
+
res.json({
|
|
1166
|
+
contentSourceType: ['workflow_folder', 'google_doc', 'google_sheet', 'file'].includes(String(o.contentSourceType)) ? o.contentSourceType : 'workflow_folder',
|
|
1167
|
+
contentSourceId: typeof o.contentSourceId === 'string' ? o.contentSourceId.trim() || undefined : undefined,
|
|
1168
|
+
contentSourceDocName: typeof o.contentSourceDocName === 'string' ? o.contentSourceDocName.trim() || undefined : undefined,
|
|
1169
|
+
contentSourceSheetRange: typeof o.contentSourceSheetRange === 'string' ? o.contentSourceSheetRange.trim() || undefined : undefined,
|
|
1170
|
+
socialSkills,
|
|
1171
|
+
confirmBeforePost: o.confirmBeforePost === true,
|
|
1172
|
+
customPrompt: typeof o.customPrompt === 'string' ? o.customPrompt.trim() : '',
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
catch (e) {
|
|
1176
|
+
sendGatewayError(res, e);
|
|
1177
|
+
}
|
|
1178
|
+
});
|
|
1179
|
+
app.put('/api/settings/social-agent-setup', (req, res) => {
|
|
1180
|
+
try {
|
|
1181
|
+
const body = (req.body || {});
|
|
1182
|
+
const contentSourceType = ['workflow_folder', 'google_doc', 'google_sheet', 'file'].includes(String(body.contentSourceType)) ? body.contentSourceType : 'workflow_folder';
|
|
1183
|
+
const contentSourceId = typeof body.contentSourceId === 'string' ? body.contentSourceId.trim() || undefined : undefined;
|
|
1184
|
+
const contentSourceDocName = typeof body.contentSourceDocName === 'string' ? body.contentSourceDocName.trim() || undefined : undefined;
|
|
1185
|
+
const contentSourceSheetRange = typeof body.contentSourceSheetRange === 'string' ? body.contentSourceSheetRange.trim() || undefined : undefined;
|
|
1186
|
+
const rawSkills = body.socialSkills;
|
|
1187
|
+
const socialSkills = Array.isArray(rawSkills)
|
|
1188
|
+
? rawSkills.map((s) => String(s).toLowerCase().trim()).filter(Boolean)
|
|
1189
|
+
: ['bluesky'];
|
|
1190
|
+
const finalSkills = socialSkills.length > 0 ? socialSkills : ['bluesky'];
|
|
1191
|
+
const customPrompt = typeof body.customPrompt === 'string' ? body.customPrompt.trim() : '';
|
|
1192
|
+
setChannelConfig(SOCIAL_AGENT_SETUP_KEY, JSON.stringify({
|
|
1193
|
+
contentSourceType,
|
|
1194
|
+
contentSourceId,
|
|
1195
|
+
contentSourceDocName,
|
|
1196
|
+
contentSourceSheetRange,
|
|
1197
|
+
socialSkills: finalSkills,
|
|
1198
|
+
confirmBeforePost: body.confirmBeforePost === true,
|
|
1199
|
+
customPrompt,
|
|
1200
|
+
}));
|
|
1201
|
+
res.json({ ok: true });
|
|
1202
|
+
}
|
|
1203
|
+
catch (e) {
|
|
1204
|
+
sendGatewayError(res, e);
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
app.delete('/api/settings/social-agent-setup', (_req, res) => {
|
|
1208
|
+
try {
|
|
1209
|
+
setChannelConfig(SOCIAL_AGENT_SETUP_KEY, '');
|
|
1210
|
+
res.json({ ok: true });
|
|
1211
|
+
}
|
|
1212
|
+
catch (e) {
|
|
1213
|
+
sendGatewayError(res, e);
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
// Content Writer Agent setup (reads from source, writes to destination)
|
|
1217
|
+
const CONTENT_WRITER_AGENT_SETUP_KEY = 'content_writer_agent_setup';
|
|
1218
|
+
app.get('/api/settings/content-writer-agent-setup', (_req, res) => {
|
|
1219
|
+
try {
|
|
1220
|
+
const raw = getChannelConfig(CONTENT_WRITER_AGENT_SETUP_KEY);
|
|
1221
|
+
if (!raw?.trim()) {
|
|
1222
|
+
res.json({
|
|
1223
|
+
contentSourceType: 'workflow_folder',
|
|
1224
|
+
contentSourceId: undefined,
|
|
1225
|
+
contentSourceDocName: undefined,
|
|
1226
|
+
contentSourceSheetRange: undefined,
|
|
1227
|
+
outputDestinationType: 'workflow_folder',
|
|
1228
|
+
outputDestinationId: undefined,
|
|
1229
|
+
outputDestinationDocName: undefined,
|
|
1230
|
+
outputDestinationSheetRange: undefined,
|
|
1231
|
+
outputStyle: 'mixed',
|
|
1232
|
+
customPrompt: '',
|
|
1233
|
+
});
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
const o = JSON.parse(raw);
|
|
1237
|
+
res.json({
|
|
1238
|
+
contentSourceType: ['google_doc', 'google_sheet', 'file'].includes(String(o.contentSourceType)) ? o.contentSourceType : 'google_doc',
|
|
1239
|
+
contentSourceId: typeof o.contentSourceId === 'string' ? o.contentSourceId.trim() || undefined : undefined,
|
|
1240
|
+
contentSourceDocName: typeof o.contentSourceDocName === 'string' ? o.contentSourceDocName.trim() || undefined : undefined,
|
|
1241
|
+
contentSourceSheetRange: typeof o.contentSourceSheetRange === 'string' ? o.contentSourceSheetRange.trim() || undefined : undefined,
|
|
1242
|
+
outputDestinationType: ['workflow_folder', 'google_doc', 'google_sheet', 'notion_page', 'notion_database'].includes(String(o.outputDestinationType)) ? o.outputDestinationType : 'workflow_folder',
|
|
1243
|
+
outputDestinationId: typeof o.outputDestinationId === 'string' ? o.outputDestinationId.trim() || undefined : undefined,
|
|
1244
|
+
outputDestinationDocName: typeof o.outputDestinationDocName === 'string' ? o.outputDestinationDocName.trim() || undefined : undefined,
|
|
1245
|
+
outputDestinationSheetRange: typeof o.outputDestinationSheetRange === 'string' ? o.outputDestinationSheetRange.trim() || undefined : undefined,
|
|
1246
|
+
outputStyle: ['article', 'summary', 'social_posts', 'mixed'].includes(String(o.outputStyle)) ? o.outputStyle : 'mixed',
|
|
1247
|
+
customPrompt: typeof o.customPrompt === 'string' ? o.customPrompt.trim() : '',
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
catch (e) {
|
|
1251
|
+
sendGatewayError(res, e);
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
app.put('/api/settings/content-writer-agent-setup', (req, res) => {
|
|
1255
|
+
try {
|
|
1256
|
+
const body = (req.body || {});
|
|
1257
|
+
const contentSourceType = ['workflow_folder', 'google_doc', 'google_sheet', 'file'].includes(String(body.contentSourceType)) ? body.contentSourceType : 'workflow_folder';
|
|
1258
|
+
const contentSourceId = typeof body.contentSourceId === 'string' ? body.contentSourceId.trim() || undefined : undefined;
|
|
1259
|
+
const contentSourceDocName = typeof body.contentSourceDocName === 'string' ? body.contentSourceDocName.trim() || undefined : undefined;
|
|
1260
|
+
const contentSourceSheetRange = typeof body.contentSourceSheetRange === 'string' ? body.contentSourceSheetRange.trim() || undefined : undefined;
|
|
1261
|
+
const outputDestinationType = ['workflow_folder', 'google_doc', 'google_sheet', 'notion_page', 'notion_database'].includes(String(body.outputDestinationType)) ? body.outputDestinationType : 'workflow_folder';
|
|
1262
|
+
const outputDestinationId = typeof body.outputDestinationId === 'string' ? body.outputDestinationId.trim() || undefined : undefined;
|
|
1263
|
+
const outputDestinationDocName = typeof body.outputDestinationDocName === 'string' ? body.outputDestinationDocName.trim() || undefined : undefined;
|
|
1264
|
+
const outputDestinationSheetRange = typeof body.outputDestinationSheetRange === 'string' ? body.outputDestinationSheetRange.trim() || undefined : undefined;
|
|
1265
|
+
const outputStyle = ['article', 'summary', 'social_posts', 'mixed'].includes(String(body.outputStyle)) ? body.outputStyle : 'mixed';
|
|
1266
|
+
const customPrompt = typeof body.customPrompt === 'string' ? body.customPrompt.trim() : '';
|
|
1267
|
+
setChannelConfig(CONTENT_WRITER_AGENT_SETUP_KEY, JSON.stringify({
|
|
1268
|
+
contentSourceType,
|
|
1269
|
+
contentSourceId,
|
|
1270
|
+
contentSourceDocName,
|
|
1271
|
+
contentSourceSheetRange,
|
|
1272
|
+
outputDestinationType,
|
|
1273
|
+
outputDestinationId,
|
|
1274
|
+
outputDestinationDocName,
|
|
1275
|
+
outputDestinationSheetRange,
|
|
1276
|
+
outputStyle,
|
|
1277
|
+
customPrompt,
|
|
1278
|
+
}));
|
|
1279
|
+
res.json({ ok: true });
|
|
1280
|
+
}
|
|
1281
|
+
catch (e) {
|
|
1282
|
+
sendGatewayError(res, e);
|
|
1283
|
+
}
|
|
1284
|
+
});
|
|
1285
|
+
app.delete('/api/settings/content-writer-agent-setup', (_req, res) => {
|
|
1286
|
+
try {
|
|
1287
|
+
setChannelConfig(CONTENT_WRITER_AGENT_SETUP_KEY, '');
|
|
1288
|
+
res.json({ ok: true });
|
|
1289
|
+
}
|
|
1290
|
+
catch (e) {
|
|
1291
|
+
sendGatewayError(res, e);
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
// Workflow agent setup and schedule (Email Digest, Calendar Briefing, etc.)
|
|
1295
|
+
app.get('/api/settings/workflow-agent/:slug/setup', (req, res) => {
|
|
1296
|
+
try {
|
|
1297
|
+
const slug = Array.isArray(req.params.slug) ? req.params.slug[0] : req.params.slug;
|
|
1298
|
+
if (!slug || !isWorkflowAgent(slug)) {
|
|
1299
|
+
res.status(400).json({ error: 'Invalid workflow agent slug' });
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
const key = `${slug.replace(/-/g, '_')}_setup`;
|
|
1303
|
+
const raw = getChannelConfig(key);
|
|
1304
|
+
if (!raw?.trim()) {
|
|
1305
|
+
res.json({ customPrompt: '' });
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
const o = JSON.parse(raw);
|
|
1309
|
+
res.json({ customPrompt: typeof o.customPrompt === 'string' ? o.customPrompt.trim() : '' });
|
|
1310
|
+
}
|
|
1311
|
+
catch (e) {
|
|
1312
|
+
sendGatewayError(res, e);
|
|
1313
|
+
}
|
|
1314
|
+
});
|
|
1315
|
+
app.put('/api/settings/workflow-agent/:slug/setup', (req, res) => {
|
|
1316
|
+
try {
|
|
1317
|
+
const slug = Array.isArray(req.params.slug) ? req.params.slug[0] : req.params.slug;
|
|
1318
|
+
if (!slug || !isWorkflowAgent(slug)) {
|
|
1319
|
+
res.status(400).json({ error: 'Invalid workflow agent slug' });
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
const body = (req.body || {});
|
|
1323
|
+
const customPrompt = typeof body.customPrompt === 'string' ? body.customPrompt.trim() : '';
|
|
1324
|
+
const key = `${slug.replace(/-/g, '_')}_setup`;
|
|
1325
|
+
setChannelConfig(key, JSON.stringify({ customPrompt }));
|
|
1326
|
+
res.json({ ok: true });
|
|
1327
|
+
}
|
|
1328
|
+
catch (e) {
|
|
1329
|
+
sendGatewayError(res, e);
|
|
1330
|
+
}
|
|
1331
|
+
});
|
|
1332
|
+
app.get('/api/settings/workflow-agent/:slug/schedule', (req, res) => {
|
|
1333
|
+
try {
|
|
1334
|
+
const slug = Array.isArray(req.params.slug) ? req.params.slug[0] : req.params.slug;
|
|
1335
|
+
if (!slug || !isWorkflowAgent(slug)) {
|
|
1336
|
+
res.status(400).json({ error: 'Invalid workflow agent slug' });
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
const cfg = WORKFLOW_AGENT_CONFIG.find((c) => c.slug === slug);
|
|
1340
|
+
if (!cfg) {
|
|
1341
|
+
res.status(404).json({ error: 'Schedule config not found' });
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
const row = getScheduledJob(cfg.scheduleId) ?? listScheduledJobs(false).find((j) => j.task_type === cfg.taskType);
|
|
1345
|
+
if (!row) {
|
|
1346
|
+
res.status(404).json({ error: 'Schedule not found' });
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
res.json(row);
|
|
1350
|
+
}
|
|
1351
|
+
catch (e) {
|
|
1352
|
+
sendGatewayError(res, e);
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
app.put('/api/settings/workflow-agent/:slug/schedule', (req, res) => {
|
|
1356
|
+
try {
|
|
1357
|
+
const slug = Array.isArray(req.params.slug) ? req.params.slug[0] : req.params.slug;
|
|
1358
|
+
if (!slug || !isWorkflowAgent(slug)) {
|
|
1359
|
+
res.status(400).json({ error: 'Invalid workflow agent slug' });
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
const cfg = WORKFLOW_AGENT_CONFIG.find((c) => c.slug === slug);
|
|
1363
|
+
if (!cfg) {
|
|
1364
|
+
res.status(404).json({ error: 'Schedule config not found' });
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
const body = (req.body || {});
|
|
1368
|
+
const cronExpression = typeof body.cron_expression === 'string' && body.cron_expression.trim()
|
|
1369
|
+
? body.cron_expression.trim()
|
|
1370
|
+
: cfg.defaultCron;
|
|
1371
|
+
const enabled = body.enabled !== false ? 1 : 0;
|
|
1372
|
+
const agent = getAgentBySlug(slug);
|
|
1373
|
+
if (!agent) {
|
|
1374
|
+
res.status(400).json({ error: 'Agent not found' });
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
let row = getScheduledJob(cfg.scheduleId) ?? listScheduledJobs(false).find((j) => j.task_type === cfg.taskType);
|
|
1378
|
+
if (!row) {
|
|
1379
|
+
insertScheduledJob({
|
|
1380
|
+
id: cfg.scheduleId,
|
|
1381
|
+
name: cfg.name,
|
|
1382
|
+
description: cfg.description,
|
|
1383
|
+
cron_expression: cronExpression,
|
|
1384
|
+
task_type: cfg.taskType,
|
|
1385
|
+
payload: JSON.stringify({
|
|
1386
|
+
agentId: agent.id,
|
|
1387
|
+
provider: agent.default_provider || 'openai',
|
|
1388
|
+
model: agent.default_model || 'gpt-4o-mini',
|
|
1389
|
+
}),
|
|
1390
|
+
enabled,
|
|
1391
|
+
});
|
|
1392
|
+
row = getScheduledJob(cfg.scheduleId);
|
|
1393
|
+
}
|
|
1394
|
+
else {
|
|
1395
|
+
updateScheduledJob(row.id, { cron_expression: cronExpression, enabled });
|
|
1396
|
+
unscheduleJob(row.id);
|
|
1397
|
+
row = getScheduledJob(row.id);
|
|
1398
|
+
}
|
|
1399
|
+
if (row.enabled) {
|
|
1400
|
+
const payload = row.payload ? JSON.parse(row.payload) : { agentId: agent.id };
|
|
1401
|
+
scheduleCronById(row.id, row.cron_expression, cfg.taskType, payload);
|
|
1402
|
+
}
|
|
1403
|
+
res.json(getScheduledJob(row.id));
|
|
1404
|
+
}
|
|
1405
|
+
catch (e) {
|
|
1406
|
+
sendGatewayError(res, e);
|
|
1407
|
+
}
|
|
1408
|
+
});
|
|
778
1409
|
app.get('/api/agent/skills/required-bins', (_req, res) => {
|
|
779
1410
|
try {
|
|
780
1411
|
const bins = getAllRequiredBins(config);
|
|
@@ -1361,6 +1992,23 @@ export function createGateway(appMount = null) {
|
|
|
1361
1992
|
sendGatewayError(res, e);
|
|
1362
1993
|
}
|
|
1363
1994
|
});
|
|
1995
|
+
/** GET /api/agent/social-skills — available social posting skills (tools ending in _post, from installed skills). */
|
|
1996
|
+
app.get('/api/agent/social-skills', (_req, res) => {
|
|
1997
|
+
try {
|
|
1998
|
+
const tools = listTools();
|
|
1999
|
+
const social = tools
|
|
2000
|
+
.filter((t) => t.name.endsWith('_post'))
|
|
2001
|
+
.map((t) => {
|
|
2002
|
+
const slug = t.name.replace(/_post$/, '');
|
|
2003
|
+
const name = slug.charAt(0).toUpperCase() + slug.slice(1);
|
|
2004
|
+
return { slug, name, toolName: t.name };
|
|
2005
|
+
});
|
|
2006
|
+
res.json({ skills: social });
|
|
2007
|
+
}
|
|
2008
|
+
catch (e) {
|
|
2009
|
+
sendGatewayError(res, e);
|
|
2010
|
+
}
|
|
2011
|
+
});
|
|
1364
2012
|
/** GET /api/agent/memory/scope-keys — distinct scope_key per scope (for Settings > Memory dropdowns). */
|
|
1365
2013
|
app.get('/api/agent/memory/scope-keys', (_req, res) => {
|
|
1366
2014
|
try {
|
|
@@ -1715,6 +2363,236 @@ export function createGateway(appMount = null) {
|
|
|
1715
2363
|
sendGatewayError(res, e);
|
|
1716
2364
|
}
|
|
1717
2365
|
});
|
|
2366
|
+
/** Cancel an in-flight agent run (e.g. lead_agent_run). Sends abort signal to the worker. */
|
|
2367
|
+
app.post('/api/tasks/:id/cancel', (req, res) => {
|
|
2368
|
+
try {
|
|
2369
|
+
const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
|
|
2370
|
+
if (!id) {
|
|
2371
|
+
res.status(400).json({ error: 'Task id required' });
|
|
2372
|
+
return;
|
|
2373
|
+
}
|
|
2374
|
+
const cancelled = requestCancel(id);
|
|
2375
|
+
res.json({ id, cancelled });
|
|
2376
|
+
}
|
|
2377
|
+
catch (e) {
|
|
2378
|
+
sendGatewayError(res, e);
|
|
2379
|
+
}
|
|
2380
|
+
});
|
|
2381
|
+
// Workflows (multi-agent pipelines)
|
|
2382
|
+
app.get('/api/workflows', (_req, res) => {
|
|
2383
|
+
try {
|
|
2384
|
+
const workflows = listWorkflows();
|
|
2385
|
+
res.json({ workflows });
|
|
2386
|
+
}
|
|
2387
|
+
catch (e) {
|
|
2388
|
+
sendGatewayError(res, e);
|
|
2389
|
+
}
|
|
2390
|
+
});
|
|
2391
|
+
app.post('/api/workflows', (req, res) => {
|
|
2392
|
+
try {
|
|
2393
|
+
const { name, description, steps, cron_expression, enabled, approve_before_social_post } = req.body || {};
|
|
2394
|
+
if (!cron_expression?.trim()) {
|
|
2395
|
+
res.status(400).json({ error: 'cron_expression required' });
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
const stepsArr = Array.isArray(steps) ? steps : [];
|
|
2399
|
+
if (stepsArr.length === 0) {
|
|
2400
|
+
res.status(400).json({ error: 'steps array required and must not be empty' });
|
|
2401
|
+
return;
|
|
2402
|
+
}
|
|
2403
|
+
const validSteps = stepsArr
|
|
2404
|
+
.filter((s) => s != null && typeof s === 'object' && typeof s.agentSlug === 'string')
|
|
2405
|
+
.map((s, i) => ({
|
|
2406
|
+
order: typeof s.order === 'number' ? s.order : i,
|
|
2407
|
+
agentSlug: s.agentSlug.trim(),
|
|
2408
|
+
}));
|
|
2409
|
+
if (validSteps.length === 0) {
|
|
2410
|
+
res.status(400).json({ error: 'steps must contain objects with agentSlug' });
|
|
2411
|
+
return;
|
|
2412
|
+
}
|
|
2413
|
+
const agents = listAgents();
|
|
2414
|
+
const validSlugs = new Set(agents.map((a) => a.slug));
|
|
2415
|
+
const invalid = validSteps.find((s) => !validSlugs.has(s.agentSlug));
|
|
2416
|
+
if (invalid) {
|
|
2417
|
+
res.status(400).json({ error: `Unknown agent slug: ${invalid.agentSlug}` });
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
2420
|
+
const workflowId = `wf_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
2421
|
+
const stepsJson = JSON.stringify(validSteps);
|
|
2422
|
+
const enabledVal = enabled === false ? 0 : 1;
|
|
2423
|
+
const approveBeforeSocialPost = approve_before_social_post === false ? 0 : 1;
|
|
2424
|
+
insertWorkflow({
|
|
2425
|
+
id: workflowId,
|
|
2426
|
+
name: (name && typeof name === 'string' ? name.trim() : '') || `Workflow ${workflowId.slice(3, 15)}`,
|
|
2427
|
+
description: (description && typeof description === 'string' ? description.trim() : '') || '',
|
|
2428
|
+
steps: stepsJson,
|
|
2429
|
+
cron_expression: cron_expression.trim(),
|
|
2430
|
+
enabled: enabledVal,
|
|
2431
|
+
approve_before_social_post: approveBeforeSocialPost,
|
|
2432
|
+
});
|
|
2433
|
+
const scheduleId = getScheduledJobIdForWorkflow(workflowId);
|
|
2434
|
+
insertScheduledJob({
|
|
2435
|
+
id: scheduleId,
|
|
2436
|
+
name: (name && typeof name === 'string' ? name.trim() : '') || `Workflow ${workflowId.slice(3, 15)}`,
|
|
2437
|
+
description: (description && typeof description === 'string' ? description.trim() : '') || '',
|
|
2438
|
+
cron_expression: cron_expression.trim(),
|
|
2439
|
+
task_type: 'workflow_run',
|
|
2440
|
+
payload: JSON.stringify({ workflowId }),
|
|
2441
|
+
enabled: enabledVal,
|
|
2442
|
+
});
|
|
2443
|
+
if (enabledVal) {
|
|
2444
|
+
scheduleCronById(scheduleId, cron_expression.trim(), 'workflow_run', { workflowId });
|
|
2445
|
+
}
|
|
2446
|
+
const workflow = getWorkflow(workflowId);
|
|
2447
|
+
ensureWorkflowFolder(workflowId, workflow.name);
|
|
2448
|
+
log('gateway', 'info', 'Workflow created', { workflowId });
|
|
2449
|
+
res.status(201).json(workflow);
|
|
2450
|
+
}
|
|
2451
|
+
catch (e) {
|
|
2452
|
+
sendGatewayError(res, e);
|
|
2453
|
+
}
|
|
2454
|
+
});
|
|
2455
|
+
app.get('/api/workflows/:id', (req, res) => {
|
|
2456
|
+
try {
|
|
2457
|
+
const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
|
|
2458
|
+
if (!id) {
|
|
2459
|
+
res.status(400).json({ error: 'Workflow id required' });
|
|
2460
|
+
return;
|
|
2461
|
+
}
|
|
2462
|
+
const workflow = getWorkflow(id);
|
|
2463
|
+
if (!workflow) {
|
|
2464
|
+
res.status(404).json({ error: 'Workflow not found' });
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
res.json(workflow);
|
|
2468
|
+
}
|
|
2469
|
+
catch (e) {
|
|
2470
|
+
sendGatewayError(res, e);
|
|
2471
|
+
}
|
|
2472
|
+
});
|
|
2473
|
+
app.patch('/api/workflows/:id', (req, res) => {
|
|
2474
|
+
try {
|
|
2475
|
+
const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
|
|
2476
|
+
if (!id) {
|
|
2477
|
+
res.status(400).json({ error: 'Workflow id required' });
|
|
2478
|
+
return;
|
|
2479
|
+
}
|
|
2480
|
+
const workflow = getWorkflow(id);
|
|
2481
|
+
if (!workflow) {
|
|
2482
|
+
res.status(404).json({ error: 'Workflow not found' });
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2485
|
+
const { name, description, steps, cron_expression, enabled, approve_before_social_post } = req.body || {};
|
|
2486
|
+
const updates = {};
|
|
2487
|
+
if (typeof name === 'string')
|
|
2488
|
+
updates.name = name.trim();
|
|
2489
|
+
if (typeof description === 'string')
|
|
2490
|
+
updates.description = description.trim();
|
|
2491
|
+
if (typeof cron_expression === 'string')
|
|
2492
|
+
updates.cron_expression = cron_expression.trim();
|
|
2493
|
+
if (enabled !== undefined)
|
|
2494
|
+
updates.enabled = enabled ? 1 : 0;
|
|
2495
|
+
if (approve_before_social_post !== undefined)
|
|
2496
|
+
updates.approve_before_social_post = approve_before_social_post ? 1 : 0;
|
|
2497
|
+
const stepsArr = steps !== undefined ? (Array.isArray(steps) ? steps : []) : null;
|
|
2498
|
+
if (stepsArr !== null && stepsArr.length === 0) {
|
|
2499
|
+
res.status(400).json({ error: 'steps array must not be empty' });
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
if (stepsArr !== null && stepsArr.length > 0) {
|
|
2503
|
+
const agents = listAgents();
|
|
2504
|
+
const validSlugs = new Set(agents.map((a) => a.slug));
|
|
2505
|
+
const validSteps = stepsArr
|
|
2506
|
+
.filter((s) => s != null && typeof s === 'object' && typeof s.agentSlug === 'string')
|
|
2507
|
+
.map((s, i) => ({
|
|
2508
|
+
order: typeof s.order === 'number' ? s.order : i,
|
|
2509
|
+
agentSlug: s.agentSlug.trim(),
|
|
2510
|
+
}));
|
|
2511
|
+
const invalid = validSteps.find((s) => !validSlugs.has(s.agentSlug));
|
|
2512
|
+
if (invalid) {
|
|
2513
|
+
res.status(400).json({ error: `Unknown agent slug: ${invalid.agentSlug}` });
|
|
2514
|
+
return;
|
|
2515
|
+
}
|
|
2516
|
+
updates.steps = JSON.stringify(validSteps);
|
|
2517
|
+
}
|
|
2518
|
+
if (Object.keys(updates).length > 0) {
|
|
2519
|
+
updateWorkflow(id, updates);
|
|
2520
|
+
}
|
|
2521
|
+
const updated = getWorkflow(id);
|
|
2522
|
+
ensureWorkflowFolder(id, updated.name);
|
|
2523
|
+
const scheduleId = getScheduledJobIdForWorkflow(id);
|
|
2524
|
+
const job = getScheduledJob(scheduleId);
|
|
2525
|
+
if (job) {
|
|
2526
|
+
updateScheduledJob(scheduleId, {
|
|
2527
|
+
name: updated.name,
|
|
2528
|
+
description: updated.description,
|
|
2529
|
+
cron_expression: updated.cron_expression,
|
|
2530
|
+
payload: JSON.stringify({ workflowId: id }),
|
|
2531
|
+
enabled: updated.enabled,
|
|
2532
|
+
});
|
|
2533
|
+
unscheduleJob(scheduleId);
|
|
2534
|
+
if (updated.enabled) {
|
|
2535
|
+
scheduleCronById(scheduleId, updated.cron_expression, 'workflow_run', { workflowId: id });
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
res.json(updated);
|
|
2539
|
+
}
|
|
2540
|
+
catch (e) {
|
|
2541
|
+
sendGatewayError(res, e);
|
|
2542
|
+
}
|
|
2543
|
+
});
|
|
2544
|
+
app.delete('/api/workflows/:id', (req, res) => {
|
|
2545
|
+
try {
|
|
2546
|
+
const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
|
|
2547
|
+
if (!id) {
|
|
2548
|
+
res.status(400).json({ error: 'Workflow id required' });
|
|
2549
|
+
return;
|
|
2550
|
+
}
|
|
2551
|
+
const workflow = getWorkflow(id);
|
|
2552
|
+
if (!workflow) {
|
|
2553
|
+
res.status(404).json({ error: 'Workflow not found' });
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
const scheduleId = getScheduledJobIdForWorkflow(id);
|
|
2557
|
+
unscheduleJob(scheduleId);
|
|
2558
|
+
deleteScheduledJob(scheduleId);
|
|
2559
|
+
deleteWorkflow(id);
|
|
2560
|
+
log('gateway', 'info', 'Workflow deleted', { id });
|
|
2561
|
+
res.status(204).send();
|
|
2562
|
+
}
|
|
2563
|
+
catch (e) {
|
|
2564
|
+
sendGatewayError(res, e);
|
|
2565
|
+
}
|
|
2566
|
+
});
|
|
2567
|
+
app.post('/api/workflows/:id/run', (req, res) => {
|
|
2568
|
+
try {
|
|
2569
|
+
const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
|
|
2570
|
+
if (!id) {
|
|
2571
|
+
res.status(400).json({ error: 'Workflow id required' });
|
|
2572
|
+
return;
|
|
2573
|
+
}
|
|
2574
|
+
const workflow = getWorkflow(id);
|
|
2575
|
+
if (!workflow) {
|
|
2576
|
+
res.status(404).json({ error: 'Workflow not found' });
|
|
2577
|
+
return;
|
|
2578
|
+
}
|
|
2579
|
+
const taskId = `workflow_run_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
2580
|
+
insertTask({
|
|
2581
|
+
id: taskId,
|
|
2582
|
+
type: 'workflow_run',
|
|
2583
|
+
payload: { workflowId: id },
|
|
2584
|
+
scheduled_at: Date.now(),
|
|
2585
|
+
});
|
|
2586
|
+
const enqueueTaskId = app.locals.enqueueTaskId;
|
|
2587
|
+
if (typeof enqueueTaskId === 'function')
|
|
2588
|
+
enqueueTaskId(taskId);
|
|
2589
|
+
log('gateway', 'info', 'Workflow run enqueued', { workflowId: id, taskId });
|
|
2590
|
+
res.status(201).json({ id: taskId, type: 'workflow_run', status: 'pending' });
|
|
2591
|
+
}
|
|
2592
|
+
catch (e) {
|
|
2593
|
+
sendGatewayError(res, e);
|
|
2594
|
+
}
|
|
2595
|
+
});
|
|
1718
2596
|
// Scheduled jobs (cron → task type)
|
|
1719
2597
|
app.get('/api/schedules', (_req, res) => {
|
|
1720
2598
|
try {
|
|
@@ -1934,6 +2812,131 @@ export function createGateway(appMount = null) {
|
|
|
1934
2812
|
}
|
|
1935
2813
|
});
|
|
1936
2814
|
/** Chat attachments: upload image/video, get a URL the agent can use (e.g. Facebook photo posts, YouTube video upload). */
|
|
2815
|
+
// --- Agents (Agent OS v1) ---
|
|
2816
|
+
app.get('/api/agents', (_req, res) => {
|
|
2817
|
+
try {
|
|
2818
|
+
const agents = listAgents(false); // all agents, even disabled
|
|
2819
|
+
const result = agents.map((a) => ({
|
|
2820
|
+
id: a.id,
|
|
2821
|
+
slug: a.slug,
|
|
2822
|
+
name: a.name,
|
|
2823
|
+
description: a.description,
|
|
2824
|
+
persona_prompt: a.persona_prompt,
|
|
2825
|
+
default_provider: a.default_provider,
|
|
2826
|
+
default_model: a.default_model,
|
|
2827
|
+
workspace_root: a.workspace_root,
|
|
2828
|
+
enabled: a.enabled === 1,
|
|
2829
|
+
created_at: a.created_at,
|
|
2830
|
+
updated_at: a.updated_at,
|
|
2831
|
+
}));
|
|
2832
|
+
res.json({ agents: result });
|
|
2833
|
+
}
|
|
2834
|
+
catch (e) {
|
|
2835
|
+
sendGatewayError(res, e);
|
|
2836
|
+
}
|
|
2837
|
+
});
|
|
2838
|
+
app.get('/api/agents/:id', (req, res) => {
|
|
2839
|
+
try {
|
|
2840
|
+
const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
|
|
2841
|
+
if (!id) {
|
|
2842
|
+
res.status(400).json({ error: 'Agent id required' });
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
const agent = getAgentById(id);
|
|
2846
|
+
if (!agent) {
|
|
2847
|
+
res.status(404).json({ error: 'Agent not found' });
|
|
2848
|
+
return;
|
|
2849
|
+
}
|
|
2850
|
+
res.json({
|
|
2851
|
+
id: agent.id,
|
|
2852
|
+
slug: agent.slug,
|
|
2853
|
+
name: agent.name,
|
|
2854
|
+
description: agent.description,
|
|
2855
|
+
persona_prompt: agent.persona_prompt,
|
|
2856
|
+
default_provider: agent.default_provider,
|
|
2857
|
+
default_model: agent.default_model,
|
|
2858
|
+
default_tools: agent.default_tools ? JSON.parse(agent.default_tools) : null,
|
|
2859
|
+
default_skills: agent.default_skills ? JSON.parse(agent.default_skills) : null,
|
|
2860
|
+
workspace_root: agent.workspace_root,
|
|
2861
|
+
enabled: agent.enabled === 1,
|
|
2862
|
+
created_at: agent.created_at,
|
|
2863
|
+
updated_at: agent.updated_at,
|
|
2864
|
+
});
|
|
2865
|
+
}
|
|
2866
|
+
catch (e) {
|
|
2867
|
+
sendGatewayError(res, e);
|
|
2868
|
+
}
|
|
2869
|
+
});
|
|
2870
|
+
app.get('/api/agents/:id/metrics', (req, res) => {
|
|
2871
|
+
try {
|
|
2872
|
+
const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
|
|
2873
|
+
if (!id) {
|
|
2874
|
+
res.status(400).json({ error: 'Agent id required' });
|
|
2875
|
+
return;
|
|
2876
|
+
}
|
|
2877
|
+
const agent = getAgentById(id);
|
|
2878
|
+
if (!agent) {
|
|
2879
|
+
res.status(404).json({ error: 'Agent not found' });
|
|
2880
|
+
return;
|
|
2881
|
+
}
|
|
2882
|
+
const metrics = getLatestAgentMetrics(id);
|
|
2883
|
+
res.json({ metrics });
|
|
2884
|
+
}
|
|
2885
|
+
catch (e) {
|
|
2886
|
+
sendGatewayError(res, e);
|
|
2887
|
+
}
|
|
2888
|
+
});
|
|
2889
|
+
/** Run an agent immediately (manual trigger). Body: { sessionId?, userMessage?, provider?, model? } */
|
|
2890
|
+
app.post('/api/agents/:id/run-once', async (req, res) => {
|
|
2891
|
+
try {
|
|
2892
|
+
const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
|
|
2893
|
+
if (!id) {
|
|
2894
|
+
res.status(400).json({ error: 'Agent id required' });
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2897
|
+
const agent = getAgentById(id);
|
|
2898
|
+
if (!agent) {
|
|
2899
|
+
res.status(404).json({ error: 'Agent not found' });
|
|
2900
|
+
return;
|
|
2901
|
+
}
|
|
2902
|
+
const { sessionId: optionalSessionId, userMessage, provider: reqProvider, model: reqModel } = req.body || {};
|
|
2903
|
+
const sessionId = optionalSessionId || `agent_run_${agent.id}_${Date.now()}`;
|
|
2904
|
+
const session = getOrCreateAgentSession(sessionId, { agent_id: agent.id });
|
|
2905
|
+
const provider = typeof reqProvider === 'string' ? reqProvider : agent.default_provider || undefined;
|
|
2906
|
+
const model = typeof reqModel === 'string' ? reqModel : agent.default_model || undefined;
|
|
2907
|
+
const userMsg = typeof userMessage === 'string' ? userMessage : `Run ${agent.name} workflow.`;
|
|
2908
|
+
// Enqueue a task to run the agent
|
|
2909
|
+
const taskId = `task_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
2910
|
+
const taskType = agent.slug === 'lead-agent' ? 'lead_agent_run' :
|
|
2911
|
+
agent.slug === 'news-social-agent' ? 'news_social_agent_run' :
|
|
2912
|
+
agent.slug === 'social-agent' ? 'social_agent_run' :
|
|
2913
|
+
agent.slug === 'content-writer-agent' ? 'content_writer_agent_run' :
|
|
2914
|
+
isWorkflowAgent(agent.slug) ? SLUG_TO_TASK_TYPE[agent.slug] :
|
|
2915
|
+
'agent_run';
|
|
2916
|
+
if (taskType === 'lead_agent_run' || taskType === 'news_social_agent_run' || taskType === 'social_agent_run' || taskType === 'content_writer_agent_run') {
|
|
2917
|
+
registerCancelController(taskId);
|
|
2918
|
+
}
|
|
2919
|
+
insertTask({
|
|
2920
|
+
id: taskId,
|
|
2921
|
+
type: taskType,
|
|
2922
|
+
payload: {
|
|
2923
|
+
agentId: agent.id,
|
|
2924
|
+
sessionId: session.id,
|
|
2925
|
+
userMessage: userMsg,
|
|
2926
|
+
provider,
|
|
2927
|
+
model,
|
|
2928
|
+
},
|
|
2929
|
+
});
|
|
2930
|
+
const enqueueTaskIdFn = app.locals.enqueueTaskId;
|
|
2931
|
+
if (typeof enqueueTaskIdFn === 'function')
|
|
2932
|
+
enqueueTaskIdFn(taskId);
|
|
2933
|
+
log('gateway', 'info', `Agent run enqueued: ${agent.name}`, { agentId: agent.id, sessionId: session.id });
|
|
2934
|
+
res.status(201).json({ taskId, sessionId: session.id, status: 'queued' });
|
|
2935
|
+
}
|
|
2936
|
+
catch (e) {
|
|
2937
|
+
sendGatewayError(res, e);
|
|
2938
|
+
}
|
|
2939
|
+
});
|
|
1937
2940
|
const uploadsDir = join(projectRoot, 'uploads');
|
|
1938
2941
|
app.post('/api/upload', express.json({ limit: UPLOAD_EXPRESS_LIMIT }), (req, res) => {
|
|
1939
2942
|
try {
|
|
@@ -2010,6 +3013,10 @@ export function createGateway(appMount = null) {
|
|
|
2010
3013
|
if (p && !dirs.includes(p))
|
|
2011
3014
|
dirs.push(p);
|
|
2012
3015
|
}
|
|
3016
|
+
// Allow any path under user home (Downloads, Desktop, .sulala/workspace/uploads, etc.) so file_path uploads work when agent finds files via run_command
|
|
3017
|
+
const home = homedir();
|
|
3018
|
+
if (home && !dirs.includes(resolve(home)))
|
|
3019
|
+
dirs.push(resolve(home));
|
|
2013
3020
|
return dirs;
|
|
2014
3021
|
};
|
|
2015
3022
|
const isPathAllowed = (filePath) => {
|
|
@@ -2052,7 +3059,7 @@ export function createGateway(appMount = null) {
|
|
|
2052
3059
|
return { buf: readFileSync(filePath), contentType };
|
|
2053
3060
|
}
|
|
2054
3061
|
if (filePath)
|
|
2055
|
-
throw new Error('file_path must be under uploads, workspace,
|
|
3062
|
+
throw new Error('file_path must be under uploads, workspace, a watch folder, or the user home directory');
|
|
2056
3063
|
if (!fileUrl)
|
|
2057
3064
|
throw new Error('Body must include file_url or file_path');
|
|
2058
3065
|
try {
|
|
@@ -2079,6 +3086,7 @@ export function createGateway(appMount = null) {
|
|
|
2079
3086
|
try {
|
|
2080
3087
|
const destination = typeof req.body?.destination === 'string' ? req.body.destination.trim().toLowerCase() : '';
|
|
2081
3088
|
const connectionId = typeof req.body?.connection_id === 'string' ? req.body.connection_id.trim() : '';
|
|
3089
|
+
const accessTokenFromBody = typeof req.body?.access_token === 'string' ? req.body.access_token.trim() : '';
|
|
2082
3090
|
const fileUrl = (typeof req.body?.file_url === 'string' ? req.body.file_url.trim() : '') ||
|
|
2083
3091
|
(typeof req.body?.video_url === 'string' ? req.body.video_url.trim() : '');
|
|
2084
3092
|
const filePath = (typeof req.body?.file_path === 'string' ? req.body.file_path.trim() : '') ||
|
|
@@ -2089,8 +3097,12 @@ export function createGateway(appMount = null) {
|
|
|
2089
3097
|
const privacyStatus = ['public', 'private', 'unlisted'].includes(req.body?.privacyStatus || metadata.privacyStatus || '')
|
|
2090
3098
|
? (req.body?.privacyStatus || metadata.privacyStatus || 'private')
|
|
2091
3099
|
: 'private';
|
|
2092
|
-
if (!destination
|
|
2093
|
-
res.status(400).json({ error: 'Body must include destination
|
|
3100
|
+
if (!destination) {
|
|
3101
|
+
res.status(400).json({ error: 'Body must include destination' });
|
|
3102
|
+
return;
|
|
3103
|
+
}
|
|
3104
|
+
if (!connectionId && !accessTokenFromBody) {
|
|
3105
|
+
res.status(400).json({ error: 'Body must include connection_id (Portal) or access_token (e.g. from google-oauth)' });
|
|
2094
3106
|
return;
|
|
2095
3107
|
}
|
|
2096
3108
|
if (!filePath && !fileUrl) {
|
|
@@ -2102,12 +3114,18 @@ export function createGateway(appMount = null) {
|
|
|
2102
3114
|
res.status(400).json({ error: `File too large (max ${UPLOAD_MAX_BYTES / (1024 * 1024)} MB)` });
|
|
2103
3115
|
return;
|
|
2104
3116
|
}
|
|
2105
|
-
|
|
2106
|
-
if (
|
|
2107
|
-
|
|
2108
|
-
|
|
3117
|
+
let accessToken;
|
|
3118
|
+
if (accessTokenFromBody) {
|
|
3119
|
+
accessToken = accessTokenFromBody;
|
|
3120
|
+
}
|
|
3121
|
+
else {
|
|
3122
|
+
const tokenResult = await getConnectionToken(connectionId);
|
|
3123
|
+
if (!tokenResult.ok) {
|
|
3124
|
+
res.status(tokenResult.status).json({ error: tokenResult.error });
|
|
3125
|
+
return;
|
|
3126
|
+
}
|
|
3127
|
+
accessToken = tokenResult.accessToken;
|
|
2109
3128
|
}
|
|
2110
|
-
const accessToken = tokenResult.accessToken;
|
|
2111
3129
|
const handler = getUploadHandler(destination);
|
|
2112
3130
|
if (!handler) {
|
|
2113
3131
|
const available = listUploadDestinations();
|
|
@@ -2118,11 +3136,22 @@ export function createGateway(appMount = null) {
|
|
|
2118
3136
|
return;
|
|
2119
3137
|
}
|
|
2120
3138
|
const ctx = {
|
|
2121
|
-
connectionId,
|
|
3139
|
+
connectionId: connectionId || '',
|
|
2122
3140
|
accessToken,
|
|
2123
3141
|
buf,
|
|
2124
3142
|
contentType,
|
|
2125
|
-
body: {
|
|
3143
|
+
body: {
|
|
3144
|
+
title,
|
|
3145
|
+
description,
|
|
3146
|
+
privacyStatus,
|
|
3147
|
+
...metadata,
|
|
3148
|
+
file_url: fileUrl || undefined,
|
|
3149
|
+
file_path: typeof req.body?.file_path === 'string' ? req.body.file_path.trim() || undefined : undefined,
|
|
3150
|
+
target_path: typeof req.body?.target_path === 'string' ? req.body.target_path.trim() || undefined : undefined,
|
|
3151
|
+
mode: typeof req.body?.mode === 'string' ? req.body.mode.trim() : undefined,
|
|
3152
|
+
folder_id: typeof req.body?.folder_id === 'string' ? req.body.folder_id.trim() || undefined : undefined,
|
|
3153
|
+
name: typeof req.body?.name === 'string' ? req.body.name.trim() || undefined : undefined,
|
|
3154
|
+
},
|
|
2126
3155
|
};
|
|
2127
3156
|
const result = await handler(ctx);
|
|
2128
3157
|
if (result.ok) {
|
|
@@ -2183,7 +3212,20 @@ a{color:#2563eb;}</style></head>
|
|
|
2183
3212
|
return app;
|
|
2184
3213
|
}
|
|
2185
3214
|
export function attachWebSocket(server, onConnection = () => { }) {
|
|
2186
|
-
const
|
|
3215
|
+
const verifyClient = config.gatewayApiKey?.trim() ?
|
|
3216
|
+
(info, callback) => {
|
|
3217
|
+
const url = info.req?.url || '';
|
|
3218
|
+
const params = new URLSearchParams(url.includes('?') ? url.split('?')[1] : '');
|
|
3219
|
+
const key = params.get('api_key') || '';
|
|
3220
|
+
if (key === config.gatewayApiKey) {
|
|
3221
|
+
callback(true);
|
|
3222
|
+
}
|
|
3223
|
+
else {
|
|
3224
|
+
callback(false, 401, 'Invalid or missing API key');
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
: undefined;
|
|
3228
|
+
const wss = new WebSocketServer({ server, path: '/ws', verifyClient });
|
|
2187
3229
|
const broadcast = (data) => {
|
|
2188
3230
|
const msg = typeof data === 'string' ? data : JSON.stringify(data);
|
|
2189
3231
|
wss.clients.forEach((c) => {
|
|
@@ -2215,11 +3257,13 @@ export function startGateway() {
|
|
|
2215
3257
|
app.locals.wsBroadcast = broadcast;
|
|
2216
3258
|
server.listen(config.port, config.host, () => {
|
|
2217
3259
|
console.log(`Sulala gateway http://${config.host}:${config.port} (WS /ws)`);
|
|
3260
|
+
startTailscaleServe(config.port);
|
|
2218
3261
|
});
|
|
2219
3262
|
return { app, server };
|
|
2220
3263
|
}
|
|
2221
3264
|
if (process.argv[1]?.includes('server')) {
|
|
2222
3265
|
(async () => {
|
|
3266
|
+
await ensureProtectedBindOrExit();
|
|
2223
3267
|
await initDb(config.dbPath);
|
|
2224
3268
|
startGateway();
|
|
2225
3269
|
})().catch((err) => {
|