xshat-lite 1.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.
@@ -0,0 +1,262 @@
1
+ import fs from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import config from '../utils/config.mjs';
4
+ import {
5
+ readJsonSafe,
6
+ resolveSiblingPath,
7
+ writeJsonAtomic
8
+ } from '../utils/storage.mjs';
9
+
10
+ function nowIso(clock = () => new Date()) {
11
+ return clock().toISOString();
12
+ }
13
+
14
+ function summarizeText(text, maxChars = 120) {
15
+ const value = String(text ?? '').replace(/\s+/g, ' ').trim();
16
+ if (value.length <= maxChars) {
17
+ return value;
18
+ }
19
+ return `${value.slice(0, Math.max(0, maxChars - 1))}…`;
20
+ }
21
+
22
+ export class TaskStore {
23
+ constructor({
24
+ dir = config.tasks.dir,
25
+ maxHistory = config.tasks.maxHistory,
26
+ maxMessages = config.tasks.maxMessages,
27
+ fsModule = fs,
28
+ clock = () => new Date()
29
+ } = {}) {
30
+ this.dir = dir;
31
+ this.maxHistory = maxHistory;
32
+ this.maxMessages = maxMessages;
33
+ this.fs = fsModule;
34
+ this.clock = clock;
35
+ this.indexPath = resolveSiblingPath(this.dir, '_index.json');
36
+ }
37
+
38
+ async init() {
39
+ await this.fs.mkdir(this.dir, { recursive: true });
40
+ }
41
+
42
+ taskPath(id) {
43
+ return resolve(this.dir, `${id}.json`);
44
+ }
45
+
46
+ toMeta(task) {
47
+ if (!task) {
48
+ return null;
49
+ }
50
+
51
+ return {
52
+ id: task.id,
53
+ title: task.title,
54
+ goal: summarizeText(task.goal, 80),
55
+ provider: task.provider,
56
+ mode: task.mode,
57
+ intervalMs: task.intervalMs ?? null,
58
+ status: task.status,
59
+ createdAt: task.createdAt,
60
+ updatedAt: task.updatedAt,
61
+ lastRunAt: task.lastRunAt,
62
+ lastCompletedAt: task.lastCompletedAt || null,
63
+ nextRunAt: task.nextRunAt,
64
+ runCount: task.runCount || 0,
65
+ retryCount: task.retryCount || 0,
66
+ retryLimit: task.retryLimit || 0,
67
+ stopRequested: Boolean(task.stopRequested),
68
+ handoffUrl: task.handoffUrl || null,
69
+ lastError: task.lastError
70
+ ? summarizeText(task.lastError, 100)
71
+ : null,
72
+ lastResult: task.lastResult
73
+ ? summarizeText(task.lastResult, 100)
74
+ : null
75
+ };
76
+ }
77
+
78
+ async loadIndex() {
79
+ const index = await readJsonSafe(this.indexPath, []);
80
+ return Array.isArray(index) ? index : [];
81
+ }
82
+
83
+ async saveIndex(entries = []) {
84
+ const normalized = entries
85
+ .filter(Boolean)
86
+ .sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
87
+ await writeJsonAtomic(this.indexPath, normalized);
88
+ }
89
+
90
+ async upsertIndexEntry(task) {
91
+ const meta = this.toMeta(task);
92
+ if (!meta?.id) {
93
+ return;
94
+ }
95
+
96
+ const current = await this.loadIndex();
97
+ const filtered = current.filter((item) => item?.id !== meta.id);
98
+ filtered.push(meta);
99
+ await this.saveIndex(filtered);
100
+ }
101
+
102
+ async removeIndexEntry(id) {
103
+ const current = await this.loadIndex();
104
+ await this.saveIndex(current.filter((item) => item?.id !== id));
105
+ }
106
+
107
+ create({
108
+ title,
109
+ goal,
110
+ schedule = null,
111
+ provider = null,
112
+ mode = 'once',
113
+ intervalMs = null,
114
+ retryLimit = 0,
115
+ retryDelayMs = null
116
+ }) {
117
+ const timestamp = Date.now();
118
+ const createdAt = nowIso(this.clock);
119
+ return {
120
+ id: `task_${timestamp}`,
121
+ title: String(title || goal || '未命名任务').trim().slice(0, 80),
122
+ goal: String(goal || '').trim(),
123
+ provider,
124
+ mode,
125
+ schedule,
126
+ intervalMs:
127
+ intervalMs === null || intervalMs === undefined
128
+ ? null
129
+ : Math.max(0, Number(intervalMs) || 0),
130
+ retryLimit: Math.max(0, Number(retryLimit) || 0),
131
+ retryDelayMs:
132
+ retryDelayMs === null || retryDelayMs === undefined
133
+ ? null
134
+ : Math.max(0, Number(retryDelayMs) || 0),
135
+ status: 'idle',
136
+ createdAt,
137
+ updatedAt: createdAt,
138
+ lastRunAt: null,
139
+ lastCompletedAt: null,
140
+ nextRunAt: null,
141
+ lastError: null,
142
+ lastResult: null,
143
+ handoffUrl: null,
144
+ stopRequested: false,
145
+ runCount: 0,
146
+ retryCount: 0,
147
+ history: [],
148
+ messages: []
149
+ };
150
+ }
151
+
152
+ async save(task) {
153
+ const nextTask = {
154
+ ...task,
155
+ updatedAt: nowIso(this.clock),
156
+ history: Array.isArray(task.history)
157
+ ? task.history.slice(-this.maxHistory)
158
+ : [],
159
+ messages: Array.isArray(task.messages)
160
+ ? task.messages.slice(-this.maxMessages)
161
+ : []
162
+ };
163
+
164
+ await writeJsonAtomic(this.taskPath(nextTask.id), nextTask);
165
+ await this.upsertIndexEntry(nextTask);
166
+ return nextTask;
167
+ }
168
+
169
+ async load(id) {
170
+ try {
171
+ const raw = await this.fs.readFile(this.taskPath(id), 'utf-8');
172
+ return JSON.parse(raw);
173
+ } catch {
174
+ return null;
175
+ }
176
+ }
177
+
178
+ async list() {
179
+ try {
180
+ const indexed = await this.loadIndex();
181
+ if (indexed.length > 0) {
182
+ return indexed;
183
+ }
184
+
185
+ const files = await this.fs.readdir(this.dir);
186
+ const tasks = await Promise.all(
187
+ files
188
+ .filter((file) => file.endsWith('.json') && file !== '_index.json')
189
+ .map(async (file) => {
190
+ try {
191
+ const raw = await this.fs.readFile(resolve(this.dir, file), 'utf-8');
192
+ const task = JSON.parse(raw);
193
+ return this.toMeta(task);
194
+ } catch {
195
+ return null;
196
+ }
197
+ })
198
+ );
199
+
200
+ const normalized = tasks
201
+ .filter(Boolean)
202
+ .sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
203
+ await this.saveIndex(normalized);
204
+ return normalized;
205
+ } catch {
206
+ return [];
207
+ }
208
+ }
209
+
210
+ async update(id, updater) {
211
+ const task = await this.load(id);
212
+ if (!task) {
213
+ return null;
214
+ }
215
+
216
+ const nextTask =
217
+ typeof updater === 'function'
218
+ ? updater(task) || task
219
+ : { ...task, ...(updater || {}) };
220
+
221
+ return this.save(nextTask);
222
+ }
223
+
224
+ async appendHistory(id, event) {
225
+ return this.update(id, (task) => ({
226
+ ...task,
227
+ history: [
228
+ ...(Array.isArray(task.history) ? task.history : []),
229
+ {
230
+ time: nowIso(this.clock),
231
+ ...event
232
+ }
233
+ ]
234
+ }));
235
+ }
236
+
237
+ async appendMessage(id, role, content) {
238
+ return this.update(id, (task) => ({
239
+ ...task,
240
+ messages: [
241
+ ...(Array.isArray(task.messages) ? task.messages : []),
242
+ {
243
+ role,
244
+ content: String(content ?? ''),
245
+ time: nowIso(this.clock)
246
+ }
247
+ ]
248
+ }));
249
+ }
250
+
251
+ async delete(id) {
252
+ try {
253
+ await this.fs.rm(this.taskPath(id), { force: true });
254
+ await this.removeIndexEntry(id);
255
+ return true;
256
+ } catch {
257
+ return false;
258
+ }
259
+ }
260
+ }
261
+
262
+ export default new TaskStore();