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,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
|
+
}
|