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.
- package/README.md +211 -0
- package/bin/xshat.mjs +36 -0
- package/package.json +60 -0
- package/src/config/default.mjs +620 -0
- package/src/index.mjs +2865 -0
- package/src/modules/agent-executor.mjs +1184 -0
- package/src/modules/agent-helpers.mjs +132 -0
- package/src/modules/agent-loop.mjs +181 -0
- package/src/modules/agent.mjs +152 -0
- package/src/modules/api-chat.mjs +212 -0
- package/src/modules/browser.mjs +1384 -0
- package/src/modules/chat-helpers.mjs +275 -0
- package/src/modules/chat.mjs +589 -0
- package/src/modules/docs.mjs +88 -0
- package/src/modules/local-intent-router.mjs +280 -0
- package/src/modules/logger.mjs +111 -0
- package/src/modules/multi-agent.mjs +183 -0
- package/src/modules/navigation-recovery.mjs +35 -0
- package/src/modules/provider-debug.mjs +98 -0
- package/src/modules/provider-pool.mjs +66 -0
- package/src/modules/provider-runtime.mjs +26 -0
- package/src/modules/provider-strategies.mjs +102 -0
- package/src/modules/response-parser.mjs +511 -0
- package/src/modules/response-pipeline.mjs +211 -0
- package/src/modules/runtime-maintenance.mjs +195 -0
- package/src/modules/session-browser.mjs +180 -0
- package/src/modules/session.mjs +370 -0
- package/src/modules/settings-menu.mjs +367 -0
- package/src/modules/task-runner.mjs +511 -0
- package/src/modules/task-store.mjs +262 -0
- package/src/modules/ui.mjs +2204 -0
- package/src/utils/config.mjs +257 -0
- package/src/utils/key-input.mjs +86 -0
- package/src/utils/storage.mjs +55 -0
|
@@ -0,0 +1,370 @@
|
|
|
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
|
+
export class SessionManager {
|
|
11
|
+
constructor({
|
|
12
|
+
dir = config.session.dir,
|
|
13
|
+
fsModule = fs,
|
|
14
|
+
clock = () => new Date()
|
|
15
|
+
} = {}) {
|
|
16
|
+
this.dir = dir;
|
|
17
|
+
this.fs = fsModule;
|
|
18
|
+
this.clock = clock;
|
|
19
|
+
this.current = null;
|
|
20
|
+
this.indexPath = resolveSiblingPath(this.dir, '_index.json');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async init() {
|
|
24
|
+
try {
|
|
25
|
+
await this.fs.mkdir(this.dir, { recursive: true });
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error('Failed to create session directory:', error.message);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_nowIso() {
|
|
32
|
+
return this.clock().toISOString();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_path(id) {
|
|
36
|
+
return resolve(this.dir, `${id}.json`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_toMeta(data) {
|
|
40
|
+
if (!data) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
id: data.id,
|
|
46
|
+
provider: data.provider,
|
|
47
|
+
providerHistory: Array.isArray(data.providerHistory) ? data.providerHistory : [],
|
|
48
|
+
title: data.title || '(无标题)',
|
|
49
|
+
url: data.url,
|
|
50
|
+
createdAt: data.createdAt,
|
|
51
|
+
updatedAt: data.updatedAt,
|
|
52
|
+
messageCount: data.messages?.length || 0
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async _loadIndex() {
|
|
57
|
+
const index = await readJsonSafe(this.indexPath, []);
|
|
58
|
+
return Array.isArray(index) ? index : [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async _saveIndex(entries = []) {
|
|
62
|
+
const normalized = entries
|
|
63
|
+
.filter(Boolean)
|
|
64
|
+
.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
|
|
65
|
+
await writeJsonAtomic(this.indexPath, normalized);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async _upsertIndexEntry(sessionLike) {
|
|
69
|
+
const nextMeta = this._toMeta(sessionLike);
|
|
70
|
+
if (!nextMeta?.id) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const currentIndex = await this._loadIndex();
|
|
75
|
+
const filtered = currentIndex.filter((item) => item?.id !== nextMeta.id);
|
|
76
|
+
filtered.push(nextMeta);
|
|
77
|
+
await this._saveIndex(filtered);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async _removeIndexEntry(id) {
|
|
81
|
+
const currentIndex = await this._loadIndex();
|
|
82
|
+
await this._saveIndex(currentIndex.filter((item) => item?.id !== id));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
create(provider) {
|
|
86
|
+
const now = this._nowIso();
|
|
87
|
+
this.current = {
|
|
88
|
+
id: `${provider.id}_${Date.now()}`,
|
|
89
|
+
provider: provider.id,
|
|
90
|
+
providerHistory: [
|
|
91
|
+
{
|
|
92
|
+
provider: provider.id,
|
|
93
|
+
reason: 'initial',
|
|
94
|
+
at: now
|
|
95
|
+
}
|
|
96
|
+
],
|
|
97
|
+
title: '',
|
|
98
|
+
url: '',
|
|
99
|
+
createdAt: now,
|
|
100
|
+
updatedAt: now,
|
|
101
|
+
messages: [],
|
|
102
|
+
memory: {
|
|
103
|
+
pinned: [],
|
|
104
|
+
summary: '',
|
|
105
|
+
plan: [],
|
|
106
|
+
lastUserMessage: '',
|
|
107
|
+
lastAssistantMessage: ''
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
return this.current;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
setUrl(url) {
|
|
114
|
+
if (this.current && url) {
|
|
115
|
+
this.current.url = url;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async switchProvider(provider, {
|
|
120
|
+
reason = 'manual'
|
|
121
|
+
} = {}) {
|
|
122
|
+
if (!this.current || !provider?.id) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const now = this._nowIso();
|
|
127
|
+
this.current.provider = provider.id;
|
|
128
|
+
if (!Array.isArray(this.current.providerHistory)) {
|
|
129
|
+
this.current.providerHistory = [];
|
|
130
|
+
}
|
|
131
|
+
this.current.providerHistory.push({
|
|
132
|
+
provider: provider.id,
|
|
133
|
+
reason,
|
|
134
|
+
at: now
|
|
135
|
+
});
|
|
136
|
+
this.current.updatedAt = now;
|
|
137
|
+
await this._save();
|
|
138
|
+
return this.current;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async addMessage(role, content) {
|
|
142
|
+
if (!this.current) return;
|
|
143
|
+
|
|
144
|
+
this.current.messages.push({
|
|
145
|
+
role,
|
|
146
|
+
content,
|
|
147
|
+
time: this._nowIso()
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (!this.current.title && role === 'user') {
|
|
151
|
+
this.current.title = content.slice(0, 40);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!this.current.memory || typeof this.current.memory !== 'object') {
|
|
155
|
+
this.current.memory = {
|
|
156
|
+
pinned: [],
|
|
157
|
+
summary: '',
|
|
158
|
+
plan: [],
|
|
159
|
+
lastUserMessage: '',
|
|
160
|
+
lastAssistantMessage: ''
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (role === 'user') {
|
|
165
|
+
this.current.memory.lastUserMessage = String(content ?? '').trim();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (role === 'assistant') {
|
|
169
|
+
this.current.memory.lastAssistantMessage = String(content ?? '').trim();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.current.updatedAt = this._nowIso();
|
|
173
|
+
await this._save();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async updateMemory(patch = {}) {
|
|
177
|
+
if (!this.current) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const currentMemory = this.current.memory && typeof this.current.memory === 'object'
|
|
182
|
+
? this.current.memory
|
|
183
|
+
: {
|
|
184
|
+
pinned: [],
|
|
185
|
+
summary: '',
|
|
186
|
+
plan: [],
|
|
187
|
+
lastUserMessage: '',
|
|
188
|
+
lastAssistantMessage: ''
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
this.current.memory = {
|
|
192
|
+
...currentMemory,
|
|
193
|
+
...patch,
|
|
194
|
+
pinned: Array.isArray(patch.pinned ?? currentMemory.pinned)
|
|
195
|
+
? (patch.pinned ?? currentMemory.pinned)
|
|
196
|
+
: [],
|
|
197
|
+
plan: Array.isArray(patch.plan ?? currentMemory.plan)
|
|
198
|
+
? (patch.plan ?? currentMemory.plan)
|
|
199
|
+
: []
|
|
200
|
+
};
|
|
201
|
+
this.current.updatedAt = this._nowIso();
|
|
202
|
+
await this._save();
|
|
203
|
+
return this.current.memory;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async pinMemory(note) {
|
|
207
|
+
const text = String(note ?? '').trim();
|
|
208
|
+
if (!this.current || !text) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const memory = this.current.memory && typeof this.current.memory === 'object'
|
|
213
|
+
? this.current.memory
|
|
214
|
+
: {};
|
|
215
|
+
const pinned = Array.isArray(memory.pinned) ? [...memory.pinned] : [];
|
|
216
|
+
pinned.unshift({
|
|
217
|
+
text,
|
|
218
|
+
at: this._nowIso()
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return this.updateMemory({
|
|
222
|
+
...memory,
|
|
223
|
+
pinned: pinned.slice(0, 12)
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async _save() {
|
|
228
|
+
if (!this.current) return;
|
|
229
|
+
try {
|
|
230
|
+
await writeJsonAtomic(this._path(this.current.id), this.current);
|
|
231
|
+
await this._upsertIndexEntry(this.current);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
console.error('Failed to save session:', error.message);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async list() {
|
|
238
|
+
try {
|
|
239
|
+
const indexed = await this._loadIndex();
|
|
240
|
+
if (indexed.length > 0) {
|
|
241
|
+
return indexed;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const files = await this.fs.readdir(this.dir);
|
|
245
|
+
const sessions = await Promise.all(
|
|
246
|
+
files
|
|
247
|
+
.filter((file) => file.endsWith('.json') && file !== '_index.json')
|
|
248
|
+
.map((file) => this._readMeta(file))
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const normalized = sessions
|
|
252
|
+
.filter(Boolean)
|
|
253
|
+
.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
|
|
254
|
+
await this._saveIndex(normalized);
|
|
255
|
+
return normalized;
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.error('Failed to list sessions:', error.message);
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async _readMeta(file) {
|
|
263
|
+
try {
|
|
264
|
+
const raw = await this.fs.readFile(resolve(this.dir, file), 'utf-8');
|
|
265
|
+
const data = JSON.parse(raw);
|
|
266
|
+
return this._toMeta(data);
|
|
267
|
+
} catch {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async search(keyword) {
|
|
273
|
+
const all = await this.list();
|
|
274
|
+
if (!keyword) return all;
|
|
275
|
+
|
|
276
|
+
const lower = keyword.toLowerCase();
|
|
277
|
+
const matched = [];
|
|
278
|
+
|
|
279
|
+
for (const meta of all) {
|
|
280
|
+
if (meta.title.toLowerCase().includes(lower)) {
|
|
281
|
+
matched.push(meta);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const full = await this.load(meta.id);
|
|
285
|
+
if (full?.messages.some(m => m.content.toLowerCase().includes(lower))) {
|
|
286
|
+
matched.push(meta);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return matched;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async load(id) {
|
|
294
|
+
try {
|
|
295
|
+
const raw = await this.fs.readFile(this._path(id), 'utf-8');
|
|
296
|
+
return JSON.parse(raw);
|
|
297
|
+
} catch {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async resume(id) {
|
|
303
|
+
const data = await this.load(id);
|
|
304
|
+
if (data) {
|
|
305
|
+
if (!Array.isArray(data.providerHistory)) {
|
|
306
|
+
data.providerHistory = data.provider
|
|
307
|
+
? [{
|
|
308
|
+
provider: data.provider,
|
|
309
|
+
reason: 'legacy',
|
|
310
|
+
at: data.createdAt || data.updatedAt || this._nowIso()
|
|
311
|
+
}]
|
|
312
|
+
: [];
|
|
313
|
+
}
|
|
314
|
+
if (!data.memory || typeof data.memory !== 'object') {
|
|
315
|
+
data.memory = {
|
|
316
|
+
pinned: [],
|
|
317
|
+
summary: '',
|
|
318
|
+
plan: [],
|
|
319
|
+
lastUserMessage: '',
|
|
320
|
+
lastAssistantMessage: ''
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
this.current = data;
|
|
324
|
+
}
|
|
325
|
+
return data;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async rename(id, title) {
|
|
329
|
+
const nextTitle = String(title ?? '').trim();
|
|
330
|
+
if (!nextTitle) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const session = await this.load(id);
|
|
335
|
+
if (!session) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
session.title = nextTitle;
|
|
340
|
+
session.updatedAt = this._nowIso();
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
await writeJsonAtomic(this._path(id), session);
|
|
344
|
+
await this._upsertIndexEntry(session);
|
|
345
|
+
if (this.current?.id === id) {
|
|
346
|
+
this.current = session;
|
|
347
|
+
}
|
|
348
|
+
return true;
|
|
349
|
+
} catch (error) {
|
|
350
|
+
console.error('Failed to rename session:', error.message);
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async delete(id) {
|
|
356
|
+
try {
|
|
357
|
+
await this.fs.unlink(this._path(id));
|
|
358
|
+
await this._removeIndexEntry(id);
|
|
359
|
+
if (this.current?.id === id) {
|
|
360
|
+
this.current = null;
|
|
361
|
+
}
|
|
362
|
+
return true;
|
|
363
|
+
} catch (error) {
|
|
364
|
+
console.error('Failed to delete session:', error.message);
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export default new SessionManager();
|