@zhin.js/core 1.1.0 → 1.1.2
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/lib/adapter.d.ts +1 -26
- package/lib/adapter.d.ts.map +1 -1
- package/lib/adapter.js +20 -117
- package/lib/adapter.js.map +1 -1
- package/lib/built/adapter-process.d.ts +0 -4
- package/lib/built/adapter-process.d.ts.map +1 -1
- package/lib/built/adapter-process.js +0 -95
- package/lib/built/adapter-process.js.map +1 -1
- package/lib/built/agent-preset.d.ts +2 -0
- package/lib/built/agent-preset.d.ts.map +1 -1
- package/lib/built/agent-preset.js +4 -0
- package/lib/built/agent-preset.js.map +1 -1
- package/lib/built/command.d.ts +4 -0
- package/lib/built/command.d.ts.map +1 -1
- package/lib/built/command.js +6 -0
- package/lib/built/command.js.map +1 -1
- package/lib/built/component.d.ts.map +1 -1
- package/lib/built/component.js +1 -0
- package/lib/built/component.js.map +1 -1
- package/lib/built/dispatcher.d.ts.map +1 -1
- package/lib/built/dispatcher.js +0 -13
- package/lib/built/dispatcher.js.map +1 -1
- package/lib/built/message-filter.d.ts +2 -0
- package/lib/built/message-filter.d.ts.map +1 -1
- package/lib/built/message-filter.js +5 -0
- package/lib/built/message-filter.js.map +1 -1
- package/lib/built/skill.d.ts +11 -0
- package/lib/built/skill.d.ts.map +1 -1
- package/lib/built/skill.js +14 -0
- package/lib/built/skill.js.map +1 -1
- package/lib/built/tool.d.ts +11 -44
- package/lib/built/tool.d.ts.map +1 -1
- package/lib/built/tool.js +14 -353
- package/lib/built/tool.js.map +1 -1
- package/lib/plugin.d.ts +1 -25
- package/lib/plugin.d.ts.map +1 -1
- package/lib/plugin.js +1 -77
- package/lib/plugin.js.map +1 -1
- package/lib/types.d.ts +0 -25
- package/lib/types.d.ts.map +1 -1
- package/package.json +10 -7
- package/CHANGELOG.md +0 -561
- package/REFACTORING_COMPLETE.md +0 -178
- package/REFACTORING_STATUS.md +0 -263
- package/src/adapter.ts +0 -275
- package/src/ai/index.ts +0 -55
- package/src/ai/providers/anthropic.ts +0 -379
- package/src/ai/providers/base.ts +0 -175
- package/src/ai/providers/index.ts +0 -13
- package/src/ai/providers/ollama.ts +0 -302
- package/src/ai/providers/openai.ts +0 -174
- package/src/ai/types.ts +0 -348
- package/src/bot.ts +0 -37
- package/src/built/adapter-process.ts +0 -177
- package/src/built/agent-preset.ts +0 -136
- package/src/built/ai-trigger.ts +0 -259
- package/src/built/command.ts +0 -108
- package/src/built/common-adapter-tools.ts +0 -242
- package/src/built/component.ts +0 -130
- package/src/built/config.ts +0 -335
- package/src/built/cron.ts +0 -156
- package/src/built/database.ts +0 -134
- package/src/built/dispatcher.ts +0 -496
- package/src/built/login-assist.ts +0 -131
- package/src/built/message-filter.ts +0 -390
- package/src/built/permission.ts +0 -151
- package/src/built/schema-feature.ts +0 -190
- package/src/built/skill.ts +0 -221
- package/src/built/tool.ts +0 -948
- package/src/command.ts +0 -87
- package/src/component.ts +0 -565
- package/src/cron.ts +0 -4
- package/src/errors.ts +0 -46
- package/src/feature.ts +0 -7
- package/src/index.ts +0 -53
- package/src/jsx-dev-runtime.ts +0 -2
- package/src/jsx-runtime.ts +0 -12
- package/src/jsx.ts +0 -135
- package/src/message.ts +0 -48
- package/src/models/system-log.ts +0 -20
- package/src/models/user.ts +0 -15
- package/src/notice.ts +0 -98
- package/src/plugin.ts +0 -896
- package/src/prompt.ts +0 -293
- package/src/request.ts +0 -95
- package/src/scheduler/index.ts +0 -19
- package/src/scheduler/scheduler.ts +0 -372
- package/src/scheduler/types.ts +0 -74
- package/src/tool-zod.ts +0 -115
- package/src/types-generator.ts +0 -78
- package/src/types.ts +0 -505
- package/src/utils.ts +0 -227
- package/tests/adapter.test.ts +0 -638
- package/tests/ai/ai-trigger.test.ts +0 -368
- package/tests/ai/providers.integration.test.ts +0 -227
- package/tests/ai/setup.ts +0 -308
- package/tests/ai/tool.test.ts +0 -800
- package/tests/bot.test.ts +0 -151
- package/tests/command.test.ts +0 -737
- package/tests/component-new.test.ts +0 -361
- package/tests/config.test.ts +0 -372
- package/tests/cron.test.ts +0 -82
- package/tests/dispatcher.test.ts +0 -293
- package/tests/errors.test.ts +0 -21
- package/tests/expression-evaluation.test.ts +0 -258
- package/tests/features-builtin.test.ts +0 -191
- package/tests/jsx-runtime.test.ts +0 -45
- package/tests/jsx.test.ts +0 -319
- package/tests/message-filter.test.ts +0 -566
- package/tests/message.test.ts +0 -402
- package/tests/notice.test.ts +0 -198
- package/tests/plugin.test.ts +0 -779
- package/tests/prompt.test.ts +0 -78
- package/tests/redos-protection.test.ts +0 -198
- package/tests/request.test.ts +0 -221
- package/tests/schema.test.ts +0 -248
- package/tests/skill-feature.test.ts +0 -179
- package/tests/test-utils.ts +0 -59
- package/tests/tool-feature.test.ts +0 -254
- package/tests/types.test.ts +0 -162
- package/tests/utils.test.ts +0 -135
- package/tsconfig.json +0 -24
|
@@ -1,372 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unified scheduler — at / every / cron + heartbeat
|
|
3
|
-
*
|
|
4
|
-
* 持久化到 data/scheduler-jobs.json,支持单次 at、间隔 every、cron 表达式,
|
|
5
|
-
* 以及可选的 HEARTBEAT.md 周期检查。
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import * as fs from 'fs';
|
|
9
|
-
import * as path from 'path';
|
|
10
|
-
import { randomUUID } from 'crypto';
|
|
11
|
-
import { Cron as Croner } from 'croner';
|
|
12
|
-
import type {
|
|
13
|
-
Schedule,
|
|
14
|
-
JobPayload,
|
|
15
|
-
ScheduledJob,
|
|
16
|
-
JobStore,
|
|
17
|
-
JobCallback,
|
|
18
|
-
AddJobOptions,
|
|
19
|
-
IScheduler,
|
|
20
|
-
} from './types.js';
|
|
21
|
-
import { Logger } from '@zhin.js/logger';
|
|
22
|
-
|
|
23
|
-
const logger = new Logger(null, 'scheduler');
|
|
24
|
-
|
|
25
|
-
const DEFAULT_HEARTBEAT_INTERVAL_MS = 30 * 60 * 1000;
|
|
26
|
-
|
|
27
|
-
const HEARTBEAT_PROMPT = `Read HEARTBEAT.md in your workspace (if it exists).
|
|
28
|
-
Follow any instructions or tasks listed there.
|
|
29
|
-
If nothing needs attention, reply with just: HEARTBEAT_OK`;
|
|
30
|
-
|
|
31
|
-
function nowMs(): number {
|
|
32
|
-
return Date.now();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function computeNextRun(schedule: Schedule, currentMs: number): number | undefined {
|
|
36
|
-
if (schedule.kind === 'at') {
|
|
37
|
-
return schedule.atMs != null && schedule.atMs > currentMs ? schedule.atMs : undefined;
|
|
38
|
-
}
|
|
39
|
-
if (schedule.kind === 'every') {
|
|
40
|
-
if (schedule.everyMs == null || schedule.everyMs <= 0) return undefined;
|
|
41
|
-
return currentMs + schedule.everyMs;
|
|
42
|
-
}
|
|
43
|
-
if (schedule.kind === 'cron' && schedule.expr) {
|
|
44
|
-
try {
|
|
45
|
-
const job = new Croner(schedule.expr, { paused: true, timezone: schedule.tz });
|
|
46
|
-
const next = job.nextRun();
|
|
47
|
-
job.stop();
|
|
48
|
-
return next ? next.getTime() : undefined;
|
|
49
|
-
} catch {
|
|
50
|
-
return undefined;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return undefined;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function createStore(): JobStore {
|
|
57
|
-
return { version: 1, jobs: [] };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function isHeartbeatEmpty(content: string | null): boolean {
|
|
61
|
-
if (!content) return true;
|
|
62
|
-
const skipPatterns = new Set(['- [ ]', '* [ ]', '- [x]', '* [x]']);
|
|
63
|
-
for (const line of content.split('\n')) {
|
|
64
|
-
const trimmed = line.trim();
|
|
65
|
-
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('<!--') || skipPatterns.has(trimmed)) continue;
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
return true;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface SchedulerOptions {
|
|
72
|
-
storePath: string;
|
|
73
|
-
workspace: string;
|
|
74
|
-
onJob?: JobCallback;
|
|
75
|
-
heartbeatEnabled?: boolean;
|
|
76
|
-
heartbeatIntervalMs?: number;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export class Scheduler implements IScheduler {
|
|
80
|
-
private storePath: string;
|
|
81
|
-
private workspace: string;
|
|
82
|
-
private onJob: JobCallback | null = null;
|
|
83
|
-
private store: JobStore | null = null;
|
|
84
|
-
private timerTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
85
|
-
private _running = false;
|
|
86
|
-
private heartbeatEnabled: boolean;
|
|
87
|
-
private heartbeatIntervalMs: number;
|
|
88
|
-
private heartbeatJobId: string | null = null;
|
|
89
|
-
|
|
90
|
-
constructor(options: SchedulerOptions) {
|
|
91
|
-
this.storePath = options.storePath;
|
|
92
|
-
this.workspace = options.workspace;
|
|
93
|
-
this.onJob = options.onJob ?? null;
|
|
94
|
-
this.heartbeatEnabled = options.heartbeatEnabled ?? true;
|
|
95
|
-
this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
private loadStore(): JobStore {
|
|
99
|
-
if (this.store) return this.store;
|
|
100
|
-
if (fs.existsSync(this.storePath)) {
|
|
101
|
-
try {
|
|
102
|
-
const data = JSON.parse(fs.readFileSync(this.storePath, 'utf-8'));
|
|
103
|
-
const jobs: ScheduledJob[] = (data.jobs || []).map((j: any) => ({
|
|
104
|
-
id: j.id,
|
|
105
|
-
name: j.name,
|
|
106
|
-
enabled: j.enabled ?? true,
|
|
107
|
-
schedule: {
|
|
108
|
-
kind: j.schedule?.kind ?? 'cron',
|
|
109
|
-
atMs: j.schedule?.atMs,
|
|
110
|
-
everyMs: j.schedule?.everyMs,
|
|
111
|
-
expr: j.schedule?.expr,
|
|
112
|
-
tz: j.schedule?.tz,
|
|
113
|
-
},
|
|
114
|
-
payload: {
|
|
115
|
-
kind: j.payload?.kind ?? 'agent_turn',
|
|
116
|
-
message: j.payload?.message ?? '',
|
|
117
|
-
deliver: j.payload?.deliver ?? false,
|
|
118
|
-
channel: j.payload?.channel,
|
|
119
|
-
to: j.payload?.to,
|
|
120
|
-
},
|
|
121
|
-
state: {
|
|
122
|
-
nextRunAtMs: j.state?.nextRunAtMs,
|
|
123
|
-
lastRunAtMs: j.state?.lastRunAtMs,
|
|
124
|
-
lastStatus: j.state?.lastStatus,
|
|
125
|
-
lastError: j.state?.lastError,
|
|
126
|
-
},
|
|
127
|
-
createdAtMs: j.createdAtMs ?? 0,
|
|
128
|
-
updatedAtMs: j.updatedAtMs ?? 0,
|
|
129
|
-
deleteAfterRun: j.deleteAfterRun ?? false,
|
|
130
|
-
}));
|
|
131
|
-
this.store = { version: data.version ?? 1, jobs };
|
|
132
|
-
} catch (e) {
|
|
133
|
-
logger.warn('Failed to load scheduler store', e);
|
|
134
|
-
this.store = createStore();
|
|
135
|
-
}
|
|
136
|
-
} else {
|
|
137
|
-
this.store = createStore();
|
|
138
|
-
}
|
|
139
|
-
return this.store;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
private saveStore(): void {
|
|
143
|
-
if (!this.store) return;
|
|
144
|
-
const dir = path.dirname(this.storePath);
|
|
145
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
146
|
-
const persistJobs = this.store.jobs.filter(j => j.id !== this.heartbeatJobId);
|
|
147
|
-
const data = {
|
|
148
|
-
version: this.store.version,
|
|
149
|
-
jobs: persistJobs.map(j => ({
|
|
150
|
-
id: j.id,
|
|
151
|
-
name: j.name,
|
|
152
|
-
enabled: j.enabled,
|
|
153
|
-
schedule: j.schedule,
|
|
154
|
-
payload: j.payload,
|
|
155
|
-
state: j.state,
|
|
156
|
-
createdAtMs: j.createdAtMs,
|
|
157
|
-
updatedAtMs: j.updatedAtMs,
|
|
158
|
-
deleteAfterRun: j.deleteAfterRun,
|
|
159
|
-
})),
|
|
160
|
-
};
|
|
161
|
-
fs.writeFileSync(this.storePath, JSON.stringify(data, null, 2));
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async start(): Promise<void> {
|
|
165
|
-
this._running = true;
|
|
166
|
-
this.loadStore();
|
|
167
|
-
if (this.heartbeatEnabled) this.addHeartbeatJob();
|
|
168
|
-
this.recomputeNextRuns();
|
|
169
|
-
this.saveStore();
|
|
170
|
-
this.armTimer();
|
|
171
|
-
logger.info({ jobs: this.store?.jobs.length ?? 0 }, 'Scheduler started');
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
stop(): void {
|
|
175
|
-
this._running = false;
|
|
176
|
-
if (this.timerTimeout) {
|
|
177
|
-
clearTimeout(this.timerTimeout);
|
|
178
|
-
this.timerTimeout = null;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
private addHeartbeatJob(): void {
|
|
183
|
-
if (!this.store) return;
|
|
184
|
-
const existing = this.store.jobs.find(j => j.payload.kind === 'heartbeat');
|
|
185
|
-
if (existing) {
|
|
186
|
-
this.heartbeatJobId = existing.id;
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
const now = nowMs();
|
|
190
|
-
const job: ScheduledJob = {
|
|
191
|
-
id: `heartbeat-${randomUUID().slice(0, 8)}`,
|
|
192
|
-
name: 'Heartbeat',
|
|
193
|
-
enabled: true,
|
|
194
|
-
schedule: { kind: 'every', everyMs: this.heartbeatIntervalMs },
|
|
195
|
-
payload: { kind: 'heartbeat', message: HEARTBEAT_PROMPT, deliver: false },
|
|
196
|
-
state: { nextRunAtMs: now + this.heartbeatIntervalMs },
|
|
197
|
-
createdAtMs: now,
|
|
198
|
-
updatedAtMs: now,
|
|
199
|
-
deleteAfterRun: false,
|
|
200
|
-
};
|
|
201
|
-
this.heartbeatJobId = job.id;
|
|
202
|
-
this.store.jobs.push(job);
|
|
203
|
-
logger.debug({ intervalMs: this.heartbeatIntervalMs }, 'Heartbeat job added');
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
private recomputeNextRuns(): void {
|
|
207
|
-
if (!this.store) return;
|
|
208
|
-
const now = nowMs();
|
|
209
|
-
for (const job of this.store.jobs) {
|
|
210
|
-
if (job.enabled) job.state.nextRunAtMs = computeNextRun(job.schedule, now);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
private getNextWakeMs(): number | undefined {
|
|
215
|
-
if (!this.store) return undefined;
|
|
216
|
-
const times = this.store.jobs
|
|
217
|
-
.filter(j => j.enabled && j.state.nextRunAtMs != null)
|
|
218
|
-
.map(j => j.state.nextRunAtMs!);
|
|
219
|
-
return times.length > 0 ? Math.min(...times) : undefined;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
private armTimer(): void {
|
|
223
|
-
if (this.timerTimeout) {
|
|
224
|
-
clearTimeout(this.timerTimeout);
|
|
225
|
-
this.timerTimeout = null;
|
|
226
|
-
}
|
|
227
|
-
const nextWake = this.getNextWakeMs();
|
|
228
|
-
if (nextWake == null || !this._running) return;
|
|
229
|
-
const delayMs = Math.max(0, nextWake - nowMs());
|
|
230
|
-
this.timerTimeout = setTimeout(async () => {
|
|
231
|
-
if (this._running) await this.onTimer();
|
|
232
|
-
}, delayMs);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
private async onTimer(): Promise<void> {
|
|
236
|
-
if (!this.store) return;
|
|
237
|
-
const now = nowMs();
|
|
238
|
-
const dueJobs = this.store.jobs.filter(
|
|
239
|
-
j => j.enabled && j.state.nextRunAtMs != null && now >= j.state.nextRunAtMs!
|
|
240
|
-
);
|
|
241
|
-
for (const job of dueJobs) await this.executeJob(job);
|
|
242
|
-
this.saveStore();
|
|
243
|
-
this.armTimer();
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
private async executeJob(job: ScheduledJob): Promise<void> {
|
|
247
|
-
const startMs = nowMs();
|
|
248
|
-
if (job.payload.kind === 'heartbeat') {
|
|
249
|
-
const shouldRun = this.checkHeartbeatFile();
|
|
250
|
-
if (!shouldRun) {
|
|
251
|
-
job.state.lastStatus = 'skipped';
|
|
252
|
-
job.state.lastRunAtMs = startMs;
|
|
253
|
-
job.updatedAtMs = nowMs();
|
|
254
|
-
job.state.nextRunAtMs = computeNextRun(job.schedule, nowMs());
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
logger.info({ jobId: job.id, name: job.name }, 'Scheduler: executing job');
|
|
259
|
-
try {
|
|
260
|
-
if (this.onJob) await this.onJob(job);
|
|
261
|
-
job.state.lastStatus = 'ok';
|
|
262
|
-
job.state.lastError = undefined;
|
|
263
|
-
logger.info({ jobId: job.id, name: job.name }, 'Scheduler: job completed');
|
|
264
|
-
} catch (error) {
|
|
265
|
-
job.state.lastStatus = 'error';
|
|
266
|
-
job.state.lastError = String(error);
|
|
267
|
-
logger.error({ jobId: job.id, name: job.name, lastError: String(error) }, 'Scheduler: job failed');
|
|
268
|
-
}
|
|
269
|
-
job.state.lastRunAtMs = startMs;
|
|
270
|
-
job.updatedAtMs = nowMs();
|
|
271
|
-
if (job.schedule.kind === 'at') {
|
|
272
|
-
if (job.deleteAfterRun && this.store) {
|
|
273
|
-
this.store.jobs = this.store.jobs.filter(j => j.id !== job.id);
|
|
274
|
-
} else {
|
|
275
|
-
job.enabled = false;
|
|
276
|
-
job.state.nextRunAtMs = undefined;
|
|
277
|
-
}
|
|
278
|
-
} else {
|
|
279
|
-
job.state.nextRunAtMs = computeNextRun(job.schedule, nowMs());
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
private checkHeartbeatFile(): boolean {
|
|
284
|
-
const heartbeatPath = path.join(this.workspace, 'HEARTBEAT.md');
|
|
285
|
-
if (!fs.existsSync(heartbeatPath)) return false;
|
|
286
|
-
try {
|
|
287
|
-
const content = fs.readFileSync(heartbeatPath, 'utf-8');
|
|
288
|
-
return !isHeartbeatEmpty(content);
|
|
289
|
-
} catch {
|
|
290
|
-
return false;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
listJobs(): ScheduledJob[] {
|
|
295
|
-
const store = this.loadStore();
|
|
296
|
-
return store.jobs
|
|
297
|
-
.filter(j => j.id !== this.heartbeatJobId)
|
|
298
|
-
.sort((a, b) => (a.state.nextRunAtMs ?? Infinity) - (b.state.nextRunAtMs ?? Infinity));
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
addJob(options: AddJobOptions): ScheduledJob {
|
|
302
|
-
const store = this.loadStore();
|
|
303
|
-
const now = nowMs();
|
|
304
|
-
const job: ScheduledJob = {
|
|
305
|
-
id: randomUUID().slice(0, 8),
|
|
306
|
-
name: options.name,
|
|
307
|
-
enabled: options.enabled ?? true,
|
|
308
|
-
schedule: options.schedule,
|
|
309
|
-
payload: options.payload,
|
|
310
|
-
state: { nextRunAtMs: computeNextRun(options.schedule, now) },
|
|
311
|
-
createdAtMs: now,
|
|
312
|
-
updatedAtMs: now,
|
|
313
|
-
deleteAfterRun: options.deleteAfterRun ?? false,
|
|
314
|
-
};
|
|
315
|
-
store.jobs.push(job);
|
|
316
|
-
this.saveStore();
|
|
317
|
-
this.armTimer();
|
|
318
|
-
logger.info({ jobId: job.id, name: job.name }, 'Scheduler: added job');
|
|
319
|
-
return job;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
removeJob(jobId: string): boolean {
|
|
323
|
-
const store = this.loadStore();
|
|
324
|
-
const before = store.jobs.length;
|
|
325
|
-
store.jobs = store.jobs.filter(j => j.id !== jobId);
|
|
326
|
-
const removed = store.jobs.length < before;
|
|
327
|
-
if (removed) {
|
|
328
|
-
this.saveStore();
|
|
329
|
-
this.armTimer();
|
|
330
|
-
logger.info({ jobId }, 'Scheduler: removed job');
|
|
331
|
-
}
|
|
332
|
-
return removed;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
enableJob(jobId: string, enabled: boolean = true): boolean {
|
|
336
|
-
const store = this.loadStore();
|
|
337
|
-
const job = store.jobs.find(j => j.id === jobId);
|
|
338
|
-
if (!job) return false;
|
|
339
|
-
job.enabled = enabled;
|
|
340
|
-
job.updatedAtMs = nowMs();
|
|
341
|
-
job.state.nextRunAtMs = enabled ? computeNextRun(job.schedule, nowMs()) : undefined;
|
|
342
|
-
this.saveStore();
|
|
343
|
-
this.armTimer();
|
|
344
|
-
return true;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
async runJob(jobId: string): Promise<void> {
|
|
348
|
-
const store = this.loadStore();
|
|
349
|
-
const job = store.jobs.find(j => j.id === jobId);
|
|
350
|
-
if (job) {
|
|
351
|
-
await this.executeJob(job);
|
|
352
|
-
this.saveStore();
|
|
353
|
-
this.armTimer();
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
status(): { running: boolean; jobCount: number; nextWakeAt?: number } {
|
|
358
|
-
const store = this.loadStore();
|
|
359
|
-
return {
|
|
360
|
-
running: this._running,
|
|
361
|
-
jobCount: store.jobs.filter(j => j.id !== this.heartbeatJobId).length,
|
|
362
|
-
nextWakeAt: this.getNextWakeMs(),
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
async triggerHeartbeat(): Promise<void> {
|
|
367
|
-
if (this.heartbeatJobId && this.store) {
|
|
368
|
-
const job = this.store.jobs.find(j => j.id === this.heartbeatJobId);
|
|
369
|
-
if (job && this.onJob) await this.onJob(job);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}
|
package/src/scheduler/types.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scheduler types
|
|
3
|
-
*
|
|
4
|
-
* 支持三种调度:at(单次指定时间)、every(固定间隔)、cron(表达式)
|
|
5
|
-
* Payload 支持 agent_turn(到点执行 prompt)、heartbeat(读 HEARTBEAT.md)、system_event
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export interface Schedule {
|
|
9
|
-
kind: 'at' | 'every' | 'cron';
|
|
10
|
-
/** 单次执行时间戳(kind=at) */
|
|
11
|
-
atMs?: number;
|
|
12
|
-
/** 间隔毫秒(kind=every) */
|
|
13
|
-
everyMs?: number;
|
|
14
|
-
/** Cron 表达式(kind=cron) */
|
|
15
|
-
expr?: string;
|
|
16
|
-
/** 时区(kind=cron 可选) */
|
|
17
|
-
tz?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface JobPayload {
|
|
21
|
-
kind: 'system_event' | 'agent_turn' | 'heartbeat';
|
|
22
|
-
/** 触发时发给 AI 的 prompt(agent_turn)或 heartbeat 说明 */
|
|
23
|
-
message: string;
|
|
24
|
-
/** 是否投递到指定 channel/user */
|
|
25
|
-
deliver: boolean;
|
|
26
|
-
channel?: string;
|
|
27
|
-
to?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface JobState {
|
|
31
|
-
nextRunAtMs?: number;
|
|
32
|
-
lastRunAtMs?: number;
|
|
33
|
-
lastStatus?: 'ok' | 'error' | 'skipped';
|
|
34
|
-
lastError?: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface ScheduledJob {
|
|
38
|
-
id: string;
|
|
39
|
-
name: string;
|
|
40
|
-
enabled: boolean;
|
|
41
|
-
schedule: Schedule;
|
|
42
|
-
payload: JobPayload;
|
|
43
|
-
state: JobState;
|
|
44
|
-
createdAtMs: number;
|
|
45
|
-
updatedAtMs: number;
|
|
46
|
-
/** 单次任务执行后是否删除 */
|
|
47
|
-
deleteAfterRun: boolean;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface JobStore {
|
|
51
|
-
version: number;
|
|
52
|
-
jobs: ScheduledJob[];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export type JobCallback = (job: ScheduledJob) => Promise<void>;
|
|
56
|
-
|
|
57
|
-
export interface AddJobOptions {
|
|
58
|
-
name: string;
|
|
59
|
-
schedule: Schedule;
|
|
60
|
-
payload: JobPayload;
|
|
61
|
-
enabled?: boolean;
|
|
62
|
-
deleteAfterRun?: boolean;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface IScheduler {
|
|
66
|
-
start(): Promise<void>;
|
|
67
|
-
stop(): void;
|
|
68
|
-
addJob(options: AddJobOptions): ScheduledJob;
|
|
69
|
-
removeJob(jobId: string): boolean;
|
|
70
|
-
enableJob(jobId: string, enabled: boolean): boolean;
|
|
71
|
-
runJob(jobId: string): Promise<void>;
|
|
72
|
-
listJobs(): ScheduledJob[];
|
|
73
|
-
status(): { running: boolean; jobCount: number; nextWakeAt?: number };
|
|
74
|
-
}
|
package/src/tool-zod.ts
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Zod 工具适配层(可选)
|
|
3
|
-
*
|
|
4
|
-
* 使用 Zod 定义工具参数时可获得类型推断与校验。需安装 zod:
|
|
5
|
-
* pnpm add zod
|
|
6
|
-
*
|
|
7
|
-
* 用法:
|
|
8
|
-
* import { createToolFromZod } from '@zhin.js/core/tool-zod';
|
|
9
|
-
* import { z } from 'zod';
|
|
10
|
-
* const tool = createToolFromZod('my_tool', '描述', z.object({ id: z.string() }), async (args) => { ... });
|
|
11
|
-
* plugin.addTool(tool);
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type { Tool, ToolContext, ToolParametersSchema } from './types.js';
|
|
15
|
-
|
|
16
|
-
type MaybePromise<T> = T | Promise<T>;
|
|
17
|
-
|
|
18
|
-
function zodFieldToJsonSchema(z: any): Record<string, unknown> {
|
|
19
|
-
if (!z || !z._def) return { type: 'string' };
|
|
20
|
-
const def = z._def;
|
|
21
|
-
const typeName = def.typeName;
|
|
22
|
-
|
|
23
|
-
if (typeName === 'ZodOptional' || typeName === 'ZodDefault') {
|
|
24
|
-
const inner = def.innerType ?? def.type;
|
|
25
|
-
return zodFieldToJsonSchema(inner);
|
|
26
|
-
}
|
|
27
|
-
if (typeName === 'ZodString') {
|
|
28
|
-
const out: Record<string, unknown> = { type: 'string' };
|
|
29
|
-
if (def.description) out.description = def.description;
|
|
30
|
-
return out;
|
|
31
|
-
}
|
|
32
|
-
if (typeName === 'ZodNumber') {
|
|
33
|
-
const out: Record<string, unknown> = { type: 'number' };
|
|
34
|
-
if (def.description) out.description = def.description;
|
|
35
|
-
return out;
|
|
36
|
-
}
|
|
37
|
-
if (typeName === 'ZodBoolean') {
|
|
38
|
-
const out: Record<string, unknown> = { type: 'boolean' };
|
|
39
|
-
if (def.description) out.description = def.description;
|
|
40
|
-
return out;
|
|
41
|
-
}
|
|
42
|
-
if (typeName === 'ZodEnum') {
|
|
43
|
-
return { type: 'string', enum: def.values };
|
|
44
|
-
}
|
|
45
|
-
if (typeName === 'ZodArray') {
|
|
46
|
-
return { type: 'array', items: zodFieldToJsonSchema(def.type ?? def.element) };
|
|
47
|
-
}
|
|
48
|
-
return { type: 'string' };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function zodToJsonSchema(schema: any): ToolParametersSchema {
|
|
52
|
-
const result: ToolParametersSchema = {
|
|
53
|
-
type: 'object',
|
|
54
|
-
properties: {} as ToolParametersSchema['properties'],
|
|
55
|
-
required: [],
|
|
56
|
-
};
|
|
57
|
-
if (!schema || !schema.shape) return result;
|
|
58
|
-
const shape = schema.shape;
|
|
59
|
-
const properties = result.properties as Record<string, any>;
|
|
60
|
-
const required: string[] = [];
|
|
61
|
-
for (const [key, value] of Object.entries(shape)) {
|
|
62
|
-
const zodValue = value as any;
|
|
63
|
-
properties[key] = zodFieldToJsonSchema(zodValue);
|
|
64
|
-
const typeName = zodValue?._def?.typeName;
|
|
65
|
-
if (typeName !== 'ZodOptional' && typeName !== 'ZodDefault') {
|
|
66
|
-
required.push(key);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
result.required = required.length > 0 ? required : undefined;
|
|
70
|
-
return result;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export interface CreateToolFromZodOptions {
|
|
74
|
-
tags?: string[];
|
|
75
|
-
keywords?: string[];
|
|
76
|
-
source?: string;
|
|
77
|
-
hidden?: boolean;
|
|
78
|
-
kind?: string;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* 从 Zod 模式创建 Tool,便于类型安全与校验。
|
|
83
|
-
* 需要安装 zod:pnpm add zod。传入的 schema 应为 z.object({ ... })。
|
|
84
|
-
*/
|
|
85
|
-
export function createToolFromZod<T extends Record<string, any>>(
|
|
86
|
-
name: string,
|
|
87
|
-
description: string,
|
|
88
|
-
schema: any,
|
|
89
|
-
execute: (args: T, context?: ToolContext) => MaybePromise<any>,
|
|
90
|
-
options?: CreateToolFromZodOptions
|
|
91
|
-
): Tool {
|
|
92
|
-
if (!schema?.safeParse) {
|
|
93
|
-
throw new Error('createToolFromZod: schema must be a Zod object schema (e.g. z.object({ ... })). Install zod: pnpm add zod');
|
|
94
|
-
}
|
|
95
|
-
const parameters = zodToJsonSchema(schema);
|
|
96
|
-
return {
|
|
97
|
-
name,
|
|
98
|
-
description,
|
|
99
|
-
parameters,
|
|
100
|
-
execute: async (args: Record<string, any>, context?: ToolContext) => {
|
|
101
|
-
const parsed = schema.safeParse(args);
|
|
102
|
-
if (!parsed.success) {
|
|
103
|
-
const msg = parsed.error.errors?.map((e: any) => `${e.path?.join('.') ?? 'root'}: ${e.message}`).join('; ') ?? 'Invalid arguments';
|
|
104
|
-
return `Error: ${msg}`;
|
|
105
|
-
}
|
|
106
|
-
return execute(parsed.data as T, context);
|
|
107
|
-
},
|
|
108
|
-
tags: options?.tags,
|
|
109
|
-
keywords: options?.keywords,
|
|
110
|
-
source: options?.source,
|
|
111
|
-
hidden: options?.hidden,
|
|
112
|
-
kind: options?.kind,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
package/src/types-generator.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { getLogger } from '@zhin.js/logger';
|
|
4
|
-
|
|
5
|
-
const logger = getLogger('TypesGenerator');
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* 更新 tsconfig.json 的类型声明
|
|
9
|
-
* @param cwd 项目根目录
|
|
10
|
-
*/
|
|
11
|
-
export async function generateEnvTypes(cwd: string): Promise<void> {
|
|
12
|
-
try {
|
|
13
|
-
// 基础类型集合
|
|
14
|
-
const types = new Set(['@types/node']);
|
|
15
|
-
|
|
16
|
-
// 检查 package.json 中的依赖
|
|
17
|
-
const pkgPath = path.join(cwd, 'package.json');
|
|
18
|
-
if (fs.existsSync(pkgPath)) {
|
|
19
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
20
|
-
const allDeps = {
|
|
21
|
-
...(pkg.dependencies || {}),
|
|
22
|
-
...(pkg.devDependencies || {})
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
// 检查所有 @zhin.js/ 开头的包
|
|
26
|
-
for (const [name] of Object.entries(allDeps)) {
|
|
27
|
-
if (name.startsWith('@zhin.js/') || name === 'zhin.js') {
|
|
28
|
-
try {
|
|
29
|
-
const depPkgPath = path.join(cwd, 'node_modules', name, 'package.json');
|
|
30
|
-
if (fs.existsSync(depPkgPath)) {
|
|
31
|
-
const depPkg = JSON.parse(fs.readFileSync(depPkgPath, 'utf-8'));
|
|
32
|
-
if (depPkg.types || depPkg.typings) {
|
|
33
|
-
types.add(name);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
} catch (err) {
|
|
37
|
-
// 如果读取失败,跳过这个包
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// 更新或创建 tsconfig.json
|
|
45
|
-
const tsconfigPath = path.join(cwd, 'tsconfig.json');
|
|
46
|
-
let tsconfig:Record<string,any> = {};
|
|
47
|
-
|
|
48
|
-
// 读取现有的 tsconfig.json
|
|
49
|
-
if (fs.existsSync(tsconfigPath)) {
|
|
50
|
-
try {
|
|
51
|
-
tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, 'utf-8'));
|
|
52
|
-
} catch (err) {
|
|
53
|
-
// console.error 已替换为注释
|
|
54
|
-
logger.warn('⚠️ Failed to parse tsconfig.json, creating new one');
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// 确保 compilerOptions 存在
|
|
59
|
-
if (!tsconfig.compilerOptions) {
|
|
60
|
-
tsconfig.compilerOptions = {};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// 合并现有的 types
|
|
64
|
-
const existingTypes = tsconfig.compilerOptions.types || [];
|
|
65
|
-
const allTypes = new Set([...existingTypes, ...types]);
|
|
66
|
-
|
|
67
|
-
// 更新 types 字段
|
|
68
|
-
tsconfig.compilerOptions.types = Array.from(allTypes);
|
|
69
|
-
|
|
70
|
-
// 写入文件
|
|
71
|
-
fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2), 'utf-8');
|
|
72
|
-
logger.info('✅ Updated TypeScript types configuration');
|
|
73
|
-
} catch (error) {
|
|
74
|
-
logger.warn('⚠️ Failed to update TypeScript types', {
|
|
75
|
-
error: error instanceof Error ? error.message : String(error)
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
}
|