@ynhcj/xiaoyi-channel 0.0.68-beta → 0.0.68-next
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/dist/index.js +50 -4
- package/dist/src/bot.js +1 -0
- package/dist/src/channel.js +15 -23
- package/dist/src/cspl/call-api.js +14 -11
- package/dist/src/cspl/config.js +3 -3
- package/dist/src/cspl/constants.d.ts +2 -0
- package/dist/src/cspl/constants.js +12 -0
- package/dist/src/cspl/utils.js +4 -2
- package/dist/src/file-download.js +3 -6
- package/dist/src/file-upload.js +52 -5
- package/dist/src/login-token-handler.d.ts +8 -0
- package/dist/src/login-token-handler.js +60 -0
- package/dist/src/monitor.js +14 -0
- package/dist/src/outbound.js +2 -7
- package/dist/src/provider.js +380 -43
- package/dist/src/reply-dispatcher.js +6 -0
- package/dist/src/self-evolution-handler.d.ts +1 -0
- package/dist/src/self-evolution-handler.js +47 -0
- package/dist/src/skill-retriever/config.d.ts +4 -0
- package/dist/src/skill-retriever/config.js +23 -0
- package/dist/src/skill-retriever/hooks.d.ts +22 -0
- package/dist/src/skill-retriever/hooks.js +91 -0
- package/dist/src/skill-retriever/tool-search.d.ts +16 -0
- package/dist/src/skill-retriever/tool-search.js +159 -0
- package/dist/src/skill-retriever/types.d.ts +34 -0
- package/dist/src/skill-retriever/types.js +1 -0
- package/dist/src/tools/call-device-tool.d.ts +5 -0
- package/dist/src/tools/call-device-tool.js +130 -0
- package/dist/src/tools/create-alarm-tool.js +5 -16
- package/dist/src/tools/delete-alarm-tool.js +1 -4
- package/dist/src/tools/device-tool-map.js +5 -1
- package/dist/src/tools/find-pc-devices-tool.d.ts +5 -0
- package/dist/src/tools/find-pc-devices-tool.js +98 -0
- package/dist/src/tools/get-alarm-tool-schema.d.ts +16 -0
- package/dist/src/tools/get-alarm-tool-schema.js +11 -0
- package/dist/src/tools/get-calendar-tool-schema.d.ts +16 -0
- package/dist/src/tools/get-calendar-tool-schema.js +9 -0
- package/dist/src/tools/get-collection-tool-schema.d.ts +16 -0
- package/dist/src/tools/get-collection-tool-schema.js +10 -0
- package/dist/src/tools/get-contact-tool-schema.d.ts +16 -0
- package/dist/src/tools/get-contact-tool-schema.js +11 -0
- package/dist/src/tools/get-device-file-tool-schema.d.ts +16 -0
- package/dist/src/tools/get-device-file-tool-schema.js +10 -0
- package/dist/src/tools/get-email-tool-schema.d.ts +16 -0
- package/dist/src/tools/get-email-tool-schema.js +9 -0
- package/dist/src/tools/get-note-tool-schema.d.ts +16 -0
- package/dist/src/tools/get-note-tool-schema.js +10 -0
- package/dist/src/tools/get-photo-tool-schema.d.ts +16 -0
- package/dist/src/tools/get-photo-tool-schema.js +10 -0
- package/dist/src/tools/image-reading-tool.js +4 -7
- package/dist/src/tools/login-token-tool.d.ts +5 -0
- package/dist/src/tools/login-token-tool.js +136 -0
- package/dist/src/tools/modify-alarm-tool.js +10 -23
- package/dist/src/tools/query-app-message-tool.d.ts +4 -0
- package/dist/src/tools/query-app-message-tool.js +138 -0
- package/dist/src/tools/query-memory-data-tool.d.ts +4 -0
- package/dist/src/tools/query-memory-data-tool.js +154 -0
- package/dist/src/tools/query-todo-task-tool.d.ts +4 -0
- package/dist/src/tools/query-todo-task-tool.js +133 -0
- package/dist/src/tools/save-file-to-phone-tool.d.ts +5 -0
- package/dist/src/tools/save-file-to-phone-tool.js +166 -0
- package/dist/src/tools/save-media-to-gallery-tool.js +13 -9
- package/dist/src/tools/save-self-evolution-skill-tool.d.ts +1 -0
- package/dist/src/tools/save-self-evolution-skill-tool.js +189 -0
- package/dist/src/tools/schema-tool-factory.d.ts +27 -0
- package/dist/src/tools/schema-tool-factory.js +32 -0
- package/dist/src/tools/search-alarm-tool.js +6 -13
- package/dist/src/tools/search-calendar-tool.js +2 -0
- package/dist/src/tools/search-email-tool.d.ts +5 -0
- package/dist/src/tools/search-email-tool.js +137 -0
- package/dist/src/tools/search-file-tool.js +4 -4
- package/dist/src/tools/search-message-tool.js +1 -0
- package/dist/src/tools/search-photo-gallery-tool.js +2 -2
- package/dist/src/tools/send-email-tool.d.ts +4 -0
- package/dist/src/tools/send-email-tool.js +134 -0
- package/dist/src/tools/send-file-to-user-tool.js +2 -4
- package/dist/src/tools/session-manager.js +2 -0
- package/dist/src/tools/upload-file-tool.js +4 -4
- package/dist/src/tools/upload-photo-tool.js +2 -2
- package/dist/src/tools/xiaoyi-add-collection-tool.js +35 -6
- package/dist/src/tools/xiaoyi-collection-tool.js +2 -1
- package/dist/src/tools/xiaoyi-delete-collection-tool.js +1 -1
- package/dist/src/utils/self-evolution-manager.d.ts +5 -0
- package/dist/src/utils/self-evolution-manager.js +47 -0
- package/dist/src/utils/tool-call-nudge-manager.d.ts +14 -0
- package/dist/src/utils/tool-call-nudge-manager.js +35 -0
- package/dist/src/websocket.js +18 -0
- package/openclaw.plugin.json +1 -0
- package/package.json +1 -1
package/dist/src/provider.js
CHANGED
|
@@ -1,107 +1,444 @@
|
|
|
1
|
+
// Xiaoyi Provider
|
|
2
|
+
// Wraps any OpenAI-compatible endpoint and injects dynamic headers
|
|
3
|
+
// (taskId, sessionId, conversationId) from the current XY channel session.
|
|
4
|
+
// Falls back to uid-based values when no session context is available.
|
|
5
|
+
//
|
|
6
|
+
// Users configure the underlying model in config:
|
|
7
|
+
// models.providers.xiaoyiprovider.baseUrl = "https://..."
|
|
8
|
+
// models.providers.xiaoyiprovider.api = "openai-completions"
|
|
9
|
+
// models.providers.xiaoyiprovider.models = [...]
|
|
10
|
+
import { createHash } from "crypto";
|
|
1
11
|
import { getCurrentSessionContext } from "./tools/session-manager.js";
|
|
12
|
+
import { selfEvolutionManager } from "./utils/self-evolution-manager.js";
|
|
13
|
+
// ── Retry config ──────────────────────────────────────────────
|
|
14
|
+
const RETRY_DELAYS_MS = [10_000, 20_000, 40_000, 60_000];
|
|
15
|
+
const MAX_RETRY_ATTEMPTS = 8;
|
|
16
|
+
/** Check if an errorMessage indicates a retryable provider error by type. */
|
|
17
|
+
function isRetryableProviderError(message) {
|
|
18
|
+
if (!message)
|
|
19
|
+
return false;
|
|
20
|
+
const lower = message.toLowerCase();
|
|
21
|
+
if (lower.includes("the server had an error while processing your request"))
|
|
22
|
+
return true;
|
|
23
|
+
if (lower.includes("rate limit reached for requests"))
|
|
24
|
+
return true;
|
|
25
|
+
if (lower.includes("现在访问有点拥挤,稍等一下再试会更顺畅哦~"))
|
|
26
|
+
return true;
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
/** Check if the request is triggered by a cron job by inspecting the first user message. */
|
|
30
|
+
function isCronTriggered(messages) {
|
|
31
|
+
if (!messages)
|
|
32
|
+
return false;
|
|
33
|
+
const firstUser = messages.find(m => m.role === "user");
|
|
34
|
+
if (!firstUser)
|
|
35
|
+
return false;
|
|
36
|
+
let text = "";
|
|
37
|
+
if (typeof firstUser.content === "string") {
|
|
38
|
+
text = firstUser.content;
|
|
39
|
+
}
|
|
40
|
+
else if (Array.isArray(firstUser.content)) {
|
|
41
|
+
const block = firstUser.content.find(b => b.type === "text" && typeof b.text === "string");
|
|
42
|
+
if (block)
|
|
43
|
+
text = block.text;
|
|
44
|
+
}
|
|
45
|
+
return /^\[cron:/i.test(text.trim());
|
|
46
|
+
}
|
|
47
|
+
/** Extract cron title from first user message matching `[cron:<uuid> <title>]`. */
|
|
48
|
+
function extractCronTitle(messages) {
|
|
49
|
+
if (!messages)
|
|
50
|
+
return undefined;
|
|
51
|
+
const firstUser = messages.find(m => m.role === "user");
|
|
52
|
+
if (!firstUser)
|
|
53
|
+
return undefined;
|
|
54
|
+
let text = "";
|
|
55
|
+
if (typeof firstUser.content === "string") {
|
|
56
|
+
text = firstUser.content;
|
|
57
|
+
}
|
|
58
|
+
else if (Array.isArray(firstUser.content)) {
|
|
59
|
+
const block = firstUser.content.find(b => b.type === "text" && typeof b.text === "string");
|
|
60
|
+
if (block)
|
|
61
|
+
text = block.text;
|
|
62
|
+
}
|
|
63
|
+
const match = text.trim().match(/^\[cron:[^\s]+\s+(.+)\]$/);
|
|
64
|
+
return match ? match[1] : undefined;
|
|
65
|
+
}
|
|
66
|
+
/** Compute retry delay in ms for the given 1-based attempt, with up to 10s jitter. */
|
|
67
|
+
function getRetryDelayMs(attempt, isCron = false) {
|
|
68
|
+
if (isCron) {
|
|
69
|
+
return 60_000 + Math.floor(Math.random() * 10_000);
|
|
70
|
+
}
|
|
71
|
+
const base = attempt <= RETRY_DELAYS_MS.length
|
|
72
|
+
? RETRY_DELAYS_MS[attempt - 1]
|
|
73
|
+
: RETRY_DELAYS_MS[RETRY_DELAYS_MS.length - 1];
|
|
74
|
+
const jitter = Math.floor(Math.random() * 10_000);
|
|
75
|
+
return base + jitter;
|
|
76
|
+
}
|
|
77
|
+
function sleep(ms) {
|
|
78
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Build a minimal EventStream-compatible object that replays a single
|
|
82
|
+
* done/error event. This avoids importing @mariozechner/pi-ai at runtime
|
|
83
|
+
* (the package is not available in the extension sandbox).
|
|
84
|
+
*/
|
|
85
|
+
function buildReplayStream(result) {
|
|
86
|
+
let settled = false;
|
|
87
|
+
const queued = [
|
|
88
|
+
result.stopReason === "error"
|
|
89
|
+
? { type: "error", reason: "error", error: result }
|
|
90
|
+
: { type: "done", reason: result.stopReason, message: result },
|
|
91
|
+
];
|
|
92
|
+
return {
|
|
93
|
+
result: () => Promise.resolve(result),
|
|
94
|
+
push: () => { },
|
|
95
|
+
end: () => { },
|
|
96
|
+
[Symbol.asyncIterator]: () => {
|
|
97
|
+
return {
|
|
98
|
+
next: async () => {
|
|
99
|
+
if (settled || queued.length === 0) {
|
|
100
|
+
settled = true;
|
|
101
|
+
return { value: undefined, done: true };
|
|
102
|
+
}
|
|
103
|
+
settled = true;
|
|
104
|
+
return { value: queued.shift(), done: false };
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Wrap the underlying stream with retry logic while preserving real-time streaming.
|
|
112
|
+
*
|
|
113
|
+
* Strategy:
|
|
114
|
+
* 1. Buffer events until the first content-bearing event is seen.
|
|
115
|
+
* 2. If the stream errors before any content, the buffer is tiny (start + error)
|
|
116
|
+
* and we can safely retry with a fresh API call.
|
|
117
|
+
* 3. Once content events appear, flush the buffer and switch to pass-through mode
|
|
118
|
+
* — the consumer sees every text_delta in real time.
|
|
119
|
+
*/
|
|
120
|
+
function createRetryingStream(createStream, cronJob) {
|
|
121
|
+
let resultResolve;
|
|
122
|
+
const resultPromise = new Promise(resolve => { resultResolve = resolve; });
|
|
123
|
+
const CONTENT_EVENT_TYPES = new Set([
|
|
124
|
+
"text_start", "text_delta", "text_end",
|
|
125
|
+
"thinking_start", "thinking_delta", "thinking_end",
|
|
126
|
+
"toolcall_start", "toolcall_delta", "toolcall_end",
|
|
127
|
+
]);
|
|
128
|
+
async function* retryGenerator() {
|
|
129
|
+
for (let attempt = 0; attempt < MAX_RETRY_ATTEMPTS; attempt++) {
|
|
130
|
+
const stream = await createStream();
|
|
131
|
+
let hasContent = false;
|
|
132
|
+
const buffer = [];
|
|
133
|
+
let errorResult = null;
|
|
134
|
+
for await (const event of stream) {
|
|
135
|
+
const isContent = CONTENT_EVENT_TYPES.has(event.type);
|
|
136
|
+
if (!hasContent && !isContent) {
|
|
137
|
+
// ── Buffer phase (no content yet) ──
|
|
138
|
+
if (event.type === "done") {
|
|
139
|
+
console.log(`[xiaoyiprovider] stream completed (no content), usage: input=${event.message?.usage?.input} output=${event.message?.usage?.output}`);
|
|
140
|
+
for (const b of buffer)
|
|
141
|
+
yield b;
|
|
142
|
+
resultResolve(event.message);
|
|
143
|
+
yield event;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (event.type === "error") {
|
|
147
|
+
errorResult = event.error;
|
|
148
|
+
}
|
|
149
|
+
buffer.push(event);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
// ── Streaming phase ──
|
|
153
|
+
if (!hasContent) {
|
|
154
|
+
console.log("[xiaoyiprovider] first content event received, switching to streaming mode");
|
|
155
|
+
hasContent = true;
|
|
156
|
+
for (const b of buffer)
|
|
157
|
+
yield b;
|
|
158
|
+
}
|
|
159
|
+
// IMPORTANT: resolve result() BEFORE yielding terminal events to avoid deadlock.
|
|
160
|
+
// The SDK calls result() when it sees done/error — if we yield first, the generator
|
|
161
|
+
// suspends and can never reach resolve, causing a permanent deadlock.
|
|
162
|
+
if (event.type === "done") {
|
|
163
|
+
console.log(`[xiaoyiprovider] stream completed, usage: input=${event.message?.usage?.input} output=${event.message?.usage?.output}`);
|
|
164
|
+
resultResolve(event.message);
|
|
165
|
+
yield event;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (event.type === "error") {
|
|
169
|
+
console.log(`[xiaoyiprovider] stream error after content: ${event.error?.errorMessage}`);
|
|
170
|
+
resultResolve(event.error);
|
|
171
|
+
yield event;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
yield event;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Stream ended during buffer phase — decide whether to retry
|
|
178
|
+
if (errorResult?.stopReason === "error" && isRetryableProviderError(errorResult.errorMessage)) {
|
|
179
|
+
if (attempt < MAX_RETRY_ATTEMPTS - 1) {
|
|
180
|
+
const delayMs = getRetryDelayMs(attempt + 1, cronJob);
|
|
181
|
+
console.log(`[xiaoyiprovider] retryable error (attempt ${attempt + 1}/${MAX_RETRY_ATTEMPTS}): ` +
|
|
182
|
+
`${errorResult.errorMessage} — retrying in ${delayMs}ms`);
|
|
183
|
+
await sleep(delayMs);
|
|
184
|
+
continue; // discard buffer, retry with a new stream
|
|
185
|
+
}
|
|
186
|
+
console.log(`[xiaoyiprovider] all ${MAX_RETRY_ATTEMPTS} retries exhausted, surfacing last error`);
|
|
187
|
+
}
|
|
188
|
+
else if (errorResult) {
|
|
189
|
+
console.log(`[xiaoyiprovider] non-retryable error: ${errorResult.errorMessage}`);
|
|
190
|
+
}
|
|
191
|
+
// Non-retryable or retries exhausted — yield buffered events.
|
|
192
|
+
// Resolve before yielding the terminal event to avoid the same deadlock.
|
|
193
|
+
for (const b of buffer) {
|
|
194
|
+
if (b.type === "done") {
|
|
195
|
+
resultResolve(b.message);
|
|
196
|
+
}
|
|
197
|
+
else if (b.type === "error") {
|
|
198
|
+
resultResolve(b.error);
|
|
199
|
+
}
|
|
200
|
+
yield b;
|
|
201
|
+
}
|
|
202
|
+
if (errorResult && buffer.every(b => b.type !== "done" && b.type !== "error")) {
|
|
203
|
+
resultResolve(errorResult);
|
|
204
|
+
}
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
// Safety: final fallback attempt
|
|
208
|
+
console.log("[xiaoyiprovider] entering final fallback attempt");
|
|
209
|
+
const lastStream = await createStream();
|
|
210
|
+
for await (const event of lastStream) {
|
|
211
|
+
if (event.type === "done") {
|
|
212
|
+
resultResolve(event.message);
|
|
213
|
+
yield event;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (event.type === "error") {
|
|
217
|
+
resultResolve(event.error);
|
|
218
|
+
yield event;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
yield event;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const gen = retryGenerator();
|
|
225
|
+
return {
|
|
226
|
+
result: () => resultPromise,
|
|
227
|
+
push: () => { },
|
|
228
|
+
end: () => { },
|
|
229
|
+
[Symbol.asyncIterator]: () => gen,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
2
232
|
/**
|
|
3
233
|
* Dynamic header keys injected via extraParams and forwarded to the HTTP request.
|
|
4
234
|
* Correspond to the three fields written to .xiaoyiruntime:
|
|
5
235
|
* TASK_ID, SESSION_ID, CONVERSATION_ID
|
|
6
236
|
*/
|
|
7
|
-
const
|
|
237
|
+
const HEADER_TRACE_ID = "x-hag-trace-id";
|
|
8
238
|
const HEADER_SESSION_ID = "x-session-id";
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
const
|
|
239
|
+
const HEADER_INTERACTION_ID = "x-interaction-id";
|
|
240
|
+
/** Internal key for passing fallback uid prefix from prepareExtraParams to wrapStreamFn. */
|
|
241
|
+
const FALLBACK_PREFIX_KEY = "_xiaoyi_fallback_prefix";
|
|
242
|
+
const SELF_EVOLUTION_PROMPT_BEGIN = "<self_evolution_prompt>";
|
|
243
|
+
const SELF_EVOLUTION_PROMPT_END = "</self_evolution_prompt>";
|
|
244
|
+
const SELF_EVOLUTION_ENABLED_PROMPT_SECTION = `
|
|
245
|
+
${SELF_EVOLUTION_PROMPT_BEGIN}
|
|
246
|
+
## Self Evolution(CRITICAL)
|
|
247
|
+
用户已启用自进化(不管之前是什么状态,不管之前的对话记录,当前视作已开启即可)。
|
|
248
|
+
自进化含义:当你识别出某个稳定、可复用的经验、调试模式或工作流程,并且它很可能对未来完成此用户的同类任务有帮助时,你可以考虑将其保存为可复用的经验。
|
|
249
|
+
|
|
250
|
+
[触发条件]
|
|
251
|
+
当满足以下任意一项时,必须触发自进化流程:
|
|
252
|
+
- 显式指令与关键词匹配(最高优先级):当用户消息中包含“进化”、“下次应该”、“以后必须”、“永远”等表达长期意图的词汇,或伴随明显的行为纠正、流程规范化或交付标准设定的指导动作等。
|
|
253
|
+
- 主动固化要求:用户主动要求将过去的交互经验(如交付件规格、skill选择、任务工作流等)进行沉淀/自进化。
|
|
254
|
+
- 隐式经验与避坑沉淀:当前任务执行过程中,你识别出了可以持久化为长期经验的标准工作流,或者在执行中遇到了工具调用错误、命令参数错误等明确的“踩坑点”,以后需要规避。
|
|
255
|
+
|
|
256
|
+
[触发后处理]
|
|
257
|
+
- 遵循 "self-evolution" 技能指导逐步进行技能评估。
|
|
258
|
+
- 只能在任务完成后,正式回复用户前读取该技能。
|
|
259
|
+
- 禁止直接修改文件。需要先创建提案,向用户发送进化请求,用户同意后才能进行更改。
|
|
260
|
+
${SELF_EVOLUTION_PROMPT_END}
|
|
261
|
+
`.trim();
|
|
262
|
+
const SELF_EVOLUTION_DISABLED_PROMPT_SECTION = `
|
|
263
|
+
${SELF_EVOLUTION_PROMPT_BEGIN}
|
|
264
|
+
## Self Evolution
|
|
265
|
+
用户当前已禁用自我进化。
|
|
266
|
+
你不得执行自进化相关行为,并且应将此功能视为不可用(不管之前是什么状态,不管之前的对话记录,当前视作已关闭即可)。
|
|
267
|
+
${SELF_EVOLUTION_PROMPT_END}
|
|
268
|
+
`.trim();
|
|
269
|
+
function stripSelfEvolutionPrompt(prompt) {
|
|
270
|
+
return prompt
|
|
271
|
+
.replace(/\n*<self_evolution_prompt>[\s\S]*?<\/self_evolution_prompt>\n*/gu, "\n\n")
|
|
272
|
+
.replace(/\n{3,}/gu, "\n\n")
|
|
273
|
+
.trim();
|
|
274
|
+
}
|
|
13
275
|
/**
|
|
14
|
-
* Encode uid
|
|
276
|
+
* Encode uid via SHA-256 and take first 32 hex chars.
|
|
15
277
|
*/
|
|
16
278
|
function encodeUid(uid) {
|
|
17
|
-
return
|
|
279
|
+
return createHash("sha256").update(uid).digest("hex").slice(0, 32);
|
|
18
280
|
}
|
|
19
281
|
/**
|
|
20
|
-
* Get uid from
|
|
282
|
+
* Get uid from channel config (OpenClawConfig -> channels -> xiaoyi-channel -> uid).
|
|
21
283
|
*/
|
|
22
284
|
function getUidFromConfig(config) {
|
|
23
|
-
return config?.
|
|
285
|
+
return config?.channels?.["xiaoyi-channel"]?.uid;
|
|
24
286
|
}
|
|
25
287
|
export const xiaoyiProvider = {
|
|
26
288
|
id: "xiaoyiprovider",
|
|
27
289
|
label: "Xiaoyi Provider",
|
|
28
290
|
docsPath: "/providers/models",
|
|
29
291
|
auth: [],
|
|
292
|
+
isCacheTtlEligible: () => true,
|
|
30
293
|
/**
|
|
31
294
|
* Inject dynamic session params into extraParams so they flow
|
|
32
295
|
* through to wrapStreamFn's ctx.extraParams.
|
|
33
296
|
*
|
|
34
297
|
* Priority:
|
|
35
298
|
* 1. Session context (from AsyncLocalStorage, set by bot.ts)
|
|
36
|
-
* 2. uid-based fallback:
|
|
299
|
+
* 2. uid-based fallback: sha256(uid).hex[:32]_timestamp
|
|
37
300
|
* 3. No uid available → return undefined (no headers injected)
|
|
38
301
|
*/
|
|
39
302
|
prepareExtraParams: (ctx) => {
|
|
40
303
|
const sessionCtx = getCurrentSessionContext();
|
|
41
304
|
if (sessionCtx) {
|
|
305
|
+
const taskId = sessionCtx.taskId;
|
|
306
|
+
const sessionId = taskId.split("&")[0];
|
|
307
|
+
const interactionId = taskId.split("&")[1] || "";
|
|
42
308
|
return {
|
|
43
309
|
...ctx.extraParams,
|
|
44
|
-
[
|
|
45
|
-
[
|
|
46
|
-
[
|
|
310
|
+
[HEADER_TRACE_ID]: taskId,
|
|
311
|
+
[HEADER_SESSION_ID]: sessionId,
|
|
312
|
+
[HEADER_INTERACTION_ID]: interactionId,
|
|
47
313
|
};
|
|
48
314
|
}
|
|
49
|
-
// Fallback: uid
|
|
315
|
+
// Fallback: store uid prefix for lazy timestamp generation in wrapStreamFn.
|
|
316
|
+
// This ensures each model call gets a fresh timestamp instead of reusing
|
|
317
|
+
// the same one across tool-use loops and retries.
|
|
50
318
|
const uid = getUidFromConfig(ctx.config);
|
|
51
319
|
if (!uid)
|
|
52
320
|
return undefined;
|
|
53
|
-
const prefix = encodeUid(uid);
|
|
54
|
-
const ts = Date.now();
|
|
55
|
-
const fallbackValue = `${prefix}_${ts}`;
|
|
56
321
|
return {
|
|
57
322
|
...ctx.extraParams,
|
|
58
|
-
[
|
|
59
|
-
[EXTRA_PARAM_SESSION_ID]: fallbackValue,
|
|
60
|
-
[EXTRA_PARAM_CONVERSATION_ID]: fallbackValue,
|
|
323
|
+
[FALLBACK_PREFIX_KEY]: encodeUid(uid),
|
|
61
324
|
};
|
|
62
325
|
},
|
|
63
326
|
/**
|
|
64
327
|
* Wrap the stream function to inject dynamic headers into every
|
|
65
|
-
* HTTP request to the model provider
|
|
328
|
+
* HTTP request to the model provider, and retry on retryable errors
|
|
329
|
+
* (server_error / rate_limit_error) with backoff: 10s, 20s, 40s, 60s (cap).
|
|
66
330
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
331
|
+
* The retry loop awaits stream.result() to detect errors before deciding
|
|
332
|
+
* whether to retry. This keeps the agent loop waiting (no timeout risk
|
|
333
|
+
* since the default agent timeout is 48 hours).
|
|
69
334
|
*/
|
|
70
335
|
wrapStreamFn: (ctx) => {
|
|
71
336
|
const underlying = ctx.streamFn;
|
|
72
337
|
if (!underlying)
|
|
73
338
|
return underlying;
|
|
74
|
-
const dynamicHeaders = {};
|
|
75
|
-
if (ctx.extraParams) {
|
|
76
|
-
const taskId = ctx.extraParams[EXTRA_PARAM_TASK_ID];
|
|
77
|
-
const sessionId = ctx.extraParams[EXTRA_PARAM_SESSION_ID];
|
|
78
|
-
const conversationId = ctx.extraParams[EXTRA_PARAM_CONVERSATION_ID];
|
|
79
|
-
if (typeof taskId === "string")
|
|
80
|
-
dynamicHeaders[HEADER_TASK_ID] = taskId;
|
|
81
|
-
if (typeof sessionId === "string")
|
|
82
|
-
dynamicHeaders[HEADER_SESSION_ID] = sessionId;
|
|
83
|
-
if (typeof conversationId === "string")
|
|
84
|
-
dynamicHeaders[HEADER_CONVERSATION_ID] = conversationId;
|
|
85
|
-
}
|
|
86
|
-
if (Object.keys(dynamicHeaders).length === 0)
|
|
87
|
-
return underlying;
|
|
88
339
|
return async (model, context, options) => {
|
|
340
|
+
// 每次请求时从 ctx.extraParams 动态读取 header
|
|
341
|
+
const dynamicHeaders = {};
|
|
342
|
+
if (ctx.extraParams) {
|
|
343
|
+
const fallbackPrefix = ctx.extraParams[FALLBACK_PREFIX_KEY];
|
|
344
|
+
if (typeof fallbackPrefix === "string") {
|
|
345
|
+
// Fallback mode: generate fresh timestamp per request
|
|
346
|
+
const isCron = isCronTriggered(context.messages);
|
|
347
|
+
const fallbackValue = `${fallbackPrefix}_${Date.now()}`;
|
|
348
|
+
dynamicHeaders[HEADER_TRACE_ID] = isCron ? `cron_${fallbackValue}` : fallbackValue;
|
|
349
|
+
dynamicHeaders[HEADER_SESSION_ID] = fallbackValue;
|
|
350
|
+
dynamicHeaders[HEADER_INTERACTION_ID] = fallbackValue;
|
|
351
|
+
if (isCron) {
|
|
352
|
+
const cronTitle = extractCronTitle(context.messages);
|
|
353
|
+
if (cronTitle)
|
|
354
|
+
dynamicHeaders["x-cron-title"] = cronTitle;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
// Session mode: use pre-resolved session headers + fresh timestamp
|
|
359
|
+
const traceId = ctx.extraParams[HEADER_TRACE_ID];
|
|
360
|
+
const sessionId = ctx.extraParams[HEADER_SESSION_ID];
|
|
361
|
+
const interactionId = ctx.extraParams[HEADER_INTERACTION_ID];
|
|
362
|
+
const ts = `_${Date.now()}`;
|
|
363
|
+
if (typeof traceId === "string") {
|
|
364
|
+
const isCron = isCronTriggered(context.messages);
|
|
365
|
+
dynamicHeaders[HEADER_TRACE_ID] = isCron ? `cron_${traceId}${ts}` : `${traceId}${ts}`;
|
|
366
|
+
if (isCron) {
|
|
367
|
+
const cronTitle = extractCronTitle(context.messages);
|
|
368
|
+
if (cronTitle)
|
|
369
|
+
dynamicHeaders["x-cron-title"] = cronTitle;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (typeof sessionId === "string")
|
|
373
|
+
dynamicHeaders[HEADER_SESSION_ID] = sessionId;
|
|
374
|
+
if (typeof interactionId === "string")
|
|
375
|
+
dynamicHeaders[HEADER_INTERACTION_ID] = interactionId;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
89
378
|
// 记录输入
|
|
90
|
-
console.log(`[xiaoyiprovider] input messages count: ${context.messages
|
|
379
|
+
console.log(`[xiaoyiprovider] input messages count: ${context.messages?.length ?? 0}`);
|
|
91
380
|
if (context.systemPrompt) {
|
|
92
381
|
console.log(`[xiaoyiprovider] system prompt length: ${context.systemPrompt.length}`);
|
|
93
382
|
}
|
|
94
|
-
|
|
95
|
-
|
|
383
|
+
const sessionCtx = getCurrentSessionContext();
|
|
384
|
+
// 在发送给模型前,优化 systemPrompt 结构
|
|
385
|
+
if (context.systemPrompt) {
|
|
386
|
+
let sp = context.systemPrompt;
|
|
387
|
+
const beforeLen = sp.length;
|
|
388
|
+
// 删除 ## Tooling 与 TOOLS.md 声明之间的内容
|
|
389
|
+
sp = sp.replace(/(## Tooling)[\s\S]*?(TOOLS\.md does not control tool availability; it is user guidance for how to use external tools\.)/, "$1\n\n$2");
|
|
390
|
+
// (1) 提取 ## Skills (mandatory) 到 </available_skills> 作为第一部分
|
|
391
|
+
const skillsMatch = sp.match(/(## Skills \(mandatory\)[\s\S]*?<\/available_skills>)/);
|
|
392
|
+
const part1 = skillsMatch ? skillsMatch[0] : '';
|
|
393
|
+
// (2) 提取 ## /home/sandbox/.openclaw/workspace/SOUL.md 到 ## /home/sandbox/.openclaw/workspace/TOOLS.md 之前的内容作为第二部分
|
|
394
|
+
const soulMatch = sp.match(/(## \/home\/sandbox\/\.openclaw\/workspace\/SOUL\.md[\s\S]*?)(?=## \/home\/sandbox\/\.openclaw\/workspace\/TOOLS\.md)/);
|
|
395
|
+
const part2 = soulMatch ? soulMatch[1].trim() : '';
|
|
396
|
+
if (part1 || part2) {
|
|
397
|
+
// 从原始位置删除已提取的部分
|
|
398
|
+
if (skillsMatch)
|
|
399
|
+
sp = sp.replace(skillsMatch[0], '');
|
|
400
|
+
if (soulMatch)
|
|
401
|
+
sp = sp.replace(soulMatch[1], '');
|
|
402
|
+
// 清理多余空行
|
|
403
|
+
sp = sp.replace(/\n{3,}/g, '\n\n');
|
|
404
|
+
// (3) 将 第二部分 + 第一部分 插入到 ## Runtime 上面
|
|
405
|
+
const combined = (part2 + '\n\n' + part1).trim();
|
|
406
|
+
if (combined && sp.includes('## Runtime')) {
|
|
407
|
+
sp = sp.replace('## Runtime', combined + '\n\n## Runtime');
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
console.log(`[xiaoyiprovider] system prompt optimized: ${beforeLen} -> ${sp.length}`);
|
|
411
|
+
context.systemPrompt = sp;
|
|
412
|
+
}
|
|
413
|
+
const selfEvolutionEnabled = await selfEvolutionManager.isEnabled();
|
|
414
|
+
const prompt = stripSelfEvolutionPrompt(context.systemPrompt ?? "");
|
|
415
|
+
context.systemPrompt = [
|
|
416
|
+
prompt,
|
|
417
|
+
selfEvolutionEnabled
|
|
418
|
+
? SELF_EVOLUTION_ENABLED_PROMPT_SECTION
|
|
419
|
+
: SELF_EVOLUTION_DISABLED_PROMPT_SECTION,
|
|
420
|
+
]
|
|
421
|
+
.filter(Boolean)
|
|
422
|
+
.join("\n\n");
|
|
423
|
+
// Append device context to systemPrompt
|
|
424
|
+
if (sessionCtx?.deviceType) {
|
|
425
|
+
const rawDevice = sessionCtx.deviceType;
|
|
426
|
+
const displayDevice = (rawDevice === "2in1") ? "鸿蒙PC" : rawDevice;
|
|
427
|
+
const deviceSection = `\n\n## Current User Device Context\nThe current user is using the following device: ${displayDevice}\nYou need to be aware of the user's current device and provide guidance accordingly. If the response involves device-related tools or actions, you must tailor the reply based on the user's current device, using device-specific references such as "saved to the Notes/Calendar on your {deviceType}.\n"`;
|
|
428
|
+
context.systemPrompt = (context.systemPrompt ?? "") + deviceSection;
|
|
429
|
+
}
|
|
430
|
+
// ── Retry-capable streaming ──────────────────────────────
|
|
431
|
+
const cronJob = isCronTriggered(context.messages);
|
|
432
|
+
if (cronJob)
|
|
433
|
+
console.log("[xiaoyiprovider] detected cron-triggered request, using extended retry delays");
|
|
434
|
+
const makeStream = () => underlying(model, context, {
|
|
96
435
|
...options,
|
|
97
436
|
headers: {
|
|
98
437
|
...options?.headers,
|
|
99
438
|
...dynamicHeaders,
|
|
100
439
|
},
|
|
101
440
|
});
|
|
102
|
-
|
|
103
|
-
stream.result().then((msg) => console.log(`[xiaoyiprovider] output: ${JSON.stringify(msg)}`), (err) => console.log(`[xiaoyiprovider] error: ${err}`));
|
|
104
|
-
return stream;
|
|
441
|
+
return createRetryingStream(makeStream, cronJob);
|
|
105
442
|
};
|
|
106
443
|
},
|
|
107
444
|
};
|
|
@@ -267,6 +267,12 @@ export function createXYReplyDispatcher(params) {
|
|
|
267
267
|
log(`[TOOL START] Tool: ${name}, phase: ${phase}, taskId: ${currentTaskId}`);
|
|
268
268
|
if (phase === "start") {
|
|
269
269
|
const toolName = name || "unknown";
|
|
270
|
+
// call_device_tool 由自身 execute() 内部发送具体子工具名的状态更新
|
|
271
|
+
// get_xxx_tool_schema 是给 LLM 查 schema 用的,无需向用户展示
|
|
272
|
+
if (toolName === "call_device_tool" || toolName.endsWith("_tool_schema")) {
|
|
273
|
+
log(`[TOOL START] Skipping generic status for ${toolName}`);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
270
276
|
try {
|
|
271
277
|
await sendStatusUpdate({
|
|
272
278
|
config,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function handleSelfEvolutionEvent(context: any, runtime: any): void;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
2
|
+
const XIAOYIENV_PATH = "/home/sandbox/.openclaw/.xiaoyienv";
|
|
3
|
+
export function handleSelfEvolutionEvent(context, runtime) {
|
|
4
|
+
const log = runtime?.log ?? console.log;
|
|
5
|
+
const error = runtime?.error ?? console.error;
|
|
6
|
+
try {
|
|
7
|
+
const state = context.event?.payload?.selfEvolutionState;
|
|
8
|
+
if (typeof state !== "string") {
|
|
9
|
+
error("[SELF_EVOLUTION] invalid payload: missing selfEvolutionState");
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
log(`[SELF_EVOLUTION] received state: ${state}`);
|
|
13
|
+
let content;
|
|
14
|
+
try {
|
|
15
|
+
content = readFileSync(XIAOYIENV_PATH, "utf-8");
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// File doesn't exist yet — create it
|
|
19
|
+
log(`[SELF_EVOLUTION] ${XIAOYIENV_PATH} not found, creating new file`);
|
|
20
|
+
writeFileSync(XIAOYIENV_PATH, `selfEvolutionState=${state}\n`, "utf-8");
|
|
21
|
+
log(`[SELF_EVOLUTION] wrote selfEvolutionState=${state}`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const lines = content.split("\n");
|
|
25
|
+
const key = "selfEvolutionState";
|
|
26
|
+
let found = false;
|
|
27
|
+
const updated = lines.map((line) => {
|
|
28
|
+
if (line.startsWith(`${key}=`)) {
|
|
29
|
+
found = true;
|
|
30
|
+
return `${key}=${state}`;
|
|
31
|
+
}
|
|
32
|
+
return line;
|
|
33
|
+
});
|
|
34
|
+
if (!found) {
|
|
35
|
+
// Ensure trailing newline before appending
|
|
36
|
+
const trimmed = content.trimEnd();
|
|
37
|
+
writeFileSync(XIAOYIENV_PATH, `${trimmed}\n${key}=${state}\n`, "utf-8");
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
writeFileSync(XIAOYIENV_PATH, updated.join("\n"), "utf-8");
|
|
41
|
+
}
|
|
42
|
+
log(`[SELF_EVOLUTION] updated selfEvolutionState=${state} in ${XIAOYIENV_PATH}`);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
error("[SELF_EVOLUTION] failed to handle event:", err);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const DEFAULT_CONFIG = {
|
|
2
|
+
enabled: true,
|
|
3
|
+
maxTools: 2,
|
|
4
|
+
includeUninstalledOnly: true,
|
|
5
|
+
envFilePath: "~/.openclaw/.xiaoyienv",
|
|
6
|
+
timeoutMs: 1000,
|
|
7
|
+
};
|
|
8
|
+
export function normalizeToolRetrieverConfig(raw) {
|
|
9
|
+
if (!raw || typeof raw !== "object") {
|
|
10
|
+
return { ...DEFAULT_CONFIG };
|
|
11
|
+
}
|
|
12
|
+
const cfg = raw;
|
|
13
|
+
return {
|
|
14
|
+
enabled: cfg.enabled ?? DEFAULT_CONFIG.enabled,
|
|
15
|
+
maxTools: Math.min(20, Math.max(1, cfg.maxTools ?? DEFAULT_CONFIG.maxTools)),
|
|
16
|
+
includeUninstalledOnly: cfg.includeUninstalledOnly ?? DEFAULT_CONFIG.includeUninstalledOnly,
|
|
17
|
+
envFilePath: cfg.envFilePath ?? DEFAULT_CONFIG.envFilePath,
|
|
18
|
+
serviceUrl: cfg.serviceUrl,
|
|
19
|
+
apiKey: cfg.apiKey,
|
|
20
|
+
uid: cfg.uid,
|
|
21
|
+
timeoutMs: cfg.timeoutMs ?? DEFAULT_CONFIG.timeoutMs,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
interface PluginHookBeforePromptBuildEvent {
|
|
2
|
+
prompt: string;
|
|
3
|
+
messages: unknown[];
|
|
4
|
+
}
|
|
5
|
+
interface PluginHookBeforePromptBuildResult {
|
|
6
|
+
systemPrompt?: string;
|
|
7
|
+
prependContext?: string;
|
|
8
|
+
prependSystemContext?: string;
|
|
9
|
+
appendSystemContext?: string;
|
|
10
|
+
}
|
|
11
|
+
interface PluginHookAgentContext {
|
|
12
|
+
agentId?: string;
|
|
13
|
+
sessionKey?: string;
|
|
14
|
+
sessionId?: string;
|
|
15
|
+
workspaceDir?: string;
|
|
16
|
+
messageProvider?: string;
|
|
17
|
+
trigger?: string;
|
|
18
|
+
channelId?: string;
|
|
19
|
+
}
|
|
20
|
+
import type { ToolRetrieverConfig } from "./types.js";
|
|
21
|
+
export declare function createBeforePromptBuildHandler(config: ToolRetrieverConfig): (event: PluginHookBeforePromptBuildEvent, ctx?: PluginHookAgentContext) => Promise<PluginHookBeforePromptBuildResult | undefined>;
|
|
22
|
+
export {};
|