@ynhcj/xiaoyi-channel 0.0.63-beta → 0.0.63-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 +7 -4
- package/dist/src/bot.js +8 -1
- package/dist/src/channel.js +22 -19
- 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/outbound.js +2 -7
- package/dist/src/parser.d.ts +6 -0
- package/dist/src/parser.js +16 -0
- package/dist/src/provider.d.ts +2 -0
- package/dist/src/provider.js +355 -0
- package/dist/src/reply-dispatcher.js +6 -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.d.ts +4 -0
- package/dist/src/tools/device-tool-map.js +37 -0
- 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/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.d.ts +5 -0
- package/dist/src/tools/save-media-to-gallery-tool.js +174 -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.d.ts +1 -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.d.ts +4 -0
- package/dist/src/tools/xiaoyi-add-collection-tool.js +192 -0
- package/dist/src/tools/xiaoyi-collection-tool.js +43 -7
- package/dist/src/tools/xiaoyi-delete-collection-tool.d.ts +4 -0
- package/dist/src/tools/xiaoyi-delete-collection-tool.js +163 -0
- package/openclaw.plugin.json +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,355 @@
|
|
|
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";
|
|
11
|
+
import { getCurrentSessionContext } from "./tools/session-manager.js";
|
|
12
|
+
// ── Retry config ──────────────────────────────────────────────
|
|
13
|
+
const RETRY_DELAYS_MS = [10_000, 20_000, 40_000, 60_000];
|
|
14
|
+
const MAX_RETRY_ATTEMPTS = 8;
|
|
15
|
+
/** Check if an errorMessage indicates a retryable provider error by type. */
|
|
16
|
+
function isRetryableProviderError(message) {
|
|
17
|
+
if (!message)
|
|
18
|
+
return false;
|
|
19
|
+
const lower = message.toLowerCase();
|
|
20
|
+
if (lower.includes("the server had an error while processing your request"))
|
|
21
|
+
return true;
|
|
22
|
+
if (lower.includes("rate limit reached for requests"))
|
|
23
|
+
return true;
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
/** Check if the request is triggered by a cron job by inspecting the first user message. */
|
|
27
|
+
function isCronTriggered(messages) {
|
|
28
|
+
if (!messages)
|
|
29
|
+
return false;
|
|
30
|
+
const firstUser = messages.find(m => m.role === "user");
|
|
31
|
+
if (!firstUser)
|
|
32
|
+
return false;
|
|
33
|
+
let text = "";
|
|
34
|
+
if (typeof firstUser.content === "string") {
|
|
35
|
+
text = firstUser.content;
|
|
36
|
+
}
|
|
37
|
+
else if (Array.isArray(firstUser.content)) {
|
|
38
|
+
const block = firstUser.content.find(b => b.type === "text" && typeof b.text === "string");
|
|
39
|
+
if (block)
|
|
40
|
+
text = block.text;
|
|
41
|
+
}
|
|
42
|
+
return /^\[cron:/i.test(text.trim());
|
|
43
|
+
}
|
|
44
|
+
/** Compute retry delay in ms for the given 1-based attempt, with up to 10s jitter. */
|
|
45
|
+
function getRetryDelayMs(attempt, isCron = false) {
|
|
46
|
+
if (isCron) {
|
|
47
|
+
return 60_000 + Math.floor(Math.random() * 10_000);
|
|
48
|
+
}
|
|
49
|
+
const base = attempt <= RETRY_DELAYS_MS.length
|
|
50
|
+
? RETRY_DELAYS_MS[attempt - 1]
|
|
51
|
+
: RETRY_DELAYS_MS[RETRY_DELAYS_MS.length - 1];
|
|
52
|
+
const jitter = Math.floor(Math.random() * 10_000);
|
|
53
|
+
return base + jitter;
|
|
54
|
+
}
|
|
55
|
+
function sleep(ms) {
|
|
56
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Build a minimal EventStream-compatible object that replays a single
|
|
60
|
+
* done/error event. This avoids importing @mariozechner/pi-ai at runtime
|
|
61
|
+
* (the package is not available in the extension sandbox).
|
|
62
|
+
*/
|
|
63
|
+
function buildReplayStream(result) {
|
|
64
|
+
let settled = false;
|
|
65
|
+
const queued = [
|
|
66
|
+
result.stopReason === "error"
|
|
67
|
+
? { type: "error", reason: "error", error: result }
|
|
68
|
+
: { type: "done", reason: result.stopReason, message: result },
|
|
69
|
+
];
|
|
70
|
+
return {
|
|
71
|
+
result: () => Promise.resolve(result),
|
|
72
|
+
push: () => { },
|
|
73
|
+
end: () => { },
|
|
74
|
+
[Symbol.asyncIterator]: () => {
|
|
75
|
+
return {
|
|
76
|
+
next: async () => {
|
|
77
|
+
if (settled || queued.length === 0) {
|
|
78
|
+
settled = true;
|
|
79
|
+
return { value: undefined, done: true };
|
|
80
|
+
}
|
|
81
|
+
settled = true;
|
|
82
|
+
return { value: queued.shift(), done: false };
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Wrap the underlying stream with retry logic while preserving real-time streaming.
|
|
90
|
+
*
|
|
91
|
+
* Strategy:
|
|
92
|
+
* 1. Buffer events until the first content-bearing event is seen.
|
|
93
|
+
* 2. If the stream errors before any content, the buffer is tiny (start + error)
|
|
94
|
+
* and we can safely retry with a fresh API call.
|
|
95
|
+
* 3. Once content events appear, flush the buffer and switch to pass-through mode
|
|
96
|
+
* — the consumer sees every text_delta in real time.
|
|
97
|
+
*/
|
|
98
|
+
function createRetryingStream(createStream, cronJob) {
|
|
99
|
+
let resultResolve;
|
|
100
|
+
const resultPromise = new Promise(resolve => { resultResolve = resolve; });
|
|
101
|
+
const CONTENT_EVENT_TYPES = new Set([
|
|
102
|
+
"text_start", "text_delta", "text_end",
|
|
103
|
+
"thinking_start", "thinking_delta", "thinking_end",
|
|
104
|
+
"toolcall_start", "toolcall_delta", "toolcall_end",
|
|
105
|
+
]);
|
|
106
|
+
async function* retryGenerator() {
|
|
107
|
+
for (let attempt = 0; attempt < MAX_RETRY_ATTEMPTS; attempt++) {
|
|
108
|
+
const stream = await createStream();
|
|
109
|
+
let hasContent = false;
|
|
110
|
+
const buffer = [];
|
|
111
|
+
let errorResult = null;
|
|
112
|
+
for await (const event of stream) {
|
|
113
|
+
const isContent = CONTENT_EVENT_TYPES.has(event.type);
|
|
114
|
+
if (!hasContent && !isContent) {
|
|
115
|
+
// ── Buffer phase (no content yet) ──
|
|
116
|
+
if (event.type === "done") {
|
|
117
|
+
console.log(`[xiaoyiprovider] stream completed (no content), usage: input=${event.message?.usage?.input} output=${event.message?.usage?.output}`);
|
|
118
|
+
for (const b of buffer)
|
|
119
|
+
yield b;
|
|
120
|
+
resultResolve(event.message);
|
|
121
|
+
yield event;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (event.type === "error") {
|
|
125
|
+
errorResult = event.error;
|
|
126
|
+
}
|
|
127
|
+
buffer.push(event);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
// ── Streaming phase ──
|
|
131
|
+
if (!hasContent) {
|
|
132
|
+
console.log("[xiaoyiprovider] first content event received, switching to streaming mode");
|
|
133
|
+
hasContent = true;
|
|
134
|
+
for (const b of buffer)
|
|
135
|
+
yield b;
|
|
136
|
+
}
|
|
137
|
+
// IMPORTANT: resolve result() BEFORE yielding terminal events to avoid deadlock.
|
|
138
|
+
// The SDK calls result() when it sees done/error — if we yield first, the generator
|
|
139
|
+
// suspends and can never reach resolve, causing a permanent deadlock.
|
|
140
|
+
if (event.type === "done") {
|
|
141
|
+
console.log(`[xiaoyiprovider] stream completed, usage: input=${event.message?.usage?.input} output=${event.message?.usage?.output}`);
|
|
142
|
+
resultResolve(event.message);
|
|
143
|
+
yield event;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (event.type === "error") {
|
|
147
|
+
console.log(`[xiaoyiprovider] stream error after content: ${event.error?.errorMessage}`);
|
|
148
|
+
resultResolve(event.error);
|
|
149
|
+
yield event;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
yield event;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Stream ended during buffer phase — decide whether to retry
|
|
156
|
+
if (errorResult?.stopReason === "error" && isRetryableProviderError(errorResult.errorMessage)) {
|
|
157
|
+
if (attempt < MAX_RETRY_ATTEMPTS - 1) {
|
|
158
|
+
const delayMs = getRetryDelayMs(attempt + 1, cronJob);
|
|
159
|
+
console.log(`[xiaoyiprovider] retryable error (attempt ${attempt + 1}/${MAX_RETRY_ATTEMPTS}): ` +
|
|
160
|
+
`${errorResult.errorMessage} — retrying in ${delayMs}ms`);
|
|
161
|
+
await sleep(delayMs);
|
|
162
|
+
continue; // discard buffer, retry with a new stream
|
|
163
|
+
}
|
|
164
|
+
console.log(`[xiaoyiprovider] all ${MAX_RETRY_ATTEMPTS} retries exhausted, surfacing last error`);
|
|
165
|
+
}
|
|
166
|
+
else if (errorResult) {
|
|
167
|
+
console.log(`[xiaoyiprovider] non-retryable error: ${errorResult.errorMessage}`);
|
|
168
|
+
}
|
|
169
|
+
// Non-retryable or retries exhausted — yield buffered events.
|
|
170
|
+
// Resolve before yielding the terminal event to avoid the same deadlock.
|
|
171
|
+
for (const b of buffer) {
|
|
172
|
+
if (b.type === "done") {
|
|
173
|
+
resultResolve(b.message);
|
|
174
|
+
}
|
|
175
|
+
else if (b.type === "error") {
|
|
176
|
+
resultResolve(b.error);
|
|
177
|
+
}
|
|
178
|
+
yield b;
|
|
179
|
+
}
|
|
180
|
+
if (errorResult && buffer.every(b => b.type !== "done" && b.type !== "error")) {
|
|
181
|
+
resultResolve(errorResult);
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
// Safety: final fallback attempt
|
|
186
|
+
console.log("[xiaoyiprovider] entering final fallback attempt");
|
|
187
|
+
const lastStream = await createStream();
|
|
188
|
+
for await (const event of lastStream) {
|
|
189
|
+
if (event.type === "done") {
|
|
190
|
+
resultResolve(event.message);
|
|
191
|
+
yield event;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (event.type === "error") {
|
|
195
|
+
resultResolve(event.error);
|
|
196
|
+
yield event;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
yield event;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const gen = retryGenerator();
|
|
203
|
+
return {
|
|
204
|
+
result: () => resultPromise,
|
|
205
|
+
push: () => { },
|
|
206
|
+
end: () => { },
|
|
207
|
+
[Symbol.asyncIterator]: () => gen,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Dynamic header keys injected via extraParams and forwarded to the HTTP request.
|
|
212
|
+
* Correspond to the three fields written to .xiaoyiruntime:
|
|
213
|
+
* TASK_ID, SESSION_ID, CONVERSATION_ID
|
|
214
|
+
*/
|
|
215
|
+
const HEADER_TRACE_ID = "x-hag-trace-id";
|
|
216
|
+
const HEADER_SESSION_ID = "x-session-id";
|
|
217
|
+
const HEADER_INTERACTION_ID = "x-interaction-id";
|
|
218
|
+
/**
|
|
219
|
+
* Encode uid via SHA-256 and take first 32 hex chars.
|
|
220
|
+
*/
|
|
221
|
+
function encodeUid(uid) {
|
|
222
|
+
return createHash("sha256").update(uid).digest("hex").slice(0, 32);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Get uid from channel config (OpenClawConfig -> channels -> xiaoyi-channel -> uid).
|
|
226
|
+
*/
|
|
227
|
+
function getUidFromConfig(config) {
|
|
228
|
+
return config?.channels?.["xiaoyi-channel"]?.uid;
|
|
229
|
+
}
|
|
230
|
+
export const xiaoyiProvider = {
|
|
231
|
+
id: "xiaoyiprovider",
|
|
232
|
+
label: "Xiaoyi Provider",
|
|
233
|
+
docsPath: "/providers/models",
|
|
234
|
+
auth: [],
|
|
235
|
+
isCacheTtlEligible: () => true,
|
|
236
|
+
/**
|
|
237
|
+
* Inject dynamic session params into extraParams so they flow
|
|
238
|
+
* through to wrapStreamFn's ctx.extraParams.
|
|
239
|
+
*
|
|
240
|
+
* Priority:
|
|
241
|
+
* 1. Session context (from AsyncLocalStorage, set by bot.ts)
|
|
242
|
+
* 2. uid-based fallback: sha256(uid).hex[:32]_timestamp
|
|
243
|
+
* 3. No uid available → return undefined (no headers injected)
|
|
244
|
+
*/
|
|
245
|
+
prepareExtraParams: (ctx) => {
|
|
246
|
+
const sessionCtx = getCurrentSessionContext();
|
|
247
|
+
if (sessionCtx) {
|
|
248
|
+
const taskId = sessionCtx.taskId;
|
|
249
|
+
const sessionId = taskId.split("&")[0];
|
|
250
|
+
const interactionId = taskId.split("&")[1] || "";
|
|
251
|
+
return {
|
|
252
|
+
...ctx.extraParams,
|
|
253
|
+
[HEADER_TRACE_ID]: taskId,
|
|
254
|
+
[HEADER_SESSION_ID]: sessionId,
|
|
255
|
+
[HEADER_INTERACTION_ID]: interactionId,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
// Fallback: uid-based values
|
|
259
|
+
const uid = getUidFromConfig(ctx.config);
|
|
260
|
+
if (!uid)
|
|
261
|
+
return undefined;
|
|
262
|
+
const prefix = encodeUid(uid);
|
|
263
|
+
const ts = Date.now();
|
|
264
|
+
const fallbackValue = `${prefix}_${ts}`;
|
|
265
|
+
return {
|
|
266
|
+
...ctx.extraParams,
|
|
267
|
+
[HEADER_TRACE_ID]: fallbackValue,
|
|
268
|
+
[HEADER_SESSION_ID]: fallbackValue,
|
|
269
|
+
[HEADER_INTERACTION_ID]: fallbackValue,
|
|
270
|
+
};
|
|
271
|
+
},
|
|
272
|
+
/**
|
|
273
|
+
* Wrap the stream function to inject dynamic headers into every
|
|
274
|
+
* HTTP request to the model provider, and retry on retryable errors
|
|
275
|
+
* (server_error / rate_limit_error) with backoff: 10s, 20s, 40s, 60s (cap).
|
|
276
|
+
*
|
|
277
|
+
* The retry loop awaits stream.result() to detect errors before deciding
|
|
278
|
+
* whether to retry. This keeps the agent loop waiting (no timeout risk
|
|
279
|
+
* since the default agent timeout is 48 hours).
|
|
280
|
+
*/
|
|
281
|
+
wrapStreamFn: (ctx) => {
|
|
282
|
+
const underlying = ctx.streamFn;
|
|
283
|
+
if (!underlying)
|
|
284
|
+
return underlying;
|
|
285
|
+
return async (model, context, options) => {
|
|
286
|
+
// 每次请求时从 ctx.extraParams 动态读取 header
|
|
287
|
+
const dynamicHeaders = {};
|
|
288
|
+
if (ctx.extraParams) {
|
|
289
|
+
const traceId = ctx.extraParams[HEADER_TRACE_ID];
|
|
290
|
+
const sessionId = ctx.extraParams[HEADER_SESSION_ID];
|
|
291
|
+
const interactionId = ctx.extraParams[HEADER_INTERACTION_ID];
|
|
292
|
+
if (typeof traceId === "string")
|
|
293
|
+
dynamicHeaders[HEADER_TRACE_ID] = traceId;
|
|
294
|
+
if (typeof sessionId === "string")
|
|
295
|
+
dynamicHeaders[HEADER_SESSION_ID] = sessionId;
|
|
296
|
+
if (typeof interactionId === "string")
|
|
297
|
+
dynamicHeaders[HEADER_INTERACTION_ID] = interactionId;
|
|
298
|
+
}
|
|
299
|
+
// 记录输入
|
|
300
|
+
console.log(`[xiaoyiprovider] input messages count: ${context.messages?.length ?? 0}`);
|
|
301
|
+
if (context.systemPrompt) {
|
|
302
|
+
console.log(`[xiaoyiprovider] system prompt length: ${context.systemPrompt.length}`);
|
|
303
|
+
}
|
|
304
|
+
// 在发送给模型前,优化 systemPrompt 结构
|
|
305
|
+
if (context.systemPrompt) {
|
|
306
|
+
let sp = context.systemPrompt;
|
|
307
|
+
const beforeLen = sp.length;
|
|
308
|
+
// 删除 ## Tooling 与 TOOLS.md 声明之间的内容
|
|
309
|
+
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");
|
|
310
|
+
// (1) 提取 ## Skills (mandatory) 到 </available_skills> 作为第一部分
|
|
311
|
+
const skillsMatch = sp.match(/(## Skills \(mandatory\)[\s\S]*?<\/available_skills>)/);
|
|
312
|
+
const part1 = skillsMatch ? skillsMatch[0] : '';
|
|
313
|
+
// (2) 提取 ## /home/sandbox/.openclaw/workspace/SOUL.md 到 ## /home/sandbox/.openclaw/workspace/TOOLS.md 之前的内容作为第二部分
|
|
314
|
+
const soulMatch = sp.match(/(## \/home\/sandbox\/\.openclaw\/workspace\/SOUL\.md[\s\S]*?)(?=## \/home\/sandbox\/\.openclaw\/workspace\/TOOLS\.md)/);
|
|
315
|
+
const part2 = soulMatch ? soulMatch[1].trim() : '';
|
|
316
|
+
if (part1 || part2) {
|
|
317
|
+
// 从原始位置删除已提取的部分
|
|
318
|
+
if (skillsMatch)
|
|
319
|
+
sp = sp.replace(skillsMatch[0], '');
|
|
320
|
+
if (soulMatch)
|
|
321
|
+
sp = sp.replace(soulMatch[1], '');
|
|
322
|
+
// 清理多余空行
|
|
323
|
+
sp = sp.replace(/\n{3,}/g, '\n\n');
|
|
324
|
+
// (3) 将 第二部分 + 第一部分 插入到 ## Runtime 上面
|
|
325
|
+
const combined = (part2 + '\n\n' + part1).trim();
|
|
326
|
+
if (combined && sp.includes('## Runtime')) {
|
|
327
|
+
sp = sp.replace('## Runtime', combined + '\n\n## Runtime');
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
console.log(`[xiaoyiprovider] system prompt optimized: ${beforeLen} -> ${sp.length}`);
|
|
331
|
+
context.systemPrompt = sp;
|
|
332
|
+
}
|
|
333
|
+
// Append device context to systemPrompt
|
|
334
|
+
const sessionCtx = getCurrentSessionContext();
|
|
335
|
+
if (sessionCtx?.deviceType) {
|
|
336
|
+
const rawDevice = sessionCtx.deviceType;
|
|
337
|
+
const displayDevice = (rawDevice === "2in1") ? "鸿蒙PC" : rawDevice;
|
|
338
|
+
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"`;
|
|
339
|
+
context.systemPrompt = (context.systemPrompt ?? "") + deviceSection;
|
|
340
|
+
}
|
|
341
|
+
// ── Retry-capable streaming ──────────────────────────────
|
|
342
|
+
const cronJob = isCronTriggered(context.messages);
|
|
343
|
+
if (cronJob)
|
|
344
|
+
console.log("[xiaoyiprovider] detected cron-triggered request, using extended retry delays");
|
|
345
|
+
const makeStream = () => underlying(model, context, {
|
|
346
|
+
...options,
|
|
347
|
+
headers: {
|
|
348
|
+
...options?.headers,
|
|
349
|
+
...dynamicHeaders,
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
return createRetryingStream(makeStream, cronJob);
|
|
353
|
+
};
|
|
354
|
+
},
|
|
355
|
+
};
|
|
@@ -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,130 @@
|
|
|
1
|
+
import { noteTool } from "./note-tool.js";
|
|
2
|
+
import { searchNoteTool } from "./search-note-tool.js";
|
|
3
|
+
import { modifyNoteTool } from "./modify-note-tool.js";
|
|
4
|
+
import { createAlarmTool } from "./create-alarm-tool.js";
|
|
5
|
+
import { searchAlarmTool } from "./search-alarm-tool.js";
|
|
6
|
+
import { modifyAlarmTool } from "./modify-alarm-tool.js";
|
|
7
|
+
import { deleteAlarmTool } from "./delete-alarm-tool.js";
|
|
8
|
+
import { searchContactTool } from "./search-contact-tool.js";
|
|
9
|
+
import { callPhoneTool } from "./call-phone-tool.js";
|
|
10
|
+
import { searchMessageTool } from "./search-message-tool.js";
|
|
11
|
+
import { sendMessageTool } from "./send-message-tool.js";
|
|
12
|
+
import { xiaoyiAddCollectionTool } from "./xiaoyi-add-collection-tool.js";
|
|
13
|
+
import { xiaoyiCollectionTool } from "./xiaoyi-collection-tool.js";
|
|
14
|
+
import { xiaoyiDeleteCollectionTool } from "./xiaoyi-delete-collection-tool.js";
|
|
15
|
+
import { calendarTool } from "./calendar-tool.js";
|
|
16
|
+
import { searchCalendarTool } from "./search-calendar-tool.js";
|
|
17
|
+
import { searchPhotoGalleryTool } from "./search-photo-gallery-tool.js";
|
|
18
|
+
import { uploadPhotoTool } from "./upload-photo-tool.js";
|
|
19
|
+
import { saveMediaToGalleryTool } from "./save-media-to-gallery-tool.js";
|
|
20
|
+
import { searchFileTool } from "./search-file-tool.js";
|
|
21
|
+
import { uploadFileTool } from "./upload-file-tool.js";
|
|
22
|
+
import { saveFileToPhoneTool } from "./save-file-to-phone-tool.js";
|
|
23
|
+
import { sendEmailTool } from "./send-email-tool.js";
|
|
24
|
+
import { searchEmailTool } from "./search-email-tool.js";
|
|
25
|
+
import { sendStatusUpdate } from "../formatter.js";
|
|
26
|
+
import { getCurrentSessionContext } from "./session-manager.js";
|
|
27
|
+
import { getCurrentTaskId, getCurrentMessageId } from "../task-manager.js";
|
|
28
|
+
/**
|
|
29
|
+
* 端工具注册表 —— 按 name 索引所有可通过 call_device_tool 调度的工具。
|
|
30
|
+
*/
|
|
31
|
+
const deviceToolRegistry = new Map([
|
|
32
|
+
[noteTool.name, noteTool],
|
|
33
|
+
[searchNoteTool.name, searchNoteTool],
|
|
34
|
+
[modifyNoteTool.name, modifyNoteTool],
|
|
35
|
+
[createAlarmTool.name, createAlarmTool],
|
|
36
|
+
[searchAlarmTool.name, searchAlarmTool],
|
|
37
|
+
[modifyAlarmTool.name, modifyAlarmTool],
|
|
38
|
+
[deleteAlarmTool.name, deleteAlarmTool],
|
|
39
|
+
[searchContactTool.name, searchContactTool],
|
|
40
|
+
[callPhoneTool.name, callPhoneTool],
|
|
41
|
+
[searchMessageTool.name, searchMessageTool],
|
|
42
|
+
[sendMessageTool.name, sendMessageTool],
|
|
43
|
+
[xiaoyiAddCollectionTool.name, xiaoyiAddCollectionTool],
|
|
44
|
+
[xiaoyiCollectionTool.name, xiaoyiCollectionTool],
|
|
45
|
+
[xiaoyiDeleteCollectionTool.name, xiaoyiDeleteCollectionTool],
|
|
46
|
+
[calendarTool.name, calendarTool],
|
|
47
|
+
[searchCalendarTool.name, searchCalendarTool],
|
|
48
|
+
[searchPhotoGalleryTool.name, searchPhotoGalleryTool],
|
|
49
|
+
[uploadPhotoTool.name, uploadPhotoTool],
|
|
50
|
+
[saveMediaToGalleryTool.name, saveMediaToGalleryTool],
|
|
51
|
+
[searchFileTool.name, searchFileTool],
|
|
52
|
+
[uploadFileTool.name, uploadFileTool],
|
|
53
|
+
[saveFileToPhoneTool.name, saveFileToPhoneTool],
|
|
54
|
+
[sendEmailTool.name, sendEmailTool],
|
|
55
|
+
[searchEmailTool.name, searchEmailTool],
|
|
56
|
+
]);
|
|
57
|
+
/**
|
|
58
|
+
* call_device_tool - 通用端工具调度器。
|
|
59
|
+
* LLM 必须先通过 get_xxx_tool_schema 获取具体工具 schema,再用本工具执行。
|
|
60
|
+
*/
|
|
61
|
+
export const callDeviceTool = {
|
|
62
|
+
name: "call_device_tool",
|
|
63
|
+
label: "Call Device Tool",
|
|
64
|
+
description: "用户设备侧工具调用。必须先调用get_xxx_tool_schema获取了具体的工具schema,才能使用本工具执行对应设备侧工具。",
|
|
65
|
+
parameters: {
|
|
66
|
+
type: "object",
|
|
67
|
+
properties: {
|
|
68
|
+
toolName: {
|
|
69
|
+
type: "string",
|
|
70
|
+
description: "要调用的具体端工具名称,即get_xxx_tool_schema返回的工具的name",
|
|
71
|
+
},
|
|
72
|
+
arguments: {
|
|
73
|
+
type: "object",
|
|
74
|
+
description: "工具所需的具体参数JSON键值对",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
required: ["toolName", "arguments"],
|
|
78
|
+
},
|
|
79
|
+
async execute(toolCallId, params) {
|
|
80
|
+
const { toolName, arguments: toolArgs } = params;
|
|
81
|
+
// 向用户端发送具体工具名的状态更新
|
|
82
|
+
const ctx = getCurrentSessionContext();
|
|
83
|
+
if (ctx) {
|
|
84
|
+
const currentTaskId = getCurrentTaskId(ctx.sessionId) ?? ctx.taskId;
|
|
85
|
+
const currentMessageId = getCurrentMessageId(ctx.sessionId) ?? ctx.messageId;
|
|
86
|
+
try {
|
|
87
|
+
await sendStatusUpdate({
|
|
88
|
+
config: ctx.config,
|
|
89
|
+
sessionId: ctx.sessionId,
|
|
90
|
+
taskId: currentTaskId,
|
|
91
|
+
messageId: currentMessageId,
|
|
92
|
+
text: `正在使用工具: ${toolName}...`,
|
|
93
|
+
state: "working",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch (_) {
|
|
97
|
+
// 状态更新失败不影响工具执行
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const tool = deviceToolRegistry.get(toolName);
|
|
101
|
+
if (!tool) {
|
|
102
|
+
return {
|
|
103
|
+
content: [
|
|
104
|
+
{
|
|
105
|
+
type: "text",
|
|
106
|
+
text: `端工具${toolName}不存在。请确保toolName为get_xxx_tool_schema返回的工具的name。`,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
return await tool.execute(toolCallId, toolArgs);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
// ToolInputError (.name === "ToolInputError") 或其他参数校验错误
|
|
116
|
+
if (error.name === "ToolInputError") {
|
|
117
|
+
return {
|
|
118
|
+
content: [
|
|
119
|
+
{
|
|
120
|
+
type: "text",
|
|
121
|
+
text: `端工具参数错误:${error.message}。请确保arguments符合get_xxx_tool_schema返回的工具schema。`,
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// 非参数错误(网络超时等),直接向上抛出
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
};
|
|
@@ -17,18 +17,7 @@ export const createAlarmTool = {
|
|
|
17
17
|
name: "create_alarm",
|
|
18
18
|
label: "Create Alarm",
|
|
19
19
|
description: `在用户设备上创建闹钟。
|
|
20
|
-
|
|
21
|
-
必需参数:
|
|
22
|
-
- alarmTime: 闹钟时间,格式必须为:YYYYMMDD hhmmss(例如:20240315 143000,表示2024年3月15日14:30:00)
|
|
23
|
-
|
|
24
|
-
可选参数(针对用户没有提及的参数,如果有默认参数,则发送请求时使用默认参数):
|
|
25
|
-
- alarmTitle: 闹钟名称/标题,默认为"闹钟"
|
|
26
|
-
- alarmSnoozeDuration: 小睡间隔(分钟),枚举值:5,10,15,20,25,30,默认10
|
|
27
|
-
- alarmSnoozeTotal: 再响次数,枚举值:0,1,3,5,10,默认0(表示不再响)
|
|
28
|
-
- alarmRingDuration: 响铃时长(分钟),枚举值:1,5,10,15,20,30,默认5
|
|
29
|
-
- daysOfWakeType: 闹钟响铃类型,枚举值:0=单次响铃,1=法定节假日,2=每天,3=自定义时间,4=法定工作日,默认0
|
|
30
|
-
- daysOfWeek: 自定义响铃星期,仅当daysOfWakeType=3(自定义时间)时必需且有效,其他情况不要传递此参数。数组或JSON字符串,枚举值:Mon,Tues,Wed,Thur,Fri,Sat,Sun。
|
|
31
|
-
|
|
20
|
+
|
|
32
21
|
注意事项:
|
|
33
22
|
a. 操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。
|
|
34
23
|
b. 使用该工具之前需获取当前真实时间
|
|
@@ -39,7 +28,7 @@ b. 使用该工具之前需获取当前真实时间
|
|
|
39
28
|
properties: {
|
|
40
29
|
alarmTime: {
|
|
41
30
|
type: "string",
|
|
42
|
-
description: "闹钟时间,格式必须为:YYYYMMDD hhmmss(例如:20240315 143000)",
|
|
31
|
+
description: "闹钟时间,格式必须为:YYYYMMDD hhmmss(例如:20240315 143000,表示2024年3月15日14:30:00)",
|
|
43
32
|
},
|
|
44
33
|
alarmTitle: {
|
|
45
34
|
type: "string",
|
|
@@ -51,7 +40,7 @@ b. 使用该工具之前需获取当前真实时间
|
|
|
51
40
|
},
|
|
52
41
|
alarmSnoozeTotal: {
|
|
53
42
|
type: "number",
|
|
54
|
-
description: "再响次数,枚举值:0,1,3,5,10,默认0",
|
|
43
|
+
description: "再响次数,枚举值:0,1,3,5,10,默认0(表示不再响)",
|
|
55
44
|
},
|
|
56
45
|
alarmRingDuration: {
|
|
57
46
|
type: "number",
|
|
@@ -59,12 +48,12 @@ b. 使用该工具之前需获取当前真实时间
|
|
|
59
48
|
},
|
|
60
49
|
daysOfWakeType: {
|
|
61
50
|
type: "number",
|
|
62
|
-
description: "
|
|
51
|
+
description: "闹钟响铃类型,枚举值:0=单次响铃,1=法定节假日,2=每天,3=自定义时间,4=法定工作日,默认0",
|
|
63
52
|
},
|
|
64
53
|
daysOfWeek: {
|
|
65
54
|
// 不指定 type,允许传入数组或 JSON 字符串
|
|
66
55
|
// 具体的类型验证和转换在 execute 函数内部进行
|
|
67
|
-
description: "
|
|
56
|
+
description: "自定义响铃星期,仅当daysOfWakeType=3(自定义时间)时必需且有效,其他情况不要传递此参数。数组或JSON字符串,枚举值:Mon,Tues,Wed,Thur,Fri,Sat,Sun。",
|
|
68
57
|
},
|
|
69
58
|
},
|
|
70
59
|
required: ["alarmTime"],
|
|
@@ -16,9 +16,6 @@ export const deleteAlarmTool = {
|
|
|
16
16
|
label: "Delete Alarm",
|
|
17
17
|
description: `删除用户设备上的闹钟。使用前必须先调用 search_alarm 或 create_alarm 工具获取闹钟的 entityId。
|
|
18
18
|
|
|
19
|
-
工具参数:
|
|
20
|
-
- items: 要删除的闹钟列表,每个元素包含 entityId 字段。支持数组或 JSON 字符串格式。entityId 是闹钟的唯一标识符(从 search_alarm 或 create_alarm 工具获取)。
|
|
21
|
-
|
|
22
19
|
使用示例:
|
|
23
20
|
- 删除单个闹钟:{"items": [{"entityId": "6"}]}
|
|
24
21
|
- 删除多个闹钟:{"items": [{"entityId": "6"}, {"entityId": "8"}]}
|
|
@@ -35,7 +32,7 @@ export const deleteAlarmTool = {
|
|
|
35
32
|
items: {
|
|
36
33
|
// 不指定 type,允许传入数组或 JSON 字符串
|
|
37
34
|
// 具体的类型验证和转换在 execute 函数内部进行
|
|
38
|
-
description: "要删除的闹钟列表,每个元素包含 entityId 字段。支持数组或 JSON 字符串格式。",
|
|
35
|
+
description: "要删除的闹钟列表,每个元素包含 entityId 字段。支持数组或 JSON 字符串格式。entityId 是闹钟的唯一标识符(从 search_alarm 或 create_alarm 工具获取)。",
|
|
39
36
|
},
|
|
40
37
|
},
|
|
41
38
|
required: ["items"],
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Device type to tool name mapping.
|
|
2
|
+
// Supports two modes:
|
|
3
|
+
// - allowlist: only listed tools are available (used for restrictive devices like car)
|
|
4
|
+
// - denylist: listed tools are blocked, everything else is available (used for permissive devices like pc)
|
|
5
|
+
// Tools NOT listed in any device entry → available to all devices (no restriction).
|
|
6
|
+
/** Known device type enum. */
|
|
7
|
+
export const DEVICE_TYPES = ["car", "2in1", "phone"];
|
|
8
|
+
const DEVICE_TOOL_POLICY = {
|
|
9
|
+
"2in1": {
|
|
10
|
+
allowlist: false,
|
|
11
|
+
tools: [
|
|
12
|
+
"xiaoyi_gui_agent",
|
|
13
|
+
"call_phone",
|
|
14
|
+
"send_message",
|
|
15
|
+
"search_message",
|
|
16
|
+
"search_contact",
|
|
17
|
+
"get_contact_tool_schema",
|
|
18
|
+
"query_collection",
|
|
19
|
+
"add_collection",
|
|
20
|
+
"delete_collection",
|
|
21
|
+
"get_collection_tool_schema",
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
export function filterToolsByDevice(tools, deviceType) {
|
|
26
|
+
if (!deviceType)
|
|
27
|
+
return tools;
|
|
28
|
+
const policy = DEVICE_TOOL_POLICY[deviceType];
|
|
29
|
+
if (!policy)
|
|
30
|
+
return tools; // unrecognized device → no filtering
|
|
31
|
+
if (policy.allowlist) {
|
|
32
|
+
return tools.filter((tool) => policy.tools.includes(tool.name));
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
return tools.filter((tool) => !policy.tools.includes(tool.name));
|
|
36
|
+
}
|
|
37
|
+
}
|