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,589 @@
|
|
|
1
|
+
import config from '../utils/config.mjs';
|
|
2
|
+
import ui from './ui.mjs';
|
|
3
|
+
import { analyzeInterventionState } from './agent-helpers.mjs';
|
|
4
|
+
import {
|
|
5
|
+
buildCompleteConfig,
|
|
6
|
+
deriveEffectiveText,
|
|
7
|
+
normalizeResult,
|
|
8
|
+
sanitizeMessage,
|
|
9
|
+
summarizeResult
|
|
10
|
+
} from './chat-helpers.mjs';
|
|
11
|
+
import {
|
|
12
|
+
getProviderInterruptedRetryLimit,
|
|
13
|
+
isProviderInterruptedText,
|
|
14
|
+
isProviderTransientText
|
|
15
|
+
} from './provider-strategies.mjs';
|
|
16
|
+
import { isStructuredResponseIncomplete } from './response-parser.mjs';
|
|
17
|
+
|
|
18
|
+
class ChatManager {
|
|
19
|
+
constructor(page, provider) {
|
|
20
|
+
this.page = page;
|
|
21
|
+
this.provider = provider;
|
|
22
|
+
this.pollTimer = null;
|
|
23
|
+
this.debug = Boolean(config.debug?.enabled);
|
|
24
|
+
this.lastDebugSnapshot = '';
|
|
25
|
+
this.responseBaseline = null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get steps() {
|
|
29
|
+
return this.provider.steps || {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
debugLog(message, details = null) {
|
|
33
|
+
if (!this.debug) return;
|
|
34
|
+
|
|
35
|
+
const time = new Date().toLocaleTimeString('zh-CN', { hour12: false });
|
|
36
|
+
const suffix = details ? ` ${JSON.stringify(details)}` : '';
|
|
37
|
+
process.stderr.write(`[DEBUG ${time}] [${this.provider.id}] ${message}${suffix}\n`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async findFirstSelector(selectors, label = 'selector') {
|
|
41
|
+
for (const selector of selectors) {
|
|
42
|
+
try {
|
|
43
|
+
const count = await this.page.locator(selector).count();
|
|
44
|
+
if (count > 0) {
|
|
45
|
+
this.debugLog(`命中${label}`, { selector, count });
|
|
46
|
+
return selector;
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
this.debugLog('选择器异常', { label, selector, error: error.message });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
this.debugLog(`未命中${label}`, { selectors });
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async sendMessage(message) {
|
|
58
|
+
const cleanMessage = sanitizeMessage(message);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
await this.waitForIdleBeforeSend();
|
|
62
|
+
await this.runPreflight();
|
|
63
|
+
this.responseBaseline = await this.captureResponseBaseline();
|
|
64
|
+
await this.typeInInput(cleanMessage, {
|
|
65
|
+
waitAfterMs: config.chat.inputTimeout
|
|
66
|
+
});
|
|
67
|
+
await this.submitInput();
|
|
68
|
+
|
|
69
|
+
return true;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
ui.printWarning(`消息注入失败: ${error.message}`);
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async waitForIdleBeforeSend() {
|
|
77
|
+
const timeoutMs = Number(config.chat.idleBeforeSendTimeout) || 15000;
|
|
78
|
+
const stableThreshold = Number(config.chat.idleStableThreshold) || 2;
|
|
79
|
+
const idleAfterCompleteMs = Number(config.chat.idleAfterCompleteMs) || 1200;
|
|
80
|
+
const startTime = Date.now();
|
|
81
|
+
let stableCount = 0;
|
|
82
|
+
let lastFingerprint = '';
|
|
83
|
+
let lastChangedAt = Date.now();
|
|
84
|
+
|
|
85
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
86
|
+
const result = this.normalizeResult(await this.getResult());
|
|
87
|
+
|
|
88
|
+
if (result.error) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const fingerprint = JSON.stringify({
|
|
93
|
+
text: result.text,
|
|
94
|
+
pending: result.pending,
|
|
95
|
+
done: result.done,
|
|
96
|
+
transient: result.transient,
|
|
97
|
+
count: result.count
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (fingerprint !== lastFingerprint) {
|
|
101
|
+
lastFingerprint = fingerprint;
|
|
102
|
+
lastChangedAt = Date.now();
|
|
103
|
+
stableCount = 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const quietForMs = Date.now() - lastChangedAt;
|
|
107
|
+
const idleCandidate =
|
|
108
|
+
!result.pending &&
|
|
109
|
+
!result.transient &&
|
|
110
|
+
(result.done || !result.text);
|
|
111
|
+
|
|
112
|
+
if (idleCandidate && quietForMs >= idleAfterCompleteMs) {
|
|
113
|
+
stableCount++;
|
|
114
|
+
if (stableCount >= stableThreshold) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
stableCount = 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
await this.page.waitForTimeout(config.chat.pollInterval);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async prepareInput(selector) {
|
|
126
|
+
const locator = this.page.locator(selector).first();
|
|
127
|
+
await locator.click({ force: true });
|
|
128
|
+
await locator.focus();
|
|
129
|
+
|
|
130
|
+
const modifier = process.platform === 'darwin' ? 'Meta' : 'Control';
|
|
131
|
+
await this.page.keyboard.press(`${modifier}+A`);
|
|
132
|
+
await this.page.keyboard.press('Backspace');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async injectText(text) {
|
|
136
|
+
const modifier = process.platform === 'darwin' ? 'Meta' : 'Control';
|
|
137
|
+
await this.page.evaluate(async (value) => {
|
|
138
|
+
await navigator.clipboard.writeText(value);
|
|
139
|
+
}, text);
|
|
140
|
+
await this.page.keyboard.press(`${modifier}+KeyV`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async focusInput() {
|
|
144
|
+
const inputSelector = await this.findFirstSelector(
|
|
145
|
+
this.steps.input?.selectors || [],
|
|
146
|
+
'输入框'
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
if (!inputSelector) {
|
|
150
|
+
throw new Error('未找到输入框');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
await this.prepareInput(inputSelector);
|
|
154
|
+
return inputSelector;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async typeInInput(text, options = {}) {
|
|
158
|
+
const inputSelector = await this.findFirstSelector(
|
|
159
|
+
this.steps.input?.selectors || [],
|
|
160
|
+
'输入框'
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
if (!inputSelector) {
|
|
164
|
+
throw new Error('未找到输入框');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
await this.prepareInput(inputSelector);
|
|
168
|
+
await this.injectText(sanitizeMessage(text));
|
|
169
|
+
|
|
170
|
+
if (options.waitAfterMs !== 0) {
|
|
171
|
+
await this.page.waitForTimeout(
|
|
172
|
+
Number(options.waitAfterMs ?? config.chat.inputTimeout) || 0
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return inputSelector;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async submitInput() {
|
|
180
|
+
const sendSelector = await this.findFirstSelector(
|
|
181
|
+
this.steps.send?.selectors || [],
|
|
182
|
+
'发送按钮'
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
if (sendSelector) {
|
|
186
|
+
await this.page.locator(sendSelector).first().click();
|
|
187
|
+
return sendSelector;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
await this.page.keyboard.press('Enter');
|
|
191
|
+
return 'Enter';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async startNewChat() {
|
|
195
|
+
const candidates = [
|
|
196
|
+
...(this.steps.newChat?.selectors || []),
|
|
197
|
+
'a[href*="/new"]',
|
|
198
|
+
'button[aria-label*="新建"]',
|
|
199
|
+
'button[aria-label*="New chat"]',
|
|
200
|
+
'button:has-text("新建对话")',
|
|
201
|
+
'button:has-text("新对话")',
|
|
202
|
+
'button:has-text("New chat")',
|
|
203
|
+
'button:has-text("New Chat")'
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
const selector = await this.findFirstSelector(candidates, '新建聊天');
|
|
207
|
+
|
|
208
|
+
if (!selector) {
|
|
209
|
+
throw new Error('未找到新建聊天入口');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
await this.page.locator(selector).first().click({
|
|
213
|
+
timeout: 5000,
|
|
214
|
+
force: true
|
|
215
|
+
});
|
|
216
|
+
await this.page.waitForTimeout(800);
|
|
217
|
+
this.responseBaseline = await this.captureResponseBaseline();
|
|
218
|
+
return selector;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async runPreflight() {
|
|
222
|
+
const preflight = this.steps.preflight || {};
|
|
223
|
+
const clickSelectors = preflight.clickSelectors || [];
|
|
224
|
+
const waitAfterMs = preflight.waitAfterMs || 500;
|
|
225
|
+
|
|
226
|
+
for (const selector of clickSelectors) {
|
|
227
|
+
try {
|
|
228
|
+
const locator = this.page.locator(selector).first();
|
|
229
|
+
const count = await this.page.locator(selector).count();
|
|
230
|
+
|
|
231
|
+
if (count === 0) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
this.debugLog('执行预处理点击', { selector, count });
|
|
236
|
+
await locator.click({ force: true, timeout: 2000 });
|
|
237
|
+
await this.page.waitForTimeout(waitAfterMs);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
this.debugLog('预处理点击失败', {
|
|
240
|
+
selector,
|
|
241
|
+
error: error.message
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
buildCompleteConfig() {
|
|
248
|
+
return buildCompleteConfig(this.steps);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async captureResponseBaseline() {
|
|
252
|
+
const complete = this.buildCompleteConfig();
|
|
253
|
+
const selector = await this.findFirstSelector(
|
|
254
|
+
complete.selectors.response,
|
|
255
|
+
'基线读取节点'
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
if (!selector) {
|
|
259
|
+
return {
|
|
260
|
+
selector: null,
|
|
261
|
+
count: 0,
|
|
262
|
+
text: ''
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const locator = this.page.locator(selector);
|
|
267
|
+
const count = await locator.count();
|
|
268
|
+
const text =
|
|
269
|
+
count > 0 ? (await locator.nth(count - 1).textContent()) || '' : '';
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
selector,
|
|
273
|
+
count,
|
|
274
|
+
text
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
normalizeResult(result) {
|
|
279
|
+
const normalized = normalizeResult(result);
|
|
280
|
+
if (!normalized.transient && isProviderTransientText(this.provider.id, normalized.text)) {
|
|
281
|
+
return {
|
|
282
|
+
...normalized,
|
|
283
|
+
transient: true
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
return normalized;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async runCompletionScript(script, selectors) {
|
|
290
|
+
const fn = new Function(`
|
|
291
|
+
return ({ selectors }) => {
|
|
292
|
+
const runner = (${script.trim()});
|
|
293
|
+
const result = runner({ selectors }) || {};
|
|
294
|
+
|
|
295
|
+
if (!Number.isFinite(result.count)) {
|
|
296
|
+
let count = 0;
|
|
297
|
+
|
|
298
|
+
for (const selector of selectors.response || []) {
|
|
299
|
+
try {
|
|
300
|
+
const nodes = document.querySelectorAll(selector);
|
|
301
|
+
if (nodes.length > 0) {
|
|
302
|
+
count = nodes.length;
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
} catch {
|
|
306
|
+
// ignore invalid selectors and keep trying candidates
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
result.count = count;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return result;
|
|
314
|
+
};
|
|
315
|
+
`)();
|
|
316
|
+
const result = await this.page.evaluate(fn, { selectors });
|
|
317
|
+
return this.normalizeResult(result || null);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async getResult() {
|
|
321
|
+
try {
|
|
322
|
+
const complete = this.buildCompleteConfig();
|
|
323
|
+
|
|
324
|
+
if (complete.mode === 'script' && complete.script) {
|
|
325
|
+
return await this.runCompletionScript(complete.script, complete.selectors);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const responseSelector = await this.findFirstSelector(
|
|
329
|
+
complete.selectors.response,
|
|
330
|
+
'读取节点'
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
if (!responseSelector) {
|
|
334
|
+
return this.normalizeResult(null);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const locator = this.page.locator(responseSelector);
|
|
338
|
+
const count = await locator.count();
|
|
339
|
+
const text = count > 0 ? await locator.nth(count - 1).textContent() : '';
|
|
340
|
+
return this.normalizeResult({
|
|
341
|
+
text: text || '',
|
|
342
|
+
pending: false,
|
|
343
|
+
done: !!text,
|
|
344
|
+
count
|
|
345
|
+
});
|
|
346
|
+
} catch (error) {
|
|
347
|
+
return this.normalizeResult({ error: error.message });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
summarizeResult(result) {
|
|
352
|
+
return summarizeResult(result);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async inspectSite() {
|
|
356
|
+
const inputSelector = await this.findFirstSelector(
|
|
357
|
+
this.steps.input?.selectors || [],
|
|
358
|
+
'输入框'
|
|
359
|
+
);
|
|
360
|
+
const sendSelector = await this.findFirstSelector(
|
|
361
|
+
this.steps.send?.selectors || [],
|
|
362
|
+
'发送按钮'
|
|
363
|
+
);
|
|
364
|
+
const readSelector = await this.findFirstSelector(
|
|
365
|
+
this.steps.read?.selectors || [],
|
|
366
|
+
'读取节点'
|
|
367
|
+
);
|
|
368
|
+
const complete = this.buildCompleteConfig();
|
|
369
|
+
const result = await this.getResult();
|
|
370
|
+
const intervention = await this.inspectInterventionState({
|
|
371
|
+
inputSelector,
|
|
372
|
+
sendSelector
|
|
373
|
+
});
|
|
374
|
+
const observation = await this.inspectPageObservation();
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
provider: this.provider.id,
|
|
378
|
+
inputSelector,
|
|
379
|
+
sendSelector,
|
|
380
|
+
readSelector,
|
|
381
|
+
completionMode: complete.mode,
|
|
382
|
+
result: this.summarizeResult(result),
|
|
383
|
+
intervention,
|
|
384
|
+
observation
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async inspectPageObservation() {
|
|
389
|
+
const browserManager = this.page?.__xshatBrowserManager;
|
|
390
|
+
const channelName = this.page?.__xshatChannelName || 'chat';
|
|
391
|
+
|
|
392
|
+
if (!browserManager || typeof browserManager.readChannelSnapshot !== 'function') {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
return await browserManager.readChannelSnapshot(channelName, {
|
|
398
|
+
maxChars: 1500,
|
|
399
|
+
includeDomSummary: true,
|
|
400
|
+
includeObservation: true
|
|
401
|
+
});
|
|
402
|
+
} catch {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async readVisibleText() {
|
|
408
|
+
try {
|
|
409
|
+
return await this.page.evaluate(() => document.body?.innerText || '');
|
|
410
|
+
} catch {
|
|
411
|
+
return '';
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async inspectInterventionState(prefetched = {}) {
|
|
416
|
+
const inputSelector =
|
|
417
|
+
prefetched.inputSelector !== undefined
|
|
418
|
+
? prefetched.inputSelector
|
|
419
|
+
: await this.findFirstSelector(this.steps.input?.selectors || [], '输入框');
|
|
420
|
+
const sendSelector =
|
|
421
|
+
prefetched.sendSelector !== undefined
|
|
422
|
+
? prefetched.sendSelector
|
|
423
|
+
: await this.findFirstSelector(this.steps.send?.selectors || [], '发送按钮');
|
|
424
|
+
|
|
425
|
+
return analyzeInterventionState({
|
|
426
|
+
providerId: this.provider.id,
|
|
427
|
+
url: this.page?.url?.() || '',
|
|
428
|
+
visibleText: await this.readVisibleText(),
|
|
429
|
+
inputSelector,
|
|
430
|
+
sendSelector
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async waitForResponse(onChunk, options = {}) {
|
|
435
|
+
return new Promise((resolve, reject) => {
|
|
436
|
+
let textLength = 0;
|
|
437
|
+
let stableCount = 0;
|
|
438
|
+
let finalContent = '';
|
|
439
|
+
let lastEffectiveText = '';
|
|
440
|
+
let noChangeCount = 0;
|
|
441
|
+
let interruptedCount = 0;
|
|
442
|
+
const startTime = Date.now();
|
|
443
|
+
let lastTextChangeAt = startTime;
|
|
444
|
+
const timeoutMs = Number(options.timeoutMs) || config.chat.responseTimeout;
|
|
445
|
+
const quietAfterChangeMs =
|
|
446
|
+
Number(options.quietAfterChangeMs) ||
|
|
447
|
+
Number(config.chat.idleAfterCompleteMs) ||
|
|
448
|
+
1200;
|
|
449
|
+
const allowEmptyOnTimeout = Boolean(options.allowEmptyOnTimeout);
|
|
450
|
+
const allowInterruptedRetry = options.allowInterruptedRetry !== false;
|
|
451
|
+
const interruptedRetryLimit = getProviderInterruptedRetryLimit(this.provider.id);
|
|
452
|
+
const baseline = this.responseBaseline || {
|
|
453
|
+
selector: null,
|
|
454
|
+
count: 0,
|
|
455
|
+
text: ''
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
this.pollTimer = setInterval(async () => {
|
|
459
|
+
try {
|
|
460
|
+
if (Date.now() - startTime > timeoutMs) {
|
|
461
|
+
clearInterval(this.pollTimer);
|
|
462
|
+
this.pollTimer = null;
|
|
463
|
+
if (finalContent) {
|
|
464
|
+
resolve(finalContent.trim());
|
|
465
|
+
} else if (allowEmptyOnTimeout) {
|
|
466
|
+
resolve('');
|
|
467
|
+
} else {
|
|
468
|
+
reject(new Error('响应超时'));
|
|
469
|
+
}
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const result = this.normalizeResult(await this.getResult());
|
|
474
|
+
const { text, done, pending, error, count, transient } = result;
|
|
475
|
+
const effectiveText = deriveEffectiveText(result, baseline);
|
|
476
|
+
|
|
477
|
+
if (this.debug) {
|
|
478
|
+
const snapshot = JSON.stringify({
|
|
479
|
+
textLength: text.length,
|
|
480
|
+
pending,
|
|
481
|
+
done,
|
|
482
|
+
error,
|
|
483
|
+
count,
|
|
484
|
+
transient
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
if (snapshot !== this.lastDebugSnapshot) {
|
|
488
|
+
this.lastDebugSnapshot = snapshot;
|
|
489
|
+
this.debugLog('轮询结果', this.summarizeResult(result));
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (error) {
|
|
494
|
+
clearInterval(this.pollTimer);
|
|
495
|
+
this.pollTimer = null;
|
|
496
|
+
reject(new Error(error));
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (transient) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (
|
|
505
|
+
allowInterruptedRetry &&
|
|
506
|
+
isProviderInterruptedText(this.provider.id, effectiveText)
|
|
507
|
+
) {
|
|
508
|
+
interruptedCount++;
|
|
509
|
+
if (interruptedCount <= interruptedRetryLimit) {
|
|
510
|
+
noChangeCount = 0;
|
|
511
|
+
stableCount = 0;
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (pending && !effectiveText) {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const textChanged = effectiveText !== lastEffectiveText;
|
|
521
|
+
|
|
522
|
+
if (textChanged) {
|
|
523
|
+
const previousText = lastEffectiveText;
|
|
524
|
+
const canStreamIncrementally =
|
|
525
|
+
effectiveText.length > previousText.length &&
|
|
526
|
+
effectiveText.startsWith(previousText);
|
|
527
|
+
|
|
528
|
+
if (canStreamIncrementally) {
|
|
529
|
+
const newText = effectiveText.slice(previousText.length);
|
|
530
|
+
if (typeof onChunk === 'function' && newText) {
|
|
531
|
+
onChunk(newText, previousText.length === 0);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
finalContent = effectiveText;
|
|
536
|
+
textLength = effectiveText.length;
|
|
537
|
+
lastEffectiveText = effectiveText;
|
|
538
|
+
lastTextChangeAt = Date.now();
|
|
539
|
+
stableCount = 0;
|
|
540
|
+
noChangeCount = 0;
|
|
541
|
+
} else {
|
|
542
|
+
noChangeCount++;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const quietForMs = Date.now() - lastTextChangeAt;
|
|
546
|
+
const settled = quietForMs >= quietAfterChangeMs;
|
|
547
|
+
|
|
548
|
+
if (done && effectiveText.length === textLength && textLength > 0 && settled) {
|
|
549
|
+
if (isStructuredResponseIncomplete(finalContent)) {
|
|
550
|
+
stableCount = 0;
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
stableCount++;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (noChangeCount > 30 && finalContent.length > 0 && settled) {
|
|
557
|
+
if (isStructuredResponseIncomplete(finalContent)) {
|
|
558
|
+
noChangeCount = 0;
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
clearInterval(this.pollTimer);
|
|
562
|
+
this.pollTimer = null;
|
|
563
|
+
resolve(finalContent.trim());
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (stableCount >= config.chat.stableThreshold) {
|
|
568
|
+
clearInterval(this.pollTimer);
|
|
569
|
+
this.pollTimer = null;
|
|
570
|
+
resolve(finalContent.trim());
|
|
571
|
+
}
|
|
572
|
+
} catch (error) {
|
|
573
|
+
clearInterval(this.pollTimer);
|
|
574
|
+
this.pollTimer = null;
|
|
575
|
+
reject(error);
|
|
576
|
+
}
|
|
577
|
+
}, config.chat.pollInterval);
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
cleanup() {
|
|
582
|
+
if (this.pollTimer) {
|
|
583
|
+
clearInterval(this.pollTimer);
|
|
584
|
+
this.pollTimer = null;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export default ChatManager;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export function buildQuickStartDoc() {
|
|
2
|
+
return `
|
|
3
|
+
# XShat 使用说明
|
|
4
|
+
|
|
5
|
+
## 1. 开始使用
|
|
6
|
+
|
|
7
|
+
1. 启动程序后选择:
|
|
8
|
+
- \`c\` 继续当前聊天(有活跃会话时出现)
|
|
9
|
+
- \`1\` 新建聊天
|
|
10
|
+
- \`2\` 恢复历史会话
|
|
11
|
+
- \`s\` 系统设置
|
|
12
|
+
- \`d\` 查看文档
|
|
13
|
+
- \`q\` 退出程序
|
|
14
|
+
|
|
15
|
+
2. 默认提供方在系统设置里统一管理。
|
|
16
|
+
新建聊天、任务执行都会优先使用这里设置的默认提供方。
|
|
17
|
+
|
|
18
|
+
3. 直接输入问题开始聊天。
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 2. 常用命令
|
|
23
|
+
|
|
24
|
+
- \`/help\`:查看聊天内可用命令
|
|
25
|
+
- 历史会话 / 任务面板支持 \`W/S\` 或方向键选择,\`A/D\` 或左右键翻页,\`Q\` / \`Esc\` 返回
|
|
26
|
+
- \`/status\`:查看当前运行状态、阻塞信息与推荐操作
|
|
27
|
+
- \`/doc\`:查看这份简要文档
|
|
28
|
+
- \`/menu\`:返回主菜单
|
|
29
|
+
- \`/quit\`:退出程序
|
|
30
|
+
- \`/sessions\`:打开历史会话浏览器
|
|
31
|
+
- \`/resume\`:恢复最近一条历史会话
|
|
32
|
+
- \`/rename-session\`:修改当前会话标题
|
|
33
|
+
- \`/delete-session\`:删除当前会话
|
|
34
|
+
- \`/task\`:打开任务面板
|
|
35
|
+
- \`/mode\`:切换普通模式 / Agent 模式
|
|
36
|
+
- \`/model\`:打开系统设置中的提供方配置
|
|
37
|
+
- \`/inspect\`:检查当前聊天网页适配状态
|
|
38
|
+
- \`/agent\`:手动进入人工接管流程
|
|
39
|
+
- \`/paste\`:进入多行粘贴模式
|
|
40
|
+
- \`/config\`:进入系统设置
|
|
41
|
+
- 主菜单 \`s\`:进入系统设置
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 3. Agent 模式说明
|
|
46
|
+
|
|
47
|
+
- Agent 模式会要求网页模型尽量输出结构化 JSON。
|
|
48
|
+
- 程序会解析回复,识别普通回答、命令、人工接管、错误。
|
|
49
|
+
- 如果模型要求打开网页、读取页面、执行命令,程序会先询问你是否执行;开启自动执行后会自动运行。
|
|
50
|
+
- 遇到登录、验证码、人工验证时,可以手动接管。
|
|
51
|
+
- 开启“多 Agent 协作”后,会按主控、网页侦察、系统执行、文件执行、人工接管这几类角色来分工和显示时间线。
|
|
52
|
+
- XShat 的底层思路不是只绑定一个模型,而是优先使用免费网页大模型;当前提供方卡住时,可以自动切换到下一个提供方继续任务。
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 4. Agent 能做什么
|
|
57
|
+
|
|
58
|
+
- 打开网页、等待页面加载、读取页面文本
|
|
59
|
+
- 点击、输入、滚动、按键、下拉框选择
|
|
60
|
+
- 读取应用内部状态
|
|
61
|
+
- 直接聚焦聊天输入框、填写聊天框、提交消息、新建网页对话
|
|
62
|
+
- 读取、写入、删除工作区文件
|
|
63
|
+
- 执行系统命令
|
|
64
|
+
- 如果开启“统一登录态”,聊天通道和 Agent 通道会尽量同步登录状态
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 5. 小提示
|
|
69
|
+
|
|
70
|
+
- 多行内容可以先输入 \`/paste\`,粘贴完成后单独输入 \`/end\` 发送,输入 \`/cancel\` 取消。
|
|
71
|
+
- 如果多轮对话变长,程序会自动压缩较早上下文,只保留最近几轮原文。
|
|
72
|
+
- 如果某个网页模型回复不稳定,可以去系统设置切换默认提供方后继续当前任务。
|
|
73
|
+
- 也可以在系统设置中开启“自动故障切换”,让程序优先在多个网页提供方之间自动接力。
|
|
74
|
+
- 如果想使用 API 提供方,可设置环境变量:
|
|
75
|
+
- \`OPENAI_API_KEY\` / \`OPENAI_BASE_URL\` / \`OPENAI_MODEL\`
|
|
76
|
+
- \`DEEPSEEK_API_KEY\` / \`DEEPSEEK_BASE_URL\` / \`DEEPSEEK_MODEL\`
|
|
77
|
+
- 如果页面卡在登录或验证,输入 \`/agent\` 更方便。
|
|
78
|
+
- 如果你不希望程序弹出可见浏览器,可在系统设置中关闭“允许可见浏览器”;关闭后,遇到登录或验证只会提示你手动处理,不会自动开窗口。
|
|
79
|
+
- 如果要长期挂着干活,可以在系统设置里打开“长时运行模式”和“自动恢复”。
|
|
80
|
+
- 如果想让 Agent 更像任务型产品,可以在系统设置里打开“多 Agent 协作”,更容易看出当前是哪一类角色在推进。
|
|
81
|
+
- 任务面板支持单次执行、持续循环、轮询监控、定时间隔,并可设置失败重试。
|
|
82
|
+
- 程序启动时会自动清理临时文件,并把超出保留上限的旧会话、旧任务归档到 \`runtime/archive\`。
|
|
83
|
+
`.trim();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default {
|
|
87
|
+
buildQuickStartDoc
|
|
88
|
+
};
|