added nextcloud
This commit is contained in:
+14
-242
@@ -8,242 +8,11 @@ import cron from 'node-cron'
|
||||
import loginUser, { getUser } from './helpers/resolve-user'
|
||||
import { authCall, callNewChat, ollamaInp, schedInp } from './helpers/ollamaCalls'
|
||||
|
||||
const DEFAULT_TZ = "America/New_York"
|
||||
|
||||
const openApiSpec = {
|
||||
openapi: '3.0.1',
|
||||
info: {
|
||||
title: 'Scheduler API',
|
||||
description: 'API for managing scheduled prompts. Cron expressions run in America/New_York (EST).',
|
||||
version: '1.0.0'
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: '/',
|
||||
description: 'Current server'
|
||||
}
|
||||
],
|
||||
paths: {
|
||||
'/api/schedules': {
|
||||
get: {
|
||||
summary: 'List schedules',
|
||||
security: [{ BearerAuth: [] }],
|
||||
responses: {
|
||||
200: {
|
||||
description: 'Successful response',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
ok: { type: 'boolean', example: true },
|
||||
items: {
|
||||
type: 'array',
|
||||
items: { $ref: '#/components/schemas/Schedule' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
401: { $ref: '#/components/responses/Unauthorized' }
|
||||
}
|
||||
},
|
||||
post: {
|
||||
summary: 'Create or replace a schedule',
|
||||
security: [{ BearerAuth: [] }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/ScheduleInput' }
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
201: {
|
||||
description: 'Schedule created or updated',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
ok: { type: 'boolean', example: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
400: { $ref: '#/components/responses/BadRequest' },
|
||||
401: { $ref: '#/components/responses/Unauthorized' }
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/schedules/{name}': {
|
||||
delete: {
|
||||
summary: 'Delete a schedule',
|
||||
security: [{ BearerAuth: [] }],
|
||||
parameters: [
|
||||
{
|
||||
name: 'name',
|
||||
in: 'path',
|
||||
description: 'Schedule name (raw or scoped)',
|
||||
required: true,
|
||||
schema: { type: 'string' }
|
||||
}
|
||||
],
|
||||
responses: {
|
||||
204: { description: 'Schedule deleted' },
|
||||
401: { $ref: '#/components/responses/Unauthorized' },
|
||||
403: { $ref: '#/components/responses/Forbidden' },
|
||||
404: { $ref: '#/components/responses/NotFound' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
securitySchemes: {
|
||||
BearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT'
|
||||
}
|
||||
},
|
||||
schemas: {
|
||||
Schedule: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'Scoped identifier used internally' },
|
||||
displayName: { type: 'string' },
|
||||
userId: { type: 'string' },
|
||||
schedules: {
|
||||
type: 'array',
|
||||
items: { type: 'string', description: 'Cron expression' }
|
||||
},
|
||||
startAt: { type: 'string', format: 'date-time', nullable: true },
|
||||
oneShot: { type: 'boolean' },
|
||||
templateRef: { $ref: '#/components/schemas/TemplateRef' },
|
||||
prompt: { type: 'string' },
|
||||
model: { type: 'string' },
|
||||
tools: { type: 'array', items: { type: 'string' } },
|
||||
features: {
|
||||
type: 'object',
|
||||
additionalProperties: { type: 'boolean' }
|
||||
},
|
||||
files: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
fname: { type: 'string' },
|
||||
fkey: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['name', 'userId', 'schedules', 'oneShot']
|
||||
},
|
||||
ScheduleInput: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
when: { $ref: '#/components/schemas/ScheduleWhen' },
|
||||
oneShot: { type: 'boolean', default: false },
|
||||
template: { $ref: '#/components/schemas/TemplateInput' },
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: true
|
||||
},
|
||||
files: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
fname: { type: 'string' },
|
||||
fkey: {
|
||||
type: 'string',
|
||||
description: 'Optional previously stored file reference'
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Base64 encoded file contents'
|
||||
}
|
||||
},
|
||||
required: ['fname']
|
||||
}
|
||||
},
|
||||
prompt: { type: 'string' },
|
||||
model: { type: 'string' },
|
||||
tools: { type: 'array', items: { type: 'string' } },
|
||||
features: {
|
||||
type: 'object',
|
||||
additionalProperties: { type: 'boolean' }
|
||||
}
|
||||
},
|
||||
required: ['name', 'when', 'template', 'parameters', 'prompt', 'model', 'tools']
|
||||
},
|
||||
ScheduleWhen: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
cron: {
|
||||
type: 'string',
|
||||
description: '5-field cron expression evaluated in America/New_York'
|
||||
},
|
||||
start: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
description: 'Optional start gate; required when oneShot is true'
|
||||
}
|
||||
},
|
||||
required: []
|
||||
},
|
||||
TemplateInput: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
clusterScope: { type: 'boolean', default: false }
|
||||
},
|
||||
required: ['name']
|
||||
},
|
||||
TemplateRef: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
clusterScope: { type: 'boolean' }
|
||||
},
|
||||
required: ['name']
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
Unauthorized: {
|
||||
description: 'Missing or invalid credentials'
|
||||
},
|
||||
BadRequest: {
|
||||
description: 'Invalid request payload',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
ok: { type: 'boolean', example: false },
|
||||
error: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Forbidden: {
|
||||
description: 'Schedule exists but belongs to another user'
|
||||
},
|
||||
NotFound: {
|
||||
description: 'Schedule not found'
|
||||
}
|
||||
}
|
||||
}
|
||||
} as const;
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url),
|
||||
// KEEP AS STRING
|
||||
const openApiSpec = fs.readFileSync('./openapi.json', 'utf-8'),
|
||||
__filename = fileURLToPath(import.meta.url),
|
||||
__dirname = path.dirname(__filename),
|
||||
DEFAULT_TZ = "America/New_York",
|
||||
|
||||
// folders/files
|
||||
DATA_DIR = process.env.DATA_DIR || path.join(__dirname, 'data'),
|
||||
@@ -254,8 +23,8 @@ const __filename = fileURLToPath(import.meta.url),
|
||||
|
||||
let FILES_DIR = FILES_DIR_PREFERRED;
|
||||
|
||||
// defaults
|
||||
PORT = Number(process.env.PORT) || 12253
|
||||
// defaults
|
||||
const PORT = Number(process.env.PORT) || 12253
|
||||
|
||||
// connect to docker (via local socket by default)
|
||||
// const docker = new Docker({ socketPath: process.env.DOCKER_SOCKET || '/var/run/docker.sock' })
|
||||
@@ -294,7 +63,6 @@ const normalizeFeatures = (value: unknown): Record<string, boolean> => {
|
||||
type IncomingFile = { fname?: string, fkey?: string, content?: string };
|
||||
|
||||
const sanitizeBase64 = (raw?: string) => typeof raw === 'string' ? raw.replace(/^data:[^;]+;base64,/, '') : '';
|
||||
|
||||
const prepareIncomingFiles = (value: unknown): { fname: string, fkey: string }[] | string => {
|
||||
if (!value) return [];
|
||||
if (!Array.isArray(value)) return 'files must be an array';
|
||||
@@ -302,20 +70,23 @@ const prepareIncomingFiles = (value: unknown): { fname: string, fkey: string }[]
|
||||
const saved: { fname: string, fkey: string }[] = [];
|
||||
for (const entry of value as IncomingFile[]) {
|
||||
if (!entry || typeof entry !== 'object') return 'invalid file entry';
|
||||
|
||||
const fname = typeof entry.fname === 'string' ? entry.fname.trim() : '';
|
||||
if (!fname) return 'file entries require fname';
|
||||
|
||||
if (entry.content && typeof entry.content === 'string') {
|
||||
const b64 = sanitizeBase64(entry.content);
|
||||
if (!b64) return `file ${fname} is missing content`;
|
||||
const fkey = crypto.randomUUID();
|
||||
const target = path.join(FILES_DIR, fkey);
|
||||
|
||||
const fkey = crypto.randomUUID(),
|
||||
target = path.join(FILES_DIR, fkey);
|
||||
try {
|
||||
fs.writeFileSync(target, Buffer.from(b64, 'base64'));
|
||||
} catch (err) {
|
||||
console.error('failed to store file', fname, err);
|
||||
return `failed to store file ${fname}`;
|
||||
}
|
||||
|
||||
saved.push({ fname, fkey });
|
||||
continue;
|
||||
}
|
||||
@@ -382,7 +153,7 @@ function scheduleOrReplace(defInput: ollamaInp) {
|
||||
const key = def.name
|
||||
if (tasks.has(key)) { try { tasks.get(key).task.stop() } catch { } tasks.delete(key) }
|
||||
|
||||
const startTs = def.startAt ? new Date(def.startAt).getTime() : null;
|
||||
const startTs = def.startAt ? new Date(def.startAt).getTime() : null;
|
||||
|
||||
// forbid overlapping runs per schedule (like argo's Forbid)
|
||||
let running = false
|
||||
@@ -505,7 +276,7 @@ const server = http.createServer(async (req, res) => {
|
||||
allowed = ['/', ...fs.readdirSync(publicDir).map(f => `/${f}`)];
|
||||
|
||||
if (req.method === 'GET' && pathname === '/openapi.json') {
|
||||
return res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(openApiSpec, null, 2));
|
||||
return res.writeHead(200, { 'content-type': 'application/json' }).end(openApiSpec);
|
||||
}
|
||||
|
||||
// list schedules for the calling user
|
||||
@@ -591,6 +362,7 @@ const server = http.createServer(async (req, res) => {
|
||||
|
||||
const userId = await fetchUserID(req),
|
||||
nameParam = decodeURIComponent(paramRaw),
|
||||
|
||||
// stored names are already scoped; accept either raw or scoped
|
||||
key = tasks.has(nameParam) ? nameParam : scopedName(nameParam, userId),
|
||||
existing = tasks.get(key)?.def;
|
||||
|
||||
Reference in New Issue
Block a user