opc-agent 1.3.2 → 2.0.0
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/.github/ISSUE_TEMPLATE/bug_report.md +20 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +14 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +13 -0
- package/.github/workflows/ci.yml +24 -0
- package/CHANGELOG.md +48 -63
- package/CONTRIBUTING.md +21 -60
- package/README.md +284 -348
- package/README.zh-CN.md +415 -415
- package/dist/channels/slack.js +93 -10
- package/dist/channels/telegram.d.ts +30 -9
- package/dist/channels/telegram.js +125 -33
- package/dist/channels/web.d.ts +10 -0
- package/dist/channels/web.js +33 -2
- package/dist/cli.js +667 -65
- package/dist/core/agent.d.ts +23 -0
- package/dist/core/agent.js +120 -3
- package/dist/core/runtime.d.ts +5 -0
- package/dist/core/runtime.js +71 -0
- package/dist/core/scheduler.d.ts +52 -0
- package/dist/core/scheduler.js +168 -0
- package/dist/core/subagent.d.ts +28 -0
- package/dist/core/subagent.js +65 -0
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.js +134 -0
- package/dist/deploy/hermes.js +22 -22
- package/dist/deploy/openclaw.js +31 -40
- package/dist/index.d.ts +10 -10
- package/dist/index.js +22 -15
- package/dist/providers/index.d.ts +6 -2
- package/dist/providers/index.js +22 -9
- package/dist/schema/oad.d.ts +180 -6
- package/dist/schema/oad.js +12 -1
- package/dist/skills/auto-learn.d.ts +28 -0
- package/dist/skills/auto-learn.js +257 -0
- package/dist/templates/code-reviewer.d.ts +0 -8
- package/dist/templates/code-reviewer.js +5 -9
- package/dist/templates/customer-service.d.ts +0 -8
- package/dist/templates/customer-service.js +2 -6
- package/dist/templates/data-analyst.d.ts +0 -8
- package/dist/templates/data-analyst.js +5 -9
- package/dist/templates/knowledge-base.d.ts +0 -8
- package/dist/templates/knowledge-base.js +2 -6
- package/dist/templates/sales-assistant.d.ts +0 -8
- package/dist/templates/sales-assistant.js +4 -8
- package/dist/templates/teacher.d.ts +0 -8
- package/dist/templates/teacher.js +6 -10
- package/dist/tools/builtin/datetime.d.ts +3 -0
- package/dist/tools/builtin/datetime.js +44 -0
- package/dist/tools/builtin/file.d.ts +3 -0
- package/dist/tools/builtin/file.js +151 -0
- package/dist/tools/builtin/index.d.ts +15 -0
- package/dist/tools/builtin/index.js +30 -0
- package/dist/tools/builtin/shell.d.ts +3 -0
- package/dist/tools/builtin/shell.js +43 -0
- package/dist/tools/builtin/web.d.ts +3 -0
- package/dist/tools/builtin/web.js +37 -0
- package/dist/tools/mcp-client.d.ts +24 -0
- package/dist/tools/mcp-client.js +119 -0
- package/dist/traces/index.d.ts +49 -0
- package/dist/traces/index.js +102 -0
- package/docs/.vitepress/config.ts +103 -103
- package/docs/api/cli.md +48 -48
- package/docs/api/oad-schema.md +64 -64
- package/docs/api/sdk.md +80 -80
- package/docs/guide/concepts.md +51 -51
- package/docs/guide/configuration.md +79 -79
- package/docs/guide/deployment.md +42 -42
- package/docs/guide/getting-started.md +44 -44
- package/docs/guide/templates.md +28 -28
- package/docs/guide/testing.md +84 -84
- package/docs/index.md +27 -27
- package/docs/zh/api/cli.md +54 -54
- package/docs/zh/api/oad-schema.md +87 -87
- package/docs/zh/api/sdk.md +102 -102
- package/docs/zh/guide/concepts.md +104 -104
- package/docs/zh/guide/configuration.md +135 -135
- package/docs/zh/guide/deployment.md +81 -81
- package/docs/zh/guide/getting-started.md +82 -82
- package/docs/zh/guide/templates.md +84 -84
- package/docs/zh/guide/testing.md +88 -88
- package/docs/zh/index.md +27 -27
- package/examples/README.md +22 -0
- package/examples/basic-agent.ts +90 -0
- package/examples/brain-integration.ts +71 -0
- package/examples/customer-service-demo/README.md +90 -90
- package/examples/customer-service-demo/oad.yaml +107 -107
- package/examples/multi-channel.ts +74 -0
- package/package.json +1 -1
- package/src/analytics/index.ts +66 -66
- package/src/channels/discord.ts +192 -192
- package/src/channels/email.ts +177 -177
- package/src/channels/feishu.ts +236 -236
- package/src/channels/index.ts +15 -15
- package/src/channels/slack.ts +217 -160
- package/src/channels/telegram.ts +155 -33
- package/src/channels/voice.ts +106 -106
- package/src/channels/web.ts +38 -2
- package/src/channels/webhook.ts +199 -199
- package/src/channels/websocket.ts +87 -87
- package/src/channels/wechat.ts +149 -149
- package/src/cli.ts +697 -63
- package/src/core/a2a.ts +143 -143
- package/src/core/agent.ts +146 -3
- package/src/core/analytics-engine.ts +186 -186
- package/src/core/auth.ts +57 -57
- package/src/core/cache.ts +141 -141
- package/src/core/compose.ts +77 -77
- package/src/core/config.ts +14 -14
- package/src/core/errors.ts +148 -148
- package/src/core/hitl.ts +138 -138
- package/src/core/logger.ts +57 -57
- package/src/core/orchestrator.ts +215 -215
- package/src/core/performance.ts +187 -187
- package/src/core/rate-limiter.ts +128 -128
- package/src/core/room.ts +109 -109
- package/src/core/runtime.ts +230 -152
- package/src/core/sandbox.ts +101 -101
- package/src/core/scheduler.ts +187 -0
- package/src/core/security.ts +171 -171
- package/src/core/subagent.ts +98 -0
- package/src/core/types.ts +68 -68
- package/src/core/versioning.ts +106 -106
- package/src/core/watch.ts +178 -178
- package/src/core/workflow.ts +235 -235
- package/src/daemon.ts +96 -0
- package/src/deploy/hermes.ts +156 -156
- package/src/deploy/openclaw.ts +190 -200
- package/src/i18n/index.ts +216 -216
- package/src/index.ts +14 -10
- package/src/memory/deepbrain.ts +108 -108
- package/src/memory/index.ts +34 -34
- package/src/plugins/index.ts +208 -208
- package/src/providers/index.ts +354 -331
- package/src/schema/oad.ts +14 -2
- package/src/skills/auto-learn.ts +262 -0
- package/src/skills/base.ts +16 -16
- package/src/skills/document.ts +100 -100
- package/src/skills/http.ts +35 -35
- package/src/skills/index.ts +27 -27
- package/src/skills/scheduler.ts +80 -80
- package/src/skills/webhook-trigger.ts +59 -59
- package/src/templates/code-reviewer.ts +30 -34
- package/src/templates/customer-service.ts +76 -80
- package/src/templates/data-analyst.ts +66 -70
- package/src/templates/executive-assistant.ts +71 -71
- package/src/templates/financial-advisor.ts +60 -60
- package/src/templates/knowledge-base.ts +27 -31
- package/src/templates/legal-assistant.ts +71 -71
- package/src/templates/sales-assistant.ts +75 -79
- package/src/templates/teacher.ts +75 -79
- package/src/testing/index.ts +181 -181
- package/src/tools/builtin/datetime.ts +41 -0
- package/src/tools/builtin/file.ts +107 -0
- package/src/tools/builtin/index.ts +28 -0
- package/src/tools/builtin/shell.ts +43 -0
- package/src/tools/builtin/web.ts +35 -0
- package/src/tools/calculator.ts +73 -73
- package/src/tools/datetime.ts +149 -149
- package/src/tools/json-transform.ts +187 -187
- package/src/tools/mcp-client.ts +131 -0
- package/src/tools/mcp.ts +76 -76
- package/src/tools/text-analysis.ts +116 -116
- package/src/traces/index.ts +132 -0
- package/templates/Dockerfile +15 -15
- package/templates/code-reviewer/README.md +27 -27
- package/templates/code-reviewer/oad.yaml +41 -41
- package/templates/customer-service/README.md +22 -22
- package/templates/customer-service/oad.yaml +36 -36
- package/templates/docker-compose.yml +21 -21
- package/templates/ecommerce-assistant/README.md +45 -45
- package/templates/ecommerce-assistant/oad.yaml +47 -47
- package/templates/knowledge-base/README.md +28 -28
- package/templates/knowledge-base/oad.yaml +38 -38
- package/templates/sales-assistant/README.md +26 -26
- package/templates/sales-assistant/oad.yaml +43 -43
- package/templates/tech-support/README.md +43 -43
- package/templates/tech-support/oad.yaml +45 -45
- package/test-agent/Dockerfile +9 -0
- package/test-agent/README.md +50 -0
- package/test-agent/agent.yaml +23 -0
- package/test-agent/docker-compose.yml +11 -0
- package/test-agent/oad.yaml +31 -0
- package/test-agent/package-lock.json +1492 -0
- package/test-agent/package.json +18 -0
- package/test-agent/src/index.ts +24 -0
- package/test-agent/src/skills/echo.ts +15 -0
- package/test-agent/tsconfig.json +25 -0
- package/tests/a2a.test.ts +66 -66
- package/tests/agent.test.ts +72 -72
- package/tests/analytics.test.ts +50 -50
- package/tests/auto-learn.test.ts +105 -0
- package/tests/builtin-tools.test.ts +83 -0
- package/tests/channel.test.ts +39 -39
- package/tests/cli.test.ts +46 -0
- package/tests/e2e.test.ts +134 -134
- package/tests/errors.test.ts +83 -83
- package/tests/hitl.test.ts +71 -71
- package/tests/i18n.test.ts +41 -41
- package/tests/mcp.test.ts +54 -54
- package/tests/oad.test.ts +68 -68
- package/tests/performance.test.ts +115 -115
- package/tests/plugin.test.ts +74 -74
- package/tests/room.test.ts +106 -106
- package/tests/runtime.test.ts +42 -42
- package/tests/sandbox.test.ts +46 -46
- package/tests/security.test.ts +60 -60
- package/tests/subagent.test.ts +130 -0
- package/tests/telegram-discord.test.ts +60 -0
- package/tests/templates.test.ts +77 -77
- package/tests/v070.test.ts +76 -76
- package/tests/versioning.test.ts +75 -75
- package/tests/voice.test.ts +61 -61
- package/tests/webhook.test.ts +29 -29
- package/tests/workflow.test.ts +143 -143
- package/tsconfig.json +19 -19
- package/vitest.config.ts +9 -9
- package/dist/core/dashboard.d.ts +0 -35
- package/dist/core/dashboard.js +0 -157
- package/dist/core/priority.d.ts +0 -52
- package/dist/core/priority.js +0 -102
- package/src/core/dashboard.ts +0 -219
- package/src/core/priority.ts +0 -140
- package/src/dtv/data.ts +0 -29
- package/src/dtv/trust.ts +0 -43
- package/src/dtv/value.ts +0 -47
- package/src/marketplace/index.ts +0 -223
package/src/core/sandbox.ts
CHANGED
|
@@ -1,101 +1,101 @@
|
|
|
1
|
-
import type { TrustLevelType } from '../schema/oad';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
|
|
4
|
-
export interface SandboxConfig {
|
|
5
|
-
trustLevel: TrustLevelType;
|
|
6
|
-
agentDir: string;
|
|
7
|
-
networkAllowlist?: string[];
|
|
8
|
-
shellAllowed?: boolean;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface SandboxRestrictions {
|
|
12
|
-
fileSystem: { read: string[]; write: string[] };
|
|
13
|
-
network: { allowed: string[] };
|
|
14
|
-
shell: boolean;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const TRUST_RESTRICTIONS: Record<string, SandboxRestrictions> = {
|
|
18
|
-
sandbox: {
|
|
19
|
-
fileSystem: { read: ['.'], write: ['.'] },
|
|
20
|
-
network: { allowed: [] },
|
|
21
|
-
shell: false,
|
|
22
|
-
},
|
|
23
|
-
verified: {
|
|
24
|
-
fileSystem: { read: ['.', '..'], write: ['.'] },
|
|
25
|
-
network: { allowed: ['*.deepleaper.com', 'api.openai.com', 'api.deepseek.com'] },
|
|
26
|
-
shell: false,
|
|
27
|
-
},
|
|
28
|
-
certified: {
|
|
29
|
-
fileSystem: { read: ['*'], write: ['.', '..'] },
|
|
30
|
-
network: { allowed: ['*'] },
|
|
31
|
-
shell: true,
|
|
32
|
-
},
|
|
33
|
-
listed: {
|
|
34
|
-
fileSystem: { read: ['*'], write: ['*'] },
|
|
35
|
-
network: { allowed: ['*'] },
|
|
36
|
-
shell: true,
|
|
37
|
-
},
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export class Sandbox {
|
|
41
|
-
private config: SandboxConfig;
|
|
42
|
-
private restrictions: SandboxRestrictions;
|
|
43
|
-
|
|
44
|
-
constructor(config: SandboxConfig) {
|
|
45
|
-
this.config = config;
|
|
46
|
-
this.restrictions = {
|
|
47
|
-
...TRUST_RESTRICTIONS[config.trustLevel] ?? TRUST_RESTRICTIONS.sandbox,
|
|
48
|
-
};
|
|
49
|
-
if (config.networkAllowlist) {
|
|
50
|
-
this.restrictions.network.allowed = config.networkAllowlist;
|
|
51
|
-
}
|
|
52
|
-
if (config.shellAllowed !== undefined) {
|
|
53
|
-
this.restrictions.shell = config.shellAllowed;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
get trustLevel(): TrustLevelType {
|
|
58
|
-
return this.config.trustLevel;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
getRestrictions(): SandboxRestrictions {
|
|
62
|
-
return { ...this.restrictions };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
checkFileAccess(filePath: string, mode: 'read' | 'write'): boolean {
|
|
66
|
-
const resolved = path.resolve(filePath);
|
|
67
|
-
const agentDir = path.resolve(this.config.agentDir);
|
|
68
|
-
const allowedPaths = mode === 'read' ? this.restrictions.fileSystem.read : this.restrictions.fileSystem.write;
|
|
69
|
-
|
|
70
|
-
if (allowedPaths.includes('*')) return true;
|
|
71
|
-
|
|
72
|
-
for (const allowed of allowedPaths) {
|
|
73
|
-
const allowedResolved = path.resolve(this.config.agentDir, allowed);
|
|
74
|
-
if (resolved.startsWith(allowedResolved)) return true;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Always allow access within agent's own directory
|
|
78
|
-
return resolved.startsWith(agentDir);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
checkNetworkAccess(url: string): boolean {
|
|
82
|
-
if (this.restrictions.network.allowed.includes('*')) return true;
|
|
83
|
-
if (this.restrictions.network.allowed.length === 0) return false;
|
|
84
|
-
|
|
85
|
-
try {
|
|
86
|
-
const hostname = new URL(url).hostname;
|
|
87
|
-
return this.restrictions.network.allowed.some((pattern) => {
|
|
88
|
-
if (pattern.startsWith('*.')) {
|
|
89
|
-
return hostname.endsWith(pattern.slice(1));
|
|
90
|
-
}
|
|
91
|
-
return hostname === pattern;
|
|
92
|
-
});
|
|
93
|
-
} catch {
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
checkShellAccess(): boolean {
|
|
99
|
-
return this.restrictions.shell;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
1
|
+
import type { TrustLevelType } from '../schema/oad';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
export interface SandboxConfig {
|
|
5
|
+
trustLevel: TrustLevelType;
|
|
6
|
+
agentDir: string;
|
|
7
|
+
networkAllowlist?: string[];
|
|
8
|
+
shellAllowed?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SandboxRestrictions {
|
|
12
|
+
fileSystem: { read: string[]; write: string[] };
|
|
13
|
+
network: { allowed: string[] };
|
|
14
|
+
shell: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const TRUST_RESTRICTIONS: Record<string, SandboxRestrictions> = {
|
|
18
|
+
sandbox: {
|
|
19
|
+
fileSystem: { read: ['.'], write: ['.'] },
|
|
20
|
+
network: { allowed: [] },
|
|
21
|
+
shell: false,
|
|
22
|
+
},
|
|
23
|
+
verified: {
|
|
24
|
+
fileSystem: { read: ['.', '..'], write: ['.'] },
|
|
25
|
+
network: { allowed: ['*.deepleaper.com', 'api.openai.com', 'api.deepseek.com'] },
|
|
26
|
+
shell: false,
|
|
27
|
+
},
|
|
28
|
+
certified: {
|
|
29
|
+
fileSystem: { read: ['*'], write: ['.', '..'] },
|
|
30
|
+
network: { allowed: ['*'] },
|
|
31
|
+
shell: true,
|
|
32
|
+
},
|
|
33
|
+
listed: {
|
|
34
|
+
fileSystem: { read: ['*'], write: ['*'] },
|
|
35
|
+
network: { allowed: ['*'] },
|
|
36
|
+
shell: true,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export class Sandbox {
|
|
41
|
+
private config: SandboxConfig;
|
|
42
|
+
private restrictions: SandboxRestrictions;
|
|
43
|
+
|
|
44
|
+
constructor(config: SandboxConfig) {
|
|
45
|
+
this.config = config;
|
|
46
|
+
this.restrictions = {
|
|
47
|
+
...TRUST_RESTRICTIONS[config.trustLevel] ?? TRUST_RESTRICTIONS.sandbox,
|
|
48
|
+
};
|
|
49
|
+
if (config.networkAllowlist) {
|
|
50
|
+
this.restrictions.network.allowed = config.networkAllowlist;
|
|
51
|
+
}
|
|
52
|
+
if (config.shellAllowed !== undefined) {
|
|
53
|
+
this.restrictions.shell = config.shellAllowed;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get trustLevel(): TrustLevelType {
|
|
58
|
+
return this.config.trustLevel;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getRestrictions(): SandboxRestrictions {
|
|
62
|
+
return { ...this.restrictions };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
checkFileAccess(filePath: string, mode: 'read' | 'write'): boolean {
|
|
66
|
+
const resolved = path.resolve(filePath);
|
|
67
|
+
const agentDir = path.resolve(this.config.agentDir);
|
|
68
|
+
const allowedPaths = mode === 'read' ? this.restrictions.fileSystem.read : this.restrictions.fileSystem.write;
|
|
69
|
+
|
|
70
|
+
if (allowedPaths.includes('*')) return true;
|
|
71
|
+
|
|
72
|
+
for (const allowed of allowedPaths) {
|
|
73
|
+
const allowedResolved = path.resolve(this.config.agentDir, allowed);
|
|
74
|
+
if (resolved.startsWith(allowedResolved)) return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Always allow access within agent's own directory
|
|
78
|
+
return resolved.startsWith(agentDir);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
checkNetworkAccess(url: string): boolean {
|
|
82
|
+
if (this.restrictions.network.allowed.includes('*')) return true;
|
|
83
|
+
if (this.restrictions.network.allowed.length === 0) return false;
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const hostname = new URL(url).hostname;
|
|
87
|
+
return this.restrictions.network.allowed.some((pattern) => {
|
|
88
|
+
if (pattern.startsWith('*.')) {
|
|
89
|
+
return hostname.endsWith(pattern.slice(1));
|
|
90
|
+
}
|
|
91
|
+
return hostname === pattern;
|
|
92
|
+
});
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
checkShellAccess(): boolean {
|
|
99
|
+
return this.restrictions.shell;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple cron scheduler — no external dependencies.
|
|
3
|
+
* Supports cron expressions: star, star-slash-N, M-N, M,N for minute/hour/day/month/weekday.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface CronJob {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
schedule: string;
|
|
10
|
+
task: string;
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
lastRun?: Date;
|
|
13
|
+
nextRun?: Date;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type CronField = { type: 'any' } | { type: 'every'; step: number } | { type: 'list'; values: number[] };
|
|
17
|
+
|
|
18
|
+
interface ParsedCron {
|
|
19
|
+
minute: CronField;
|
|
20
|
+
hour: CronField;
|
|
21
|
+
dayOfMonth: CronField;
|
|
22
|
+
month: CronField;
|
|
23
|
+
dayOfWeek: CronField;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseField(field: string, min: number, max: number): CronField {
|
|
27
|
+
if (field === '*') return { type: 'any' };
|
|
28
|
+
if (field.startsWith('*/')) {
|
|
29
|
+
const step = parseInt(field.slice(2), 10);
|
|
30
|
+
if (isNaN(step) || step <= 0) throw new Error(`Invalid cron step: ${field}`);
|
|
31
|
+
return { type: 'every', step };
|
|
32
|
+
}
|
|
33
|
+
// Could be comma-separated, each part could be a range
|
|
34
|
+
const values: number[] = [];
|
|
35
|
+
for (const part of field.split(',')) {
|
|
36
|
+
if (part.includes('-')) {
|
|
37
|
+
const [a, b] = part.split('-').map(Number);
|
|
38
|
+
if (isNaN(a) || isNaN(b)) throw new Error(`Invalid cron range: ${part}`);
|
|
39
|
+
for (let i = a; i <= b; i++) values.push(i);
|
|
40
|
+
} else {
|
|
41
|
+
const n = parseInt(part, 10);
|
|
42
|
+
if (isNaN(n)) throw new Error(`Invalid cron value: ${part}`);
|
|
43
|
+
values.push(n);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { type: 'list', values };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function parseCron(expr: string): ParsedCron {
|
|
50
|
+
const parts = expr.trim().split(/\s+/);
|
|
51
|
+
if (parts.length !== 5) throw new Error(`Invalid cron expression (need 5 fields): ${expr}`);
|
|
52
|
+
return {
|
|
53
|
+
minute: parseField(parts[0], 0, 59),
|
|
54
|
+
hour: parseField(parts[1], 0, 23),
|
|
55
|
+
dayOfMonth: parseField(parts[2], 1, 31),
|
|
56
|
+
month: parseField(parts[3], 1, 12),
|
|
57
|
+
dayOfWeek: parseField(parts[4], 0, 6),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function fieldMatches(field: CronField, value: number): boolean {
|
|
62
|
+
switch (field.type) {
|
|
63
|
+
case 'any': return true;
|
|
64
|
+
case 'every': return value % field.step === 0;
|
|
65
|
+
case 'list': return field.values.includes(value);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function cronMatches(parsed: ParsedCron, date: Date): boolean {
|
|
70
|
+
return (
|
|
71
|
+
fieldMatches(parsed.minute, date.getMinutes()) &&
|
|
72
|
+
fieldMatches(parsed.hour, date.getHours()) &&
|
|
73
|
+
fieldMatches(parsed.dayOfMonth, date.getDate()) &&
|
|
74
|
+
fieldMatches(parsed.month, date.getMonth() + 1) &&
|
|
75
|
+
fieldMatches(parsed.dayOfWeek, date.getDay())
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Compute approximate next run (scans forward up to 48h). */
|
|
80
|
+
function computeNextRun(parsed: ParsedCron, from: Date): Date | undefined {
|
|
81
|
+
const d = new Date(from);
|
|
82
|
+
d.setSeconds(0, 0);
|
|
83
|
+
d.setMinutes(d.getMinutes() + 1);
|
|
84
|
+
const limit = 48 * 60; // 48 hours in minutes
|
|
85
|
+
for (let i = 0; i < limit; i++) {
|
|
86
|
+
if (cronMatches(parsed, d)) return new Date(d);
|
|
87
|
+
d.setMinutes(d.getMinutes() + 1);
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type JobHandler = (job: CronJob) => void | Promise<void>;
|
|
93
|
+
|
|
94
|
+
export class Scheduler {
|
|
95
|
+
private jobs = new Map<string, CronJob>();
|
|
96
|
+
private parsed = new Map<string, ParsedCron>();
|
|
97
|
+
private timer: ReturnType<typeof setInterval> | null = null;
|
|
98
|
+
private handler: JobHandler;
|
|
99
|
+
|
|
100
|
+
constructor(handler: JobHandler) {
|
|
101
|
+
this.handler = handler;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
addJob(job: CronJob): void {
|
|
105
|
+
const p = parseCron(job.schedule);
|
|
106
|
+
this.parsed.set(job.id, p);
|
|
107
|
+
job.nextRun = computeNextRun(p, new Date()) ?? undefined;
|
|
108
|
+
this.jobs.set(job.id, job);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
removeJob(id: string): void {
|
|
112
|
+
this.jobs.delete(id);
|
|
113
|
+
this.parsed.delete(id);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
enableJob(id: string): void {
|
|
117
|
+
const job = this.jobs.get(id);
|
|
118
|
+
if (job) job.enabled = true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
disableJob(id: string): void {
|
|
122
|
+
const job = this.jobs.get(id);
|
|
123
|
+
if (job) job.enabled = false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
getJobs(): CronJob[] {
|
|
127
|
+
return Array.from(this.jobs.values());
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getJob(id: string): CronJob | undefined {
|
|
131
|
+
return this.jobs.get(id);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Run a specific job immediately */
|
|
135
|
+
async runJob(id: string): Promise<boolean> {
|
|
136
|
+
const job = this.jobs.get(id);
|
|
137
|
+
if (!job) return false;
|
|
138
|
+
job.lastRun = new Date();
|
|
139
|
+
await this.handler(job);
|
|
140
|
+
const parsed = this.parsed.get(id);
|
|
141
|
+
if (parsed) job.nextRun = computeNextRun(parsed, new Date());
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
start(): void {
|
|
146
|
+
if (this.timer) return;
|
|
147
|
+
// Check every 60 seconds
|
|
148
|
+
this.timer = setInterval(() => this.tick(), 60_000);
|
|
149
|
+
// Also tick immediately
|
|
150
|
+
this.tick();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
stop(): void {
|
|
154
|
+
if (this.timer) {
|
|
155
|
+
clearInterval(this.timer);
|
|
156
|
+
this.timer = null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private tick(): void {
|
|
161
|
+
const now = new Date();
|
|
162
|
+
for (const [id, job] of this.jobs) {
|
|
163
|
+
if (!job.enabled) continue;
|
|
164
|
+
const parsed = this.parsed.get(id);
|
|
165
|
+
if (!parsed) continue;
|
|
166
|
+
if (cronMatches(parsed, now)) {
|
|
167
|
+
// Avoid double-fire: check lastRun isn't same minute
|
|
168
|
+
if (job.lastRun) {
|
|
169
|
+
const last = job.lastRun;
|
|
170
|
+
if (last.getFullYear() === now.getFullYear() &&
|
|
171
|
+
last.getMonth() === now.getMonth() &&
|
|
172
|
+
last.getDate() === now.getDate() &&
|
|
173
|
+
last.getHours() === now.getHours() &&
|
|
174
|
+
last.getMinutes() === now.getMinutes()) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
job.lastRun = new Date(now);
|
|
179
|
+
job.nextRun = computeNextRun(parsed, now);
|
|
180
|
+
// Fire and forget (log errors)
|
|
181
|
+
Promise.resolve(this.handler(job)).catch((err) => {
|
|
182
|
+
console.error(`[scheduler] Job "${job.name}" failed:`, err);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|