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,132 @@
1
+ const RULES = [
2
+ {
3
+ kind: 'verification',
4
+ label: '人机验证',
5
+ reasons: [
6
+ /captcha/i,
7
+ /verify (that )?you(?:'| a)?re human/i,
8
+ /human verification/i,
9
+ /prove you(?:'| a)?re human/i,
10
+ /security check/i,
11
+ /not a robot/i,
12
+ /人机验证/,
13
+ /安全验证/,
14
+ /验证.*人类/,
15
+ /验证.*真人/
16
+ ]
17
+ },
18
+ {
19
+ kind: 'login',
20
+ label: '登录',
21
+ reasons: [
22
+ /\blog in\b/i,
23
+ /\blog on\b/i,
24
+ /\bsign in\b/i,
25
+ /please log in/i,
26
+ /please sign in/i,
27
+ /登录/,
28
+ /登入/,
29
+ /请先登录/,
30
+ /继续使用.*登录/,
31
+ /手机号登录/,
32
+ /验证码登录/
33
+ ]
34
+ },
35
+ {
36
+ kind: 'access',
37
+ label: '站点限制',
38
+ reasons: [
39
+ /unable to load site/i,
40
+ /access denied/i,
41
+ /request blocked/i,
42
+ /temporarily unavailable/i,
43
+ /service unavailable/i
44
+ ]
45
+ }
46
+ ];
47
+
48
+ const URL_HINTS = [
49
+ {
50
+ kind: 'verification',
51
+ label: '人机验证',
52
+ pattern: /(captcha|challenge|verify)/i
53
+ },
54
+ {
55
+ kind: 'login',
56
+ label: '登录',
57
+ pattern: /(login|signin|sign-in|auth)/i
58
+ }
59
+ ];
60
+
61
+ function compactText(text) {
62
+ return String(text ?? '')
63
+ .replace(/\s+/g, ' ')
64
+ .trim()
65
+ .slice(0, 4000);
66
+ }
67
+
68
+ export function analyzeInterventionState({
69
+ providerId = '',
70
+ url = '',
71
+ visibleText = '',
72
+ inputSelector = null,
73
+ sendSelector = null
74
+ } = {}) {
75
+ const normalizedUrl = String(url ?? '');
76
+ const normalizedText = compactText(visibleText);
77
+ const matches = [];
78
+ let urlMatch = null;
79
+ let textMatch = null;
80
+
81
+ for (const hint of URL_HINTS) {
82
+ if (hint.pattern.test(normalizedUrl)) {
83
+ urlMatch = {
84
+ kind: hint.kind,
85
+ label: hint.label,
86
+ evidence: `URL: ${normalizedUrl}`
87
+ };
88
+ matches.push(urlMatch);
89
+ break;
90
+ }
91
+ }
92
+
93
+ for (const rule of RULES) {
94
+ const reason = rule.reasons.find((pattern) => pattern.test(normalizedText));
95
+ if (reason) {
96
+ textMatch = {
97
+ kind: rule.kind,
98
+ label: rule.label,
99
+ evidence: reason.source
100
+ };
101
+ matches.push(textMatch);
102
+ break;
103
+ }
104
+ }
105
+
106
+ const primary = matches[0] || null;
107
+ const missingComposer = !inputSelector && !sendSelector;
108
+ const blocked = Boolean(
109
+ urlMatch ||
110
+ (textMatch?.kind === 'verification') ||
111
+ (textMatch?.kind === 'login' && missingComposer) ||
112
+ (textMatch?.kind === 'access' && missingComposer)
113
+ );
114
+
115
+ return {
116
+ blocked,
117
+ providerId,
118
+ kind: primary?.kind || null,
119
+ label: primary?.label || null,
120
+ reason: primary
121
+ ? `检测到可能需要${primary.label}`
122
+ : '未检测到需要人工介入的阻塞',
123
+ url: normalizedUrl,
124
+ visibleText: normalizedText,
125
+ inputSelector: inputSelector || null,
126
+ sendSelector: sendSelector || null,
127
+ missingComposer,
128
+ urlMatched: Boolean(urlMatch),
129
+ textMatched: Boolean(textMatch),
130
+ evidence: matches.map((item) => item.evidence)
131
+ };
132
+ }
@@ -0,0 +1,181 @@
1
+ import { buildAgentFollowupMessage } from './chat-helpers.mjs';
2
+ import { inferAgentRoleFromCommand } from './multi-agent.mjs';
3
+ import { normalizeResponseEnvelope } from './response-pipeline.mjs';
4
+
5
+ function attachResultAgent(result, envelope) {
6
+ if (!result || typeof result !== 'object') {
7
+ return result;
8
+ }
9
+
10
+ if (!result.agent) {
11
+ result.agent = envelope?.agent || inferAgentRoleFromCommand(envelope?.command) || null;
12
+ }
13
+
14
+ return result;
15
+ }
16
+
17
+ export async function runAgentLoop({
18
+ initialEnvelope,
19
+ initialStep = 0,
20
+ maxSteps = 1,
21
+ agentExecutor,
22
+ requestFollowupResponse,
23
+ parseEnvelope = (rawResponse) => normalizeResponseEnvelope(rawResponse, {
24
+ agentMode: true
25
+ }),
26
+ onBeforeCommand,
27
+ onAfterCommand,
28
+ onFollowupResponse
29
+ } = {}) {
30
+ let envelope = initialEnvelope;
31
+ let step = initialStep;
32
+ const cappedMaxSteps = Math.max(1, Number(maxSteps) || 1);
33
+
34
+ while (true) {
35
+ if (envelope?.kind === 'handoff') {
36
+ return {
37
+ status: 'handoff',
38
+ envelope,
39
+ step
40
+ };
41
+ }
42
+
43
+ if (envelope?.kind !== 'command' || !envelope?.command) {
44
+ return {
45
+ status: 'message',
46
+ envelope,
47
+ step
48
+ };
49
+ }
50
+
51
+ const nextStep = step + 1;
52
+
53
+ if (typeof onBeforeCommand === 'function') {
54
+ const gate = await onBeforeCommand({
55
+ envelope,
56
+ step: nextStep,
57
+ maxSteps: cappedMaxSteps
58
+ });
59
+
60
+ if (gate?.stop) {
61
+ return {
62
+ status: gate.status || 'cancelled',
63
+ envelope,
64
+ step: nextStep
65
+ };
66
+ }
67
+ }
68
+
69
+ const result = attachResultAgent(
70
+ await agentExecutor.execute(envelope.command),
71
+ envelope
72
+ );
73
+
74
+ if (typeof onAfterCommand === 'function') {
75
+ await onAfterCommand({
76
+ envelope,
77
+ result,
78
+ step: nextStep,
79
+ maxSteps: cappedMaxSteps
80
+ });
81
+ }
82
+
83
+ if (!result?.ok) {
84
+ if (result?.recoverable && nextStep < cappedMaxSteps) {
85
+ const followupMessage = buildAgentFollowupMessage(result, {
86
+ step: nextStep,
87
+ maxSteps: cappedMaxSteps,
88
+ completion: {
89
+ status: 'recoverable-error',
90
+ reason: result.error || '命令执行失败',
91
+ guidance:
92
+ '上一条浏览器/系统动作失败,但宿主判断这类错误可恢复。请不要重复同一命令原样硬点;应基于错误原因调整策略,例如换 selector、先 wait_for_selector、scroll_page、inspect_dom、extract_selector_text,或先读取页面状态再继续。'
93
+ }
94
+ });
95
+
96
+ const rawResponse = await requestFollowupResponse(followupMessage, {
97
+ followupStep: nextStep,
98
+ step: nextStep,
99
+ maxSteps: cappedMaxSteps,
100
+ result
101
+ });
102
+
103
+ envelope = parseEnvelope(rawResponse);
104
+ step = nextStep;
105
+
106
+ if (typeof onFollowupResponse === 'function') {
107
+ await onFollowupResponse({
108
+ rawResponse,
109
+ envelope,
110
+ step,
111
+ maxSteps: cappedMaxSteps
112
+ });
113
+ }
114
+
115
+ continue;
116
+ }
117
+
118
+ return {
119
+ status: 'error',
120
+ envelope,
121
+ result,
122
+ step: nextStep
123
+ };
124
+ }
125
+
126
+ if (result.handoff) {
127
+ return {
128
+ status: 'handoff',
129
+ envelope,
130
+ result,
131
+ step: nextStep
132
+ };
133
+ }
134
+
135
+ if (!agentExecutor.shouldContinue(result)) {
136
+ return {
137
+ status: 'completed',
138
+ envelope,
139
+ result,
140
+ step: nextStep
141
+ };
142
+ }
143
+
144
+ if (nextStep >= cappedMaxSteps) {
145
+ return {
146
+ status: 'limit',
147
+ envelope,
148
+ result,
149
+ step: nextStep
150
+ };
151
+ }
152
+
153
+ const followupMessage = buildAgentFollowupMessage(result, {
154
+ step: nextStep,
155
+ maxSteps: cappedMaxSteps
156
+ });
157
+
158
+ const rawResponse = await requestFollowupResponse(followupMessage, {
159
+ followupStep: nextStep,
160
+ step: nextStep,
161
+ maxSteps: cappedMaxSteps,
162
+ result
163
+ });
164
+
165
+ envelope = parseEnvelope(rawResponse);
166
+ step = nextStep;
167
+
168
+ if (typeof onFollowupResponse === 'function') {
169
+ await onFollowupResponse({
170
+ rawResponse,
171
+ envelope,
172
+ step,
173
+ maxSteps: cappedMaxSteps
174
+ });
175
+ }
176
+ }
177
+ }
178
+
179
+ export default {
180
+ runAgentLoop
181
+ };
@@ -0,0 +1,152 @@
1
+ function isAffirmative(answer) {
2
+ const normalized = String(answer ?? '')
3
+ .trim()
4
+ .toLowerCase();
5
+ return ['y', 'yes', 'done', 'ok', '好了', '完成'].includes(normalized);
6
+ }
7
+
8
+ function buildBlockedByPolicyRows({
9
+ diagnosis = {},
10
+ stage = '',
11
+ error = null
12
+ } = {}) {
13
+ return [
14
+ { label: '阻塞类型', value: diagnosis.label || diagnosis.kind || '人工介入' },
15
+ { label: '阶段', value: stage || '(未知)' },
16
+ { label: '原因', value: diagnosis.reason || '检测到需要人工处理的页面状态' },
17
+ { label: '当前页面', value: diagnosis.url || '(未知)' },
18
+ {
19
+ label: '触发原因',
20
+ value: error?.message || '页面可能需要登录、验证码、权限确认或人工判断'
21
+ },
22
+ { label: '可见浏览器', value: '当前配置已禁用' },
23
+ {
24
+ label: '建议下一步',
25
+ value: '如需手动接管,请先在 /config 中开启“允许可见浏览器”,或改用已登录的 USER_DATA_DIR 后重试。'
26
+ }
27
+ ];
28
+ }
29
+
30
+ export class ConversationAgent {
31
+ constructor({ browserManager, ui, prompt }) {
32
+ this.browserManager = browserManager;
33
+ this.ui = ui;
34
+ this.prompt = prompt;
35
+ }
36
+
37
+ async diagnose(chatManager) {
38
+ if (!chatManager || typeof chatManager.inspectInterventionState !== 'function') {
39
+ return {
40
+ blocked: false,
41
+ reason: '',
42
+ url: '',
43
+ providerId: chatManager?.provider?.id || null
44
+ };
45
+ }
46
+
47
+ return chatManager.inspectInterventionState();
48
+ }
49
+
50
+ async handleIntervention({ chatManager, stage, error }) {
51
+ const diagnosis = await this.diagnose(chatManager);
52
+
53
+ if (!diagnosis.blocked) {
54
+ return {
55
+ handled: false,
56
+ diagnosis
57
+ };
58
+ }
59
+
60
+ this.ui.stopSpinner?.();
61
+ this.ui.printWarning(`${diagnosis.reason},当前阶段: ${stage}`);
62
+ if (error?.message) {
63
+ this.ui.printInfo(`触发原因: ${error.message}`);
64
+ }
65
+ if (diagnosis.url) {
66
+ this.ui.printInfo(`当前页面: ${diagnosis.url}`);
67
+ }
68
+
69
+ const consent = await this.prompt(
70
+ '是否切换到可见浏览器,由你手动完成后继续?(y/n) '
71
+ );
72
+
73
+ if (!isAffirmative(consent)) {
74
+ return {
75
+ handled: false,
76
+ diagnosis,
77
+ declined: true
78
+ };
79
+ }
80
+
81
+ const targetUrl = diagnosis.url || chatManager.provider.url;
82
+ const shouldRestoreHeadless =
83
+ this.browserManager?.browserConfig?.headless !== false;
84
+ const allowVisibleBrowser =
85
+ this.browserManager?.browserConfig?.allowVisibleBrowser !== false;
86
+
87
+ if (!allowVisibleBrowser) {
88
+ if (typeof this.ui.printActionPanel === 'function') {
89
+ this.ui.printActionPanel(
90
+ '人工接管受限',
91
+ buildBlockedByPolicyRows({
92
+ diagnosis,
93
+ stage,
94
+ error
95
+ })
96
+ );
97
+ } else {
98
+ this.ui.printInfo('当前配置禁止切换到可见浏览器,请手动开启 browser.allowVisibleBrowser 后再进行人工接管。');
99
+ }
100
+ return {
101
+ handled: false,
102
+ diagnosis,
103
+ blockedByPolicy: true
104
+ };
105
+ }
106
+
107
+ await this.browserManager.relaunch({
108
+ headless: false,
109
+ url: targetUrl
110
+ });
111
+
112
+ this.ui.printSuccess('已切换到可见浏览器');
113
+ this.ui.printInfo('请在浏览器里完成登录、验证码或权限确认。');
114
+ if (this.browserManager?.browserConfig?.shareAuthState) {
115
+ this.ui.printInfo('登录状态会保留在当前 USER_DATA_DIR,并尽量同步到聊天/Agent 通道。');
116
+ }
117
+
118
+ while (true) {
119
+ const answer = await this.prompt(
120
+ '完成后输入 done 继续,输入 cancel 取消: '
121
+ );
122
+ const normalized = String(answer ?? '')
123
+ .trim()
124
+ .toLowerCase();
125
+
126
+ if (normalized === 'cancel' || normalized === 'n' || normalized === 'no') {
127
+ return {
128
+ handled: false,
129
+ diagnosis,
130
+ cancelled: true
131
+ };
132
+ }
133
+
134
+ if (isAffirmative(normalized)) {
135
+ if (shouldRestoreHeadless) {
136
+ await this.browserManager.relaunch({
137
+ headless: true,
138
+ url: targetUrl
139
+ });
140
+ this.ui.printSuccess('已恢复为默认浏览器模式');
141
+ }
142
+
143
+ return {
144
+ handled: true,
145
+ diagnosis
146
+ };
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ export default ConversationAgent;
@@ -0,0 +1,212 @@
1
+ import config from '../utils/config.mjs';
2
+
3
+ function normalizeMessages(messages = []) {
4
+ return messages
5
+ .map((message) => {
6
+ if (typeof message === 'string') {
7
+ return {
8
+ role: 'user',
9
+ content: message
10
+ };
11
+ }
12
+
13
+ return {
14
+ role: message.role || 'user',
15
+ content: String(message.content ?? '')
16
+ };
17
+ })
18
+ .filter((message) => message.content.trim());
19
+ }
20
+
21
+ function deepClone(value) {
22
+ return JSON.parse(JSON.stringify(value));
23
+ }
24
+
25
+ function extractAssistantContent(data = {}) {
26
+ const message = data?.choices?.[0]?.message;
27
+ const directContent = message?.content ?? data?.choices?.[0]?.text ?? '';
28
+
29
+ if (typeof directContent === 'string') {
30
+ return directContent;
31
+ }
32
+
33
+ if (Array.isArray(directContent)) {
34
+ return directContent
35
+ .map((part) => {
36
+ if (typeof part === 'string') {
37
+ return part;
38
+ }
39
+
40
+ if (part && typeof part === 'object') {
41
+ return part.text || part.content || '';
42
+ }
43
+
44
+ return '';
45
+ })
46
+ .join('');
47
+ }
48
+
49
+ return '';
50
+ }
51
+
52
+ export default class ApiChatManager {
53
+ constructor(provider) {
54
+ this.provider = provider;
55
+ this.messageHistory = [];
56
+ this.pendingMessages = [];
57
+ }
58
+
59
+ get apiConfig() {
60
+ const providerApi = this.provider.api || {};
61
+ const globalApi = config.api || {};
62
+ const envPrefix = String(providerApi.envPrefix || 'OPENAI').toUpperCase();
63
+
64
+ return {
65
+ baseUrl:
66
+ process.env[`${envPrefix}_BASE_URL`] ||
67
+ providerApi.baseUrl ||
68
+ globalApi.baseUrl ||
69
+ 'https://api.openai.com/v1',
70
+ apiKey:
71
+ process.env[`${envPrefix}_API_KEY`] ||
72
+ providerApi.apiKey ||
73
+ globalApi.apiKey ||
74
+ '',
75
+ model:
76
+ process.env[`${envPrefix}_MODEL`] ||
77
+ providerApi.model ||
78
+ globalApi.model ||
79
+ 'gpt-4o-mini',
80
+ headers: providerApi.headers || {},
81
+ responseFormat:
82
+ providerApi.responseFormat ||
83
+ globalApi.responseFormat ||
84
+ null,
85
+ temperature:
86
+ Number.isFinite(providerApi.temperature)
87
+ ? providerApi.temperature
88
+ : Number.isFinite(globalApi.temperature)
89
+ ? globalApi.temperature
90
+ : 0.2
91
+ };
92
+ }
93
+
94
+ async sendMessage(message) {
95
+ const normalized = normalizeMessages(
96
+ Array.isArray(message) ? message : [{ role: 'user', content: message }]
97
+ );
98
+
99
+ if (normalized.length === 0) {
100
+ return false;
101
+ }
102
+
103
+ this.pendingMessages = normalized;
104
+ this.messageHistory.push(...deepClone(normalized));
105
+ return true;
106
+ }
107
+
108
+ rollbackPendingMessages() {
109
+ const pendingCount = this.pendingMessages.length;
110
+ if (pendingCount > 0 && this.messageHistory.length >= pendingCount) {
111
+ this.messageHistory.splice(this.messageHistory.length - pendingCount, pendingCount);
112
+ }
113
+ this.pendingMessages = [];
114
+ }
115
+
116
+ async waitForResponse(onChunk = () => {}) {
117
+ const {
118
+ baseUrl,
119
+ apiKey,
120
+ model,
121
+ headers,
122
+ responseFormat,
123
+ temperature
124
+ } = this.apiConfig;
125
+
126
+ if (!apiKey) {
127
+ throw new Error(
128
+ `API 提供方 ${this.provider.name} 缺少密钥,请设置 ${String(
129
+ this.provider.api?.envPrefix || 'OPENAI'
130
+ ).toUpperCase()}_API_KEY`
131
+ );
132
+ }
133
+
134
+ let response;
135
+
136
+ try {
137
+ const body = {
138
+ model,
139
+ messages: this.messageHistory,
140
+ temperature,
141
+ stream: false
142
+ };
143
+
144
+ if (responseFormat) {
145
+ body.response_format = responseFormat;
146
+ }
147
+
148
+ response = await fetch(`${baseUrl.replace(/\/$/, '')}/chat/completions`, {
149
+ method: 'POST',
150
+ headers: {
151
+ 'content-type': 'application/json',
152
+ authorization: `Bearer ${apiKey}`,
153
+ ...headers
154
+ },
155
+ body: JSON.stringify(body)
156
+ });
157
+ } catch (error) {
158
+ this.rollbackPendingMessages();
159
+ throw error;
160
+ }
161
+
162
+ if (!response.ok) {
163
+ const detail = await response.text().catch(() => '');
164
+ this.rollbackPendingMessages();
165
+ throw new Error(`API 请求失败: ${response.status} ${detail}`.trim());
166
+ }
167
+
168
+ const data = await response.json();
169
+ const content = extractAssistantContent(data);
170
+
171
+ if (content) {
172
+ onChunk(content, true);
173
+ this.messageHistory.push({
174
+ role: 'assistant',
175
+ content
176
+ });
177
+ }
178
+
179
+ this.pendingMessages = [];
180
+ return String(content || '').trim();
181
+ }
182
+
183
+ async inspectSite() {
184
+ const { baseUrl, model } = this.apiConfig;
185
+ return {
186
+ provider: this.provider.id,
187
+ inputSelector: '(API 模式)',
188
+ sendSelector: '(HTTP 请求)',
189
+ readSelector: '(响应体)',
190
+ completionMode: 'api',
191
+ result: {
192
+ pending: false,
193
+ done: true,
194
+ error: null,
195
+ preview: `${baseUrl} :: ${model}`
196
+ },
197
+ intervention: null,
198
+ observation: null
199
+ };
200
+ }
201
+
202
+ async inspectInterventionState() {
203
+ return {
204
+ blocked: false,
205
+ reason: '',
206
+ url: '',
207
+ providerId: this.provider.id
208
+ };
209
+ }
210
+
211
+ cleanup() {}
212
+ }