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,275 @@
1
+ export function sanitizeMessage(message) {
2
+ return String(message ?? '')
3
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
4
+ .replace(/\r/g, '');
5
+ }
6
+
7
+ export function stripProviderWrapper(text) {
8
+ return String(text ?? '').replace(
9
+ /^\s*(?:Gemini|AI|Assistant)\s*(?:说|:|:)\s*/i,
10
+ ''
11
+ );
12
+ }
13
+
14
+ function clipText(text, maxChars = 160) {
15
+ const normalized = sanitizeMessage(stripProviderWrapper(String(text ?? '')))
16
+ .replace(/\s+/g, ' ')
17
+ .trim();
18
+
19
+ if (normalized.length <= maxChars) {
20
+ return normalized;
21
+ }
22
+
23
+ return `${normalized.slice(0, Math.max(0, maxChars - 1))}…`;
24
+ }
25
+
26
+ export function buildCompleteConfig(steps = {}) {
27
+ const complete = steps.complete || {};
28
+ const readSelectors = steps.read?.selectors || [];
29
+
30
+ return {
31
+ mode: complete.mode || 'script',
32
+ script: complete.script || null,
33
+ selectors: {
34
+ response: complete.selectors?.response || readSelectors,
35
+ pending: complete.selectors?.pending || [],
36
+ done: complete.selectors?.done || [],
37
+ interimTexts: complete.selectors?.interimTexts || []
38
+ }
39
+ };
40
+ }
41
+
42
+ export function normalizeResult(result) {
43
+ if (!result || typeof result !== 'object') {
44
+ return {
45
+ text: '',
46
+ pending: false,
47
+ done: false,
48
+ error: null,
49
+ count: 0,
50
+ transient: false
51
+ };
52
+ }
53
+
54
+ return {
55
+ text: typeof result.text === 'string' ? result.text : '',
56
+ pending: Boolean(result.pending),
57
+ done: Boolean(result.done),
58
+ error: result.error ? String(result.error) : null,
59
+ count: Number.isFinite(result.count) ? result.count : 0,
60
+ transient: Boolean(result.transient)
61
+ };
62
+ }
63
+
64
+ export function summarizeResult(result) {
65
+ const normalized = normalizeResult(result);
66
+
67
+ return {
68
+ pending: normalized.pending,
69
+ done: normalized.done,
70
+ error: normalized.error,
71
+ count: normalized.count,
72
+ transient: normalized.transient,
73
+ preview: normalized.text.replace(/\s+/g, ' ').slice(0, 120)
74
+ };
75
+ }
76
+
77
+ export function deriveEffectiveText(result, baseline = {}) {
78
+ const normalized = normalizeResult(result);
79
+ const safeBaseline = {
80
+ count: baseline.count ?? 0,
81
+ text: baseline.text ?? ''
82
+ };
83
+
84
+ if (normalized.count > safeBaseline.count) {
85
+ if (safeBaseline.text && normalized.text.startsWith(safeBaseline.text)) {
86
+ return normalized.text.slice(safeBaseline.text.length);
87
+ }
88
+ return normalized.text;
89
+ }
90
+
91
+ if (
92
+ normalized.count === safeBaseline.count &&
93
+ safeBaseline.text &&
94
+ normalized.text.startsWith(safeBaseline.text)
95
+ ) {
96
+ return normalized.text.slice(safeBaseline.text.length);
97
+ }
98
+
99
+ if (
100
+ normalized.count === safeBaseline.count &&
101
+ safeBaseline.text !== normalized.text
102
+ ) {
103
+ return normalized.text;
104
+ }
105
+
106
+ return '';
107
+ }
108
+
109
+ export function compressConversationContext(messages = [], {
110
+ compressThreshold = 12,
111
+ keepRecentMessages = 6,
112
+ maxSummaryChars = 1200,
113
+ maxMessageChars = 160
114
+ } = {}) {
115
+ const safeMessages = Array.isArray(messages) ? messages : [];
116
+ if (safeMessages.length <= compressThreshold) {
117
+ return {
118
+ compressed: false,
119
+ summary: '',
120
+ recentMessages: safeMessages
121
+ };
122
+ }
123
+
124
+ const keepCount = Math.max(2, keepRecentMessages);
125
+ const recentMessages = safeMessages.slice(-keepCount);
126
+ const olderMessages = safeMessages.slice(0, Math.max(0, safeMessages.length - keepCount));
127
+ const lines = [];
128
+ let totalChars = 0;
129
+
130
+ for (const message of olderMessages) {
131
+ const role = message.role === 'user' ? 'User' : 'Assistant';
132
+ const line = `${role}: ${clipText(message.content, maxMessageChars)}`;
133
+ const nextLength = totalChars + line.length + 1;
134
+
135
+ if (nextLength > maxSummaryChars) {
136
+ lines.push('...');
137
+ break;
138
+ }
139
+
140
+ lines.push(line);
141
+ totalChars = nextLength;
142
+ }
143
+
144
+ return {
145
+ compressed: true,
146
+ summary: lines.join('\n'),
147
+ recentMessages
148
+ };
149
+ }
150
+
151
+ export function buildHistoryPrimer(messages = [], options = {}) {
152
+ const context = compressConversationContext(messages, options);
153
+ const historyText = context.recentMessages
154
+ .map((message) => {
155
+ const role = message.role === 'user' ? 'User' : 'Assistant';
156
+ return `${role}: ${stripProviderWrapper(message.content)}`;
157
+ })
158
+ .join('\n\n');
159
+
160
+ const sections = [
161
+ '以下是我们之前的对话记录,请记住这些内容作为上下文,不需要对此回复,只需确认你已了解:'
162
+ ];
163
+
164
+ if (context.summary) {
165
+ sections.push(`较早对话摘要:\n${context.summary}`);
166
+ }
167
+
168
+ if (historyText) {
169
+ sections.push(`最近对话原文:\n\n${historyText}`);
170
+ }
171
+
172
+ sections.push('请回复"已了解"即可,等待我的新问题。');
173
+ return sections.join('\n\n');
174
+ }
175
+
176
+ export function buildAgentPrimer(systemPrompt, messages = [], options = {}) {
177
+ const context = compressConversationContext(messages, options);
178
+ const historyText = context.recentMessages
179
+ .map((message) => {
180
+ const role = message.role === 'user' ? 'User' : 'Assistant';
181
+ return `${role}: ${stripProviderWrapper(message.content)}`;
182
+ })
183
+ .join('\n\n');
184
+
185
+ return [
186
+ systemPrompt,
187
+ context.summary ? `以下是较早上下文摘要:\n${context.summary}` : '',
188
+ historyText ? `以下是最近上下文原文:\n\n${historyText}` : '',
189
+ '请仅回复符合要求的 JSON。'
190
+ ]
191
+ .filter(Boolean)
192
+ .join('\n\n');
193
+ }
194
+
195
+ export function wrapAgentTurnMessage(message, { providerId = '' } = {}) {
196
+ const normalizedProviderId = String(providerId).trim().toLowerCase();
197
+ const providerHints = [];
198
+
199
+ if (normalizedProviderId === 'deepseek' || normalizedProviderId === 'api-deepseek') {
200
+ providerHints.push(
201
+ '你当前运行在 DeepSeek 通道。不要输出“根据当前协议我无法访问互联网”“我无法直接浏览网页”“没有搜索工具”。',
202
+ '如果用户问当前目录、文件是否存在、创建文件、写入内容,这通常是本地任务,不是网页任务,请优先返回文件或 shell 命令。',
203
+ '不要把路径、域名或文件名混淆。像 app.txt 是文件,baidu.com 更可能是网址,当前目录/项目目录是本地工作区。'
204
+ );
205
+ }
206
+
207
+ const content = sanitizeMessage(message).trim();
208
+
209
+ return [
210
+ '继续按照既定 Agent 协议工作。',
211
+ '如果你看不到更早的系统约束,也要继续严格遵守当前这条消息里的协议,不要要求我重新说明格式。',
212
+ '本轮也只能输出一个 JSON 对象,不要输出任何额外说明。',
213
+ '如果你输出 reply,请优先先给结果,再补充 2-6 条必要要点;允许在 text 字段中使用简洁 Markdown。',
214
+ '如果内容适合对比、排行、统计或赛程,请优先用紧凑列表、小表格或“项目: 值”形式,不要生成列很多的大表格。',
215
+ '面向终端输出时,优先使用: 短结论 + 项目列表;若有对比信息,再用 2-4 列的小表格;每个单元格尽量短,避免横向过宽。',
216
+ '如果内容包含步骤、建议、优缺点、预测依据、时间线或候选项,请使用编号列表或项目符号列表,不要堆成长段落。',
217
+ '如果是在做预测、推荐、对比、方案选择或参数说明,优先输出“结论 + 列表”或 2-4 列小表格,让 CLI 里一眼能扫完。',
218
+ '不要先声明“无法联网”“无法实时”“没有搜索工具”“只是单步 Agent”。先判断是否可以用 open_url、read_agent_page、extract_selector_text、write_file、run_shell 等宿主能力推进。',
219
+ '如果只需回答,用 {"type":"reply","text":"...","command":null}。',
220
+ '如果需要宿主执行一步命令,用 {"type":"action","text":"...","command":{"name":"...","args":{}}}。',
221
+ '如果需要人工处理登录、验证、授权或肉眼判断,用 {"type":"handoff","text":"...","command":null}。',
222
+ '如果当前能力不足或信息不足,用 {"type":"error","text":"...","command":null}。',
223
+ '如果你想在网页输入内容,请使用 type_selector,不要使用 input_text / fill_text / fill_input 这些别名。',
224
+ '如果你想点击网页元素,请使用 click_selector,不要发明 click_element 等别名。',
225
+ ...providerHints,
226
+ '',
227
+ `用户本轮消息如下:\n${content}`
228
+ ].join('\n');
229
+ }
230
+
231
+ export function buildAgentFollowupMessage(result, {
232
+ step = 1,
233
+ maxSteps = 1,
234
+ completion = null
235
+ } = {}) {
236
+ let parsedOutput = result?.output || '';
237
+ if (typeof parsedOutput === 'string') {
238
+ try {
239
+ parsedOutput = JSON.parse(parsedOutput);
240
+ } catch {
241
+ parsedOutput = parsedOutput;
242
+ }
243
+ }
244
+
245
+ const receipt = {
246
+ ok: Boolean(result?.ok),
247
+ command: result?.commandName || null,
248
+ handoff: Boolean(result?.handoff),
249
+ message: result?.message || '',
250
+ output: parsedOutput
251
+ };
252
+ const lines = [
253
+ `上一条命令已经执行完毕。这是第 ${step}/${maxSteps} 步的执行结果。`,
254
+ '请先根据结果判断是否已经完成用户目标。',
255
+ '如果已经完成,请直接输出 reply,并优先先给结果,再给必要要点;text 字段允许使用简洁 Markdown。',
256
+ '如果要展示结构化结果,优先小表格、列表或“项目: 值”,避免终端里很宽的大表格。',
257
+ '终端阅读优先级: 结论一句话 > 2-6 条要点 > 必要时再加短表格,不要先写冗长铺垫。',
258
+ '如果仍需下一步,只输出一个 action,并保持最小必要动作。',
259
+ '除非已经确认存在真实阻塞,否则不要直接输出 error,也不要先说自己无法联网、无法持续执行或没有搜索能力。',
260
+ '如果需要人工处理登录、验证、授权或页面异常,请输出 handoff。',
261
+ '如果当前信息仍不足,请优先 get_app_state、read_agent_page、extract_selector_text 或 inspect_dom。',
262
+ '若需要输入网页内容,请使用 type_selector;若需要点击网页元素,请使用 click_selector。',
263
+ '',
264
+ `操作回执(JSON):\n${JSON.stringify(receipt, null, 2)}`
265
+ ];
266
+
267
+ if (completion) {
268
+ lines.push(
269
+ '',
270
+ `宿主判断(JSON):\n${JSON.stringify(completion, null, 2)}`
271
+ );
272
+ }
273
+
274
+ return lines.join('\n');
275
+ }