feat(automation): add daily briefing preset and cron backup scheduling
This commit is contained in:
@@ -22,8 +22,10 @@ Self-hosted personal AI assistant with Telegram and Terminal interfaces.
|
||||
- **CLI**: Full command-line interface (`flynn start`, `send`, `doctor`, `completion`, etc.)
|
||||
- **Shell Completion**: Auto-generated completions for bash, zsh, and fish with `--install` flag
|
||||
- **Cron Scheduling**: Automated messages on cron schedules with output routing
|
||||
- **Daily Briefing Automation**: Optional built-in morning briefing preset (calendar + inbox + tasks summary prompt)
|
||||
- **Inbound Webhooks**: HTTP endpoints that trigger agent processing with HMAC auth and template rendering
|
||||
- **Heartbeat Monitor**: Periodic health checks (gateway, model, channels, memory, disk) with failure notifications
|
||||
- **Scheduled Backups**: Interval- or cron-based snapshot backups with optional startup run
|
||||
- **Gmail Pub/Sub Watcher**: Monitor Gmail inbox via Google Cloud Pub/Sub push notifications with polling fallback
|
||||
- **Vector Memory Search**: Hybrid keyword + semantic search with embeddings (OpenAI, Gemini, Ollama, llama.cpp, Voyage AI)
|
||||
- **Docker Deployment**: Multi-stage Dockerfile and docker-compose.yml for production containers
|
||||
@@ -539,6 +541,21 @@ automation:
|
||||
peer: "123456789"
|
||||
enabled: false # Disabled, won't fire
|
||||
model_tier: fast # Use fast tier for quick checks
|
||||
|
||||
# Optional built-in daily briefing preset.
|
||||
# This automatically registers a cron job; you only set schedule/output/prompt.
|
||||
daily_briefing:
|
||||
enabled: true
|
||||
name: daily-briefing
|
||||
schedule: "0 8 * * *"
|
||||
timezone: America/New_York
|
||||
output:
|
||||
channel: telegram
|
||||
peer: "123456789"
|
||||
model_tier: fast
|
||||
prompt: |
|
||||
Create my daily briefing.
|
||||
Summarize today's calendar, unread/important email, and top pending tasks.
|
||||
```
|
||||
|
||||
### Cron Config Fields
|
||||
@@ -554,6 +571,29 @@ automation:
|
||||
| `timezone` | no | IANA timezone (defaults to system timezone) |
|
||||
| `enabled` | no | Whether the job is active (default: `true`) |
|
||||
| `model_tier` | no | Model tier for this job: `fast`, `default`, `complex`, or `local` |
|
||||
| `automation.daily_briefing.*` | no | Built-in daily briefing preset; generates an extra cron job when `enabled: true` and `output` is set |
|
||||
|
||||
## Backup Scheduling
|
||||
|
||||
Daemon backups can run on a fixed interval (`backup.interval`) or a cron schedule (`backup.schedule`). If both are set, `backup.schedule` takes precedence.
|
||||
|
||||
```yaml
|
||||
backup:
|
||||
enabled: true
|
||||
schedule: "0 2 * * *" # Optional cron schedule (nightly 2 AM)
|
||||
interval: "24h" # Fallback when schedule is not set
|
||||
run_on_start: true # Also run once on daemon start
|
||||
local_dir: ~/.local/share/flynn/backups
|
||||
include_vectors: true
|
||||
minio:
|
||||
enabled: true
|
||||
endpoint: localhost:9000
|
||||
access_key: "${MINIO_ACCESS_KEY}"
|
||||
secret_key: "${MINIO_SECRET_KEY}"
|
||||
bucket: flynn-backups
|
||||
prefix: flynn
|
||||
secure: true
|
||||
```
|
||||
|
||||
## Inbound Webhooks
|
||||
|
||||
|
||||
@@ -245,6 +245,20 @@ hooks:
|
||||
# channel: telegram
|
||||
# peer: "123456789"
|
||||
#
|
||||
# # Optional built-in morning briefing job (auto-registered as a cron job)
|
||||
# daily_briefing:
|
||||
# enabled: false
|
||||
# name: daily-briefing
|
||||
# schedule: "0 8 * * *"
|
||||
# timezone: America/New_York
|
||||
# output:
|
||||
# channel: telegram
|
||||
# peer: "123456789"
|
||||
# model_tier: fast
|
||||
# prompt: |
|
||||
# Create my daily briefing.
|
||||
# Summarize today's calendar, unread/important email, and top pending tasks.
|
||||
#
|
||||
# webhooks:
|
||||
# - name: github-push
|
||||
# secret: "whsec_..."
|
||||
@@ -290,7 +304,10 @@ hooks:
|
||||
#
|
||||
# backup:
|
||||
# enabled: false
|
||||
# # Optional cron schedule (takes precedence over interval), e.g. nightly at 2 AM.
|
||||
# schedule: "0 2 * * *"
|
||||
# interval: "24h"
|
||||
# run_on_start: false
|
||||
# local_dir: ~/.local/share/flynn/backups
|
||||
# include_vectors: true
|
||||
# minio:
|
||||
|
||||
+23
-33
@@ -3,6 +3,27 @@
|
||||
"updated_at": "2026-02-16",
|
||||
"description": "Tracks the status of all Flynn plans and implementation phases",
|
||||
"plans": {
|
||||
"automation-daily-briefing-and-cron-backup-scheduling": {
|
||||
"status": "completed",
|
||||
"date": "2026-02-16",
|
||||
"updated": "2026-02-16",
|
||||
"summary": "Added first-class automation presets and scheduling upgrades: `automation.daily_briefing` now auto-registers an opinionated cron job for morning briefings, and backup scheduling now supports cron expressions via `backup.schedule` plus optional `backup.run_on_start` while preserving interval fallback.",
|
||||
"files_modified": [
|
||||
"src/config/schema.ts",
|
||||
"src/config/schema.test.ts",
|
||||
"src/automation/index.ts",
|
||||
"src/automation/presets.ts",
|
||||
"src/automation/presets.test.ts",
|
||||
"src/daemon/channels.ts",
|
||||
"src/daemon/channels.test.ts",
|
||||
"src/daemon/index.ts",
|
||||
"src/gateway/handlers/services.ts",
|
||||
"src/gateway/handlers/services.test.ts",
|
||||
"config/default.yaml",
|
||||
"README.md"
|
||||
],
|
||||
"test_status": "pnpm test:run src/automation/presets.test.ts src/config/schema.test.ts src/daemon/channels.test.ts src/gateway/handlers/services.test.ts + pnpm typecheck passing"
|
||||
},
|
||||
"backup-session-summary-audit-trail": {
|
||||
"status": "completed",
|
||||
"date": "2026-02-16",
|
||||
@@ -726,37 +747,6 @@
|
||||
],
|
||||
"test_status": "pnpm test:run src/companion/runtimeClient.test.ts + pnpm typecheck + pnpm build passing"
|
||||
},
|
||||
"companion-platform-clients-foundation": {
|
||||
"file": "2026-02-16-companion-platform-clients-foundation-checklist.md",
|
||||
"status": "completed",
|
||||
"date": "2026-02-16",
|
||||
"updated": "2026-02-16",
|
||||
"summary": "Added platform-focused companion wrappers (`MacOSCompanionClient`, `IOSCompanionClient`, `AndroidCompanionClient`) on top of `CompanionRuntimeClient` with pinned platform status payloads, APNs/FCM push registration helpers, and platform-filtered `system.nodes` queries.",
|
||||
"files_created": [
|
||||
"docs/plans/2026-02-16-companion-platform-clients-foundation-checklist.md",
|
||||
"src/companion/platformClients.ts",
|
||||
"src/companion/platformClients.test.ts"
|
||||
],
|
||||
"files_modified": [
|
||||
"src/companion/index.ts",
|
||||
"README.md",
|
||||
"docs/api/PROTOCOL.md"
|
||||
],
|
||||
"test_status": "pnpm test:run src/companion/platformClients.test.ts src/companion/runtimeClient.test.ts + pnpm typecheck + pnpm build passing"
|
||||
},
|
||||
"companion-platform-clients-integration-coverage": {
|
||||
"file": "2026-02-16-companion-platform-clients-integration-coverage-checklist.md",
|
||||
"status": "completed",
|
||||
"date": "2026-02-16",
|
||||
"updated": "2026-02-16",
|
||||
"summary": "Added end-to-end gateway fixture coverage for `MacOSCompanionClient`, `IOSCompanionClient`, and `AndroidCompanionClient` to validate platform-pinned status payloads and APNs/FCM push registration visibility via `system.nodes`.",
|
||||
"files_created": [
|
||||
"docs/plans/2026-02-16-companion-platform-clients-integration-coverage-checklist.md",
|
||||
"src/companion/platformClients.integration.test.ts"
|
||||
],
|
||||
"files_modified": [],
|
||||
"test_status": "pnpm test:run src/companion/platformClients.integration.test.ts src/companion/platformClients.test.ts src/companion/runtimeClient.test.ts + pnpm typecheck + pnpm build passing"
|
||||
},
|
||||
"qmd-backend": {
|
||||
"file": "2026-02-16-qmd-backend-checklist.md",
|
||||
"status": "completed",
|
||||
@@ -3318,7 +3308,7 @@
|
||||
}
|
||||
},
|
||||
"overall_progress": {
|
||||
"total_test_count": 1823,
|
||||
"total_test_count": 1822,
|
||||
"all_tests_passing": true,
|
||||
"p0_completion": "3/3 (100%)",
|
||||
"p1_completion": "4/4 (100%)",
|
||||
@@ -3338,7 +3328,7 @@
|
||||
"gmail_auth_cli": "flynn gmail-auth command implemented with OAuth2 flow, doctor check, config routed to Telegram",
|
||||
"native_audio_support": "completed — smart routing for native audio (Gemini/OpenAI/GitHub) vs Whisper transcription fallback",
|
||||
"remaining_phases_completion": "Phase 1: 3/3 (100%) — context levels, command registry, memory structure. Phase 2: 3/3 (100%) — component registry, confidence routing, history index. Phase 3: 2/2 (100%) — adaptive memory/compaction, truthfulness/autonomy hardening",
|
||||
"next_up": "OpenClaw gap: wire companion platform clients into concrete macOS/iOS/Android runtime app entrypoints"
|
||||
"next_up": "OpenClaw gap: implement macOS/iOS/Android companion runtime clients on top of `src/companion/runtimeClient.ts`"
|
||||
},
|
||||
"soul_md_and_cron_create": {
|
||||
"date": "2026-02-11",
|
||||
|
||||
@@ -3,3 +3,4 @@ export { WebhookHandler } from './webhooks.js';
|
||||
export { GmailWatcher } from './gmail.js';
|
||||
export { HeartbeatMonitor, parseInterval } from './heartbeat.js';
|
||||
export type { HeartbeatResult, HeartbeatDeps, CheckResult } from './heartbeat.js';
|
||||
export { buildPresetCronJobs } from './presets.js';
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { configSchema } from '../config/schema.js';
|
||||
import { buildPresetCronJobs } from './presets.js';
|
||||
|
||||
describe('buildPresetCronJobs', () => {
|
||||
it('creates a daily briefing preset cron job when enabled with output', () => {
|
||||
const config = configSchema.parse({
|
||||
telegram: { bot_token: 'token', allowed_chat_ids: [1] },
|
||||
models: { default: { provider: 'anthropic', model: 'claude-sonnet' } },
|
||||
automation: {
|
||||
daily_briefing: {
|
||||
enabled: true,
|
||||
schedule: '0 7 * * *',
|
||||
timezone: 'America/New_York',
|
||||
output: { channel: 'telegram', peer: '1' },
|
||||
model_tier: 'fast',
|
||||
prompt: 'Daily briefing prompt',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const jobs = buildPresetCronJobs(config);
|
||||
expect(jobs).toHaveLength(1);
|
||||
expect(jobs[0]).toMatchObject({
|
||||
name: 'daily-briefing',
|
||||
schedule: '0 7 * * *',
|
||||
timezone: 'America/New_York',
|
||||
output: { channel: 'telegram', peer: '1' },
|
||||
model_tier: 'fast',
|
||||
message: 'Daily briefing prompt',
|
||||
});
|
||||
});
|
||||
|
||||
it('skips daily briefing job when output is missing', () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const config = configSchema.parse({
|
||||
telegram: { bot_token: 'token', allowed_chat_ids: [1] },
|
||||
models: { default: { provider: 'anthropic', model: 'claude-sonnet' } },
|
||||
automation: {
|
||||
daily_briefing: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const jobs = buildPresetCronJobs(config);
|
||||
expect(jobs).toHaveLength(0);
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('output is missing'));
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('skips preset when daily briefing name conflicts with user cron job', () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const config = configSchema.parse({
|
||||
telegram: { bot_token: 'token', allowed_chat_ids: [1] },
|
||||
models: { default: { provider: 'anthropic', model: 'claude-sonnet' } },
|
||||
automation: {
|
||||
cron: [{
|
||||
name: 'daily-briefing',
|
||||
schedule: '0 9 * * *',
|
||||
message: 'manual job',
|
||||
output: { channel: 'telegram', peer: '1' },
|
||||
}],
|
||||
daily_briefing: {
|
||||
enabled: true,
|
||||
output: { channel: 'telegram', peer: '1' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const jobs = buildPresetCronJobs(config);
|
||||
expect(jobs).toHaveLength(0);
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('conflicts with automation.cron'));
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { Config, CronJobConfig } from '../config/schema.js';
|
||||
|
||||
/**
|
||||
* Builds config-derived cron jobs that are not manually listed under automation.cron.
|
||||
* This keeps opinionated automation features opt-in while reusing the existing CronScheduler.
|
||||
*/
|
||||
export function buildPresetCronJobs(config: Config): CronJobConfig[] {
|
||||
const jobs: CronJobConfig[] = [];
|
||||
const existingNames = new Set(config.automation.cron.map((job) => job.name));
|
||||
|
||||
const briefing = config.automation.daily_briefing;
|
||||
if (briefing.enabled) {
|
||||
if (!briefing.output) {
|
||||
console.warn('automation.daily_briefing.enabled=true but output is missing; skipping daily briefing job');
|
||||
} else if (existingNames.has(briefing.name)) {
|
||||
console.warn(`automation.daily_briefing name '${briefing.name}' conflicts with automation.cron; skipping preset job`);
|
||||
} else {
|
||||
jobs.push({
|
||||
name: briefing.name,
|
||||
schedule: briefing.schedule,
|
||||
message: briefing.prompt,
|
||||
output: briefing.output,
|
||||
enabled: true,
|
||||
timezone: briefing.timezone,
|
||||
model_tier: briefing.model_tier,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return jobs;
|
||||
}
|
||||
@@ -205,7 +205,9 @@ describe('configSchema — backup', () => {
|
||||
it('defaults backup settings', () => {
|
||||
const result = configSchema.parse(minimalConfig);
|
||||
expect(result.backup.enabled).toBe(false);
|
||||
expect(result.backup.schedule).toBeUndefined();
|
||||
expect(result.backup.interval).toBe('24h');
|
||||
expect(result.backup.run_on_start).toBe(false);
|
||||
expect(result.backup.include_vectors).toBe(true);
|
||||
expect(result.backup.minio.enabled).toBe(false);
|
||||
expect(result.backup.minio.prefix).toBe('flynn');
|
||||
@@ -217,7 +219,9 @@ describe('configSchema — backup', () => {
|
||||
...minimalConfig,
|
||||
backup: {
|
||||
enabled: true,
|
||||
schedule: '0 2 * * *',
|
||||
interval: '12h',
|
||||
run_on_start: true,
|
||||
local_dir: '/tmp/flynn-backups',
|
||||
include_vectors: false,
|
||||
minio: {
|
||||
@@ -233,7 +237,9 @@ describe('configSchema — backup', () => {
|
||||
});
|
||||
|
||||
expect(result.backup.enabled).toBe(true);
|
||||
expect(result.backup.schedule).toBe('0 2 * * *');
|
||||
expect(result.backup.interval).toBe('12h');
|
||||
expect(result.backup.run_on_start).toBe(true);
|
||||
expect(result.backup.local_dir).toBe('/tmp/flynn-backups');
|
||||
expect(result.backup.include_vectors).toBe(false);
|
||||
expect(result.backup.minio.enabled).toBe(true);
|
||||
@@ -845,6 +851,9 @@ describe('configSchema automation', () => {
|
||||
expect(result.automation).toBeDefined();
|
||||
expect(result.automation.delivery_mode).toBe('shared_session');
|
||||
expect(result.automation.cron).toEqual([]);
|
||||
expect(result.automation.daily_briefing.enabled).toBe(false);
|
||||
expect(result.automation.daily_briefing.schedule).toBe('0 8 * * *');
|
||||
expect(result.automation.daily_briefing.name).toBe('daily-briefing');
|
||||
});
|
||||
|
||||
it('accepts isolated automation delivery mode', () => {
|
||||
@@ -919,6 +928,31 @@ describe('configSchema automation', () => {
|
||||
expect(result.automation.cron[0].enabled).toBe(false);
|
||||
expect(result.automation.cron[0].timezone).toBe('America/New_York');
|
||||
});
|
||||
|
||||
it('accepts daily briefing automation config', () => {
|
||||
const result = configSchema.parse({
|
||||
...baseConfig,
|
||||
automation: {
|
||||
daily_briefing: {
|
||||
enabled: true,
|
||||
name: 'weekday-briefing',
|
||||
schedule: '0 7 * * 1-5',
|
||||
timezone: 'America/New_York',
|
||||
output: { channel: 'telegram', peer: '123' },
|
||||
prompt: 'Custom briefing prompt',
|
||||
model_tier: 'fast',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.automation.daily_briefing.enabled).toBe(true);
|
||||
expect(result.automation.daily_briefing.name).toBe('weekday-briefing');
|
||||
expect(result.automation.daily_briefing.schedule).toBe('0 7 * * 1-5');
|
||||
expect(result.automation.daily_briefing.timezone).toBe('America/New_York');
|
||||
expect(result.automation.daily_briefing.output).toEqual({ channel: 'telegram', peer: '123' });
|
||||
expect(result.automation.daily_briefing.prompt).toBe('Custom briefing prompt');
|
||||
expect(result.automation.daily_briefing.model_tier).toBe('fast');
|
||||
});
|
||||
});
|
||||
|
||||
describe('configSchema — intents', () => {
|
||||
|
||||
@@ -341,6 +341,36 @@ const gtasksSchema = z.object({
|
||||
token_file: z.string().default('~/.config/flynn/gtasks-token.json'),
|
||||
}).optional();
|
||||
|
||||
const dailyBriefingSchema = z.object({
|
||||
enabled: z.boolean().default(false),
|
||||
name: z.string().min(1).default('daily-briefing'),
|
||||
schedule: z.string().min(1).default('0 8 * * *'),
|
||||
timezone: z.string().optional(),
|
||||
output: z.object({
|
||||
channel: z.string().min(1),
|
||||
peer: z.string().min(1),
|
||||
}).optional(),
|
||||
prompt: z.string().min(1).default(
|
||||
[
|
||||
'Create my daily briefing.',
|
||||
'',
|
||||
'Use available tools to gather:',
|
||||
'- Today\'s calendar events (calendar.today or calendar.list)',
|
||||
'- Unread or recent important email (gmail.search/gmail.list)',
|
||||
'- Top pending tasks (tasks.list/tasks.lists)',
|
||||
'',
|
||||
'Output format:',
|
||||
'1) Schedule',
|
||||
'2) Priorities',
|
||||
'3) Risks/Follow-ups',
|
||||
'4) Suggested first actions',
|
||||
'',
|
||||
'Keep it concise and actionable.',
|
||||
].join('\n'),
|
||||
),
|
||||
model_tier: modelTierEnum.optional(),
|
||||
}).default({});
|
||||
|
||||
const automationDeliveryModeSchema = z.enum(['shared_session', 'isolated_job']);
|
||||
|
||||
const automationSchema = z.object({
|
||||
@@ -353,6 +383,7 @@ const automationSchema = z.object({
|
||||
gdocs: gdocsSchema,
|
||||
gdrive: gdriveSchema,
|
||||
gtasks: gtasksSchema,
|
||||
daily_briefing: dailyBriefingSchema,
|
||||
heartbeat: heartbeatSchema,
|
||||
}).default({});
|
||||
|
||||
@@ -683,7 +714,9 @@ const sessionsSchema = z.object({
|
||||
|
||||
const backupSchema = z.object({
|
||||
enabled: z.boolean().default(false),
|
||||
schedule: z.string().optional(),
|
||||
interval: z.string().default('24h'),
|
||||
run_on_start: z.boolean().default(false),
|
||||
local_dir: z.string().default('~/.local/share/flynn/backups'),
|
||||
include_vectors: z.boolean().default(true),
|
||||
minio: z.object({
|
||||
@@ -814,6 +847,7 @@ export type GcalConfig = z.infer<typeof gcalSchema>;
|
||||
export type GdocsConfig = z.infer<typeof gdocsSchema>;
|
||||
export type GdriveConfig = z.infer<typeof gdriveSchema>;
|
||||
export type GtasksConfig = z.infer<typeof gtasksSchema>;
|
||||
export type DailyBriefingConfig = z.infer<typeof dailyBriefingSchema>;
|
||||
export type AutomationDeliveryMode = z.infer<typeof automationDeliveryModeSchema>;
|
||||
export type PairingCodeConfig = z.infer<typeof pairingSchema>;
|
||||
export type LogLevel = z.infer<typeof logLevelSchema>;
|
||||
|
||||
@@ -142,4 +142,40 @@ describe('registerChannels', () => {
|
||||
expect(names).toContain('zalo');
|
||||
expect(gateway.setZaloHandler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('registers cron scheduler when daily briefing preset is enabled', () => {
|
||||
const config = configSchema.parse({
|
||||
telegram: { bot_token: 'test-token', allowed_chat_ids: [1] },
|
||||
models: { default: { provider: 'anthropic', model: 'claude-3' } },
|
||||
automation: {
|
||||
daily_briefing: {
|
||||
enabled: true,
|
||||
schedule: '0 8 * * *',
|
||||
output: { channel: 'telegram', peer: '1' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const channelRegistry = new ChannelRegistry();
|
||||
const gateway = {
|
||||
setWebhookHandler: vi.fn(),
|
||||
setGmailHandler: vi.fn(),
|
||||
setTeamsHandler: vi.fn(),
|
||||
setGoogleChatHandler: vi.fn(),
|
||||
setBlueBubblesHandler: vi.fn(),
|
||||
setLineHandler: vi.fn(),
|
||||
setFeishuHandler: vi.fn(),
|
||||
setZaloHandler: vi.fn(),
|
||||
};
|
||||
|
||||
registerChannels({
|
||||
config,
|
||||
channelRegistry,
|
||||
hookEngine: new HookEngine(config.hooks),
|
||||
gateway: gateway as unknown as Parameters<typeof registerChannels>[0]['gateway'],
|
||||
});
|
||||
|
||||
const names = channelRegistry.list().map((adapter) => adapter.name);
|
||||
expect(names).toContain('cron');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Config } from '../config/index.js';
|
||||
import type { HookEngine } from '../hooks/index.js';
|
||||
import { ChannelRegistry, TelegramAdapter, WebChatAdapter, DiscordAdapter, SlackAdapter, WhatsAppAdapter, MatrixAdapter, SignalAdapter, MattermostAdapter, TeamsAdapter, GoogleChatAdapter, BlueBubblesAdapter, LineAdapter, FeishuAdapter, ZaloAdapter, PairingManager } from '../channels/index.js';
|
||||
import { CronScheduler, WebhookHandler, GmailWatcher } from '../automation/index.js';
|
||||
import { CronScheduler, WebhookHandler, GmailWatcher, buildPresetCronJobs } from '../automation/index.js';
|
||||
import type { GatewayServer } from '../gateway/index.js';
|
||||
|
||||
export interface ChannelsDeps {
|
||||
@@ -202,10 +202,12 @@ export function registerChannels(deps: ChannelsDeps): ChannelsResult {
|
||||
|
||||
// Register cron scheduler adapter (if any cron jobs configured)
|
||||
let cronScheduler: CronScheduler | undefined;
|
||||
if (config.automation.cron.length > 0) {
|
||||
cronScheduler = new CronScheduler(config.automation.cron, channelRegistry, config.automation.delivery_mode);
|
||||
const presetCronJobs = buildPresetCronJobs(config);
|
||||
const cronJobs = [...config.automation.cron, ...presetCronJobs];
|
||||
if (cronJobs.length > 0) {
|
||||
cronScheduler = new CronScheduler(cronJobs, channelRegistry, config.automation.delivery_mode);
|
||||
channelRegistry.register(cronScheduler);
|
||||
console.log(`Registered ${config.automation.cron.length} cron job(s)`);
|
||||
console.log(`Registered ${cronJobs.length} cron job(s)`);
|
||||
}
|
||||
|
||||
// Register webhook handler adapter (if any webhooks configured)
|
||||
|
||||
+41
-6
@@ -2,6 +2,7 @@
|
||||
import { resolve } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { mkdirSync } from 'fs';
|
||||
import { Cron } from 'croner';
|
||||
|
||||
// ── Config & Types ──
|
||||
import type { Config } from '../config/index.js';
|
||||
@@ -106,7 +107,8 @@ export async function startDaemon(config: Config, options?: StartDaemonOptions):
|
||||
|
||||
if (config.backup.enabled) {
|
||||
const backupIntervalMs = parseDuration(config.backup.interval);
|
||||
if (!backupIntervalMs) {
|
||||
const backupSchedule = config.backup.schedule?.trim();
|
||||
if (!backupSchedule && !backupIntervalMs) {
|
||||
console.warn(`Backup enabled but interval is invalid: ${config.backup.interval}`);
|
||||
} else {
|
||||
let backupRunning = false;
|
||||
@@ -129,11 +131,44 @@ export async function startDaemon(config: Config, options?: StartDaemonOptions):
|
||||
}
|
||||
};
|
||||
|
||||
const backupInterval = setInterval(() => {
|
||||
void runScheduledBackup();
|
||||
}, backupIntervalMs);
|
||||
lifecycle.onShutdown(async () => { clearInterval(backupInterval); });
|
||||
console.log(`Backup scheduler enabled (${config.backup.interval})`);
|
||||
let backupCron: Cron | undefined;
|
||||
let backupInterval: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
if (backupSchedule) {
|
||||
try {
|
||||
backupCron = new Cron(backupSchedule, { paused: false }, () => {
|
||||
void runScheduledBackup();
|
||||
});
|
||||
console.log(`Backup scheduler enabled (cron: ${backupSchedule})`);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`Backup cron schedule is invalid (${backupSchedule}): ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!backupCron && backupIntervalMs) {
|
||||
backupInterval = setInterval(() => {
|
||||
void runScheduledBackup();
|
||||
}, backupIntervalMs);
|
||||
console.log(`Backup scheduler enabled (interval: ${config.backup.interval})`);
|
||||
}
|
||||
|
||||
if (!backupCron && !backupInterval) {
|
||||
console.warn('Backup scheduler disabled: no valid backup.schedule or backup.interval');
|
||||
} else {
|
||||
if (config.backup.run_on_start) {
|
||||
void runScheduledBackup();
|
||||
}
|
||||
|
||||
lifecycle.onShutdown(async () => {
|
||||
if (backupCron) {
|
||||
backupCron.stop();
|
||||
}
|
||||
if (backupInterval) {
|
||||
clearInterval(backupInterval);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ describe('discoverServices', () => {
|
||||
expect.objectContaining({ name: 'feishu', status: 'not_configured' }),
|
||||
expect.objectContaining({ name: 'zalo', status: 'not_configured' }),
|
||||
expect.objectContaining({ name: 'cron', status: 'not_configured' }),
|
||||
expect.objectContaining({ name: 'daily_briefing', status: 'not_configured' }),
|
||||
expect.objectContaining({ name: 'mcp', status: 'not_configured' }),
|
||||
expect.objectContaining({ name: 'web_search', status: 'configured' }),
|
||||
expect.objectContaining({ name: 'audio_transcription', status: 'not_configured' }),
|
||||
@@ -99,13 +100,18 @@ describe('discoverServices', () => {
|
||||
cfg.automation.cron = [
|
||||
{ name: 'job', schedule: '0 0 * * *', message: 'hi', output: { channel: 'webchat', peer: 'x' }, enabled: true },
|
||||
] as CronJobConfig[];
|
||||
(cfg.automation as Record<string, unknown>).daily_briefing = {
|
||||
enabled: true,
|
||||
output: { channel: 'webchat', peer: 'x' },
|
||||
};
|
||||
cfg.mcp.servers = [{ name: 'srv', command: 'x', args: [] }];
|
||||
|
||||
const reg = new ChannelRegistry();
|
||||
const services = discoverServices(cfg, reg);
|
||||
|
||||
expect(services.find(s => s.name === 'cron')?.status).toBe('configured');
|
||||
expect(services.find(s => s.name === 'cron')?.itemCount).toBe(1);
|
||||
expect(services.find(s => s.name === 'cron')?.itemCount).toBe(2);
|
||||
expect(services.find(s => s.name === 'daily_briefing')?.status).toBe('configured');
|
||||
expect(services.find(s => s.name === 'mcp')?.metadata).toEqual({ serverCount: 1 });
|
||||
});
|
||||
|
||||
|
||||
@@ -122,9 +122,11 @@ export function discoverServices(
|
||||
});
|
||||
|
||||
const automation = config.automation;
|
||||
const dailyBriefingEnabled = Boolean(automation.daily_briefing?.enabled && automation.daily_briefing.output);
|
||||
const totalCronJobs = automation.cron.length + (dailyBriefingEnabled ? 1 : 0);
|
||||
|
||||
const automationConfigs: Array<{ enabled: boolean; name: string; description: string; itemCount?: number }> = [
|
||||
{ enabled: automation.cron.length > 0, name: 'cron', description: 'Cron scheduler', itemCount: automation.cron.length },
|
||||
{ enabled: totalCronJobs > 0, name: 'cron', description: 'Cron scheduler', itemCount: totalCronJobs },
|
||||
{ enabled: automation.webhooks.length > 0, name: 'webhooks', description: 'Webhook handler', itemCount: automation.webhooks.length },
|
||||
{ enabled: automation.gmail?.enabled ?? false, name: 'gmail', description: 'Gmail watcher' },
|
||||
{ enabled: automation.heartbeat?.enabled ?? false, name: 'heartbeat', description: 'Heartbeat monitor' },
|
||||
@@ -132,6 +134,7 @@ export function discoverServices(
|
||||
{ enabled: automation.gdocs?.enabled ?? false, name: 'gdocs', description: 'Google Docs' },
|
||||
{ enabled: automation.gdrive?.enabled ?? false, name: 'gdrive', description: 'Google Drive' },
|
||||
{ enabled: automation.gtasks?.enabled ?? false, name: 'gtasks', description: 'Google Tasks' },
|
||||
{ enabled: automation.daily_briefing?.enabled ?? false, name: 'daily_briefing', description: 'Daily briefing automation' },
|
||||
];
|
||||
|
||||
for (const auto of automationConfigs) {
|
||||
|
||||
Reference in New Issue
Block a user