@ynhcj/xiaoyi-channel 1.1.15 → 1.1.17
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.d.ts +1 -0
- package/dist/src/bot.js +43 -8
- package/dist/src/channel.js +15 -1
- package/dist/src/cspl/call-api.js +14 -11
- package/dist/src/cspl/config.js +3 -3
- package/dist/src/cspl/constants.d.ts +1 -0
- package/dist/src/monitor.js +1 -0
- 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 +116 -0
- package/dist/src/tools/device-tool-map.d.ts +4 -0
- package/dist/src/tools/device-tool-map.js +35 -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/save-file-to-phone-tool.d.ts +5 -0
- package/dist/src/tools/save-file-to-phone-tool.js +170 -0
- package/dist/src/tools/save-media-to-gallery-tool.d.ts +5 -0
- package/dist/src/tools/save-media-to-gallery-tool.js +178 -0
- package/dist/src/tools/send-command-to-car-tool.d.ts +5 -0
- package/dist/src/tools/send-command-to-car-tool.js +85 -0
- package/dist/src/tools/session-manager.d.ts +1 -0
- package/dist/src/tools/upload-file-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 +187 -0
- package/dist/src/tools/xiaoyi-collection-tool.js +42 -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/dist/src/utils/runtime-manager.d.ts +7 -0
- package/dist/src/utils/runtime-manager.js +42 -0
- package/openclaw.plugin.json +1 -0
- package/package.json +2 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
2
2
|
import { xyPlugin } from "./src/channel.js";
|
|
3
|
+
import { xiaoyiProvider } from "./src/provider.js";
|
|
3
4
|
import { setXYRuntime } from "./src/runtime.js";
|
|
4
5
|
import { tryInjectSteer } from "./src/steer-injector.js";
|
|
5
6
|
import { callCsplApi } from "./src/cspl/call-api.js";
|
|
@@ -18,13 +19,14 @@ const plugin = {
|
|
|
18
19
|
register(api) {
|
|
19
20
|
setXYRuntime(api.runtime);
|
|
20
21
|
api.registerChannel({ plugin: xyPlugin });
|
|
21
|
-
|
|
22
|
+
api.registerProvider(xiaoyiProvider);
|
|
23
|
+
// SENTINEL HOOK after_tool_call hook: 监听工具结果,发送至安全检测 API 进行安全检测
|
|
22
24
|
// 如果响应为 REJECT,注入 steer 消息中止当前对话
|
|
23
25
|
api.on("after_tool_call", async (event, ctx) => {
|
|
24
26
|
if (!ALLOWED_TOOLS.includes(event.toolName)) {
|
|
25
27
|
return;
|
|
26
28
|
}
|
|
27
|
-
console.log(`[
|
|
29
|
+
console.log(`[SENTINEL HOOK] after_tool_call triggered: toolName=${event.toolName}, sessionKey=${ctx.sessionKey ?? "none"}`);
|
|
28
30
|
try {
|
|
29
31
|
const resultText = extractResultText(event, event.toolName);
|
|
30
32
|
const resultLength = resultText.length;
|
|
@@ -33,6 +35,7 @@ const plugin = {
|
|
|
33
35
|
}
|
|
34
36
|
// 构造 sentinel_hook 格式的 payload: { tool, output: [{ content }] }
|
|
35
37
|
const questionText = {
|
|
38
|
+
subSceneID: 'TOOL_OUTPUT',
|
|
36
39
|
tool: event.toolName,
|
|
37
40
|
output: [{ content: "" }],
|
|
38
41
|
};
|
|
@@ -47,13 +50,13 @@ const plugin = {
|
|
|
47
50
|
}
|
|
48
51
|
const response = await callCsplApi(finalJson, api.config);
|
|
49
52
|
const result = parseSecurityResult(response);
|
|
50
|
-
console.log(`[
|
|
53
|
+
console.log(`[SENTINEL HOOK] Security result: status=${result.status}`);
|
|
51
54
|
if (result.status === "REJECT") {
|
|
52
55
|
await tryInjectSteer(ctx.sessionKey, STEER_ABORT_MESSAGE);
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
catch (err) {
|
|
56
|
-
api.logger.error(`[
|
|
59
|
+
api.logger.error(`[SENTINEL HOOK] after_tool_call error: ${err}`);
|
|
57
60
|
}
|
|
58
61
|
});
|
|
59
62
|
},
|
package/dist/src/bot.d.ts
CHANGED
package/dist/src/bot.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getXYRuntime } from "./runtime.js";
|
|
2
2
|
import { setCachedContext } from "./steer-injector.js";
|
|
3
3
|
import { createXYReplyDispatcher } from "./reply-dispatcher.js";
|
|
4
|
-
import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractTriggerData } from "./parser.js";
|
|
4
|
+
import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId, extractDeviceType, extractTriggerData } from "./parser.js";
|
|
5
5
|
import { downloadFilesFromParts } from "./file-download.js";
|
|
6
6
|
import { resolveXYConfig } from "./config.js";
|
|
7
7
|
import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse, sendA2AResponse } from "./formatter.js";
|
|
@@ -9,6 +9,7 @@ import { registerSession, unregisterSession, runWithSessionContext } from "./too
|
|
|
9
9
|
import { configManager } from "./utils/config-manager.js";
|
|
10
10
|
import { addPushId } from "./utils/pushid-manager.js";
|
|
11
11
|
import { getPushDataById } from "./utils/pushdata-manager.js";
|
|
12
|
+
import { saveRuntimeInfo } from "./utils/runtime-manager.js";
|
|
12
13
|
import { registerTaskId, decrementTaskIdRef, lockTaskId, unlockTaskId, hasActiveTask, } from "./task-manager.js";
|
|
13
14
|
/**
|
|
14
15
|
* Handle an incoming A2A message.
|
|
@@ -16,7 +17,7 @@ import { registerTaskId, decrementTaskIdRef, lockTaskId, unlockTaskId, hasActive
|
|
|
16
17
|
* Runtime is expected to be validated before calling this function.
|
|
17
18
|
*/
|
|
18
19
|
export async function handleXYMessage(params) {
|
|
19
|
-
const { cfg, runtime, message, accountId } = params;
|
|
20
|
+
const { cfg, runtime, message, accountId, webSocketSessionId } = params;
|
|
20
21
|
const log = runtime?.log ?? console.log;
|
|
21
22
|
const error = runtime?.error ?? console.error;
|
|
22
23
|
// 每次收到消息时更新缓存,供 steer 注入使用
|
|
@@ -125,6 +126,18 @@ export async function handleXYMessage(params) {
|
|
|
125
126
|
else {
|
|
126
127
|
log(`[BOT] ℹ️ No push_id found in message, will use config default`);
|
|
127
128
|
}
|
|
129
|
+
// Extract deviceType if present (same level as push_id in systemVariables)
|
|
130
|
+
const deviceType = extractDeviceType(parsed.parts);
|
|
131
|
+
if (deviceType) {
|
|
132
|
+
log(`[BOT] 📱 Extracted deviceType from user message: ${deviceType}`);
|
|
133
|
+
}
|
|
134
|
+
// 保存 runtime 信息到 .xiaoyiruntime 文件(异步,不阻塞主流程)
|
|
135
|
+
saveRuntimeInfo(webSocketSessionId || parsed.sessionId, // SESSION_ID (WebSocket 层级,如果没有则 fallback)
|
|
136
|
+
parsed.sessionId, // CONVERSATION_ID (param 里的 sessionId)
|
|
137
|
+
parsed.taskId // TASK_ID (param.id)
|
|
138
|
+
).catch((err) => {
|
|
139
|
+
error(`[BOT] Failed to save runtime info:`, err);
|
|
140
|
+
});
|
|
128
141
|
// Resolve configuration (needed for status updates)
|
|
129
142
|
const config = resolveXYConfig(cfg);
|
|
130
143
|
// ✅ Resolve agent route (following feishu pattern)
|
|
@@ -230,7 +243,9 @@ export async function handleXYMessage(params) {
|
|
|
230
243
|
taskId: parsed.taskId,
|
|
231
244
|
messageId: parsed.messageId,
|
|
232
245
|
agentId: route.accountId,
|
|
246
|
+
deviceType,
|
|
233
247
|
};
|
|
248
|
+
log(`[BOT-DISPATCH] ⏳ withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
|
|
234
249
|
await core.channel.reply.withReplyDispatcher({
|
|
235
250
|
dispatcher,
|
|
236
251
|
onSettled: () => {
|
|
@@ -249,12 +264,32 @@ export async function handleXYMessage(params) {
|
|
|
249
264
|
},
|
|
250
265
|
run: () =>
|
|
251
266
|
// 🔐 Use AsyncLocalStorage to provide session context to tools
|
|
252
|
-
runWithSessionContext(sessionContext, () =>
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
267
|
+
runWithSessionContext(sessionContext, async () => {
|
|
268
|
+
log(`[BOT-DISPATCH] ⏳ dispatchReplyFromConfig starting...`);
|
|
269
|
+
log(`[BOT-DISPATCH] - sessionKey: ${ctxPayload.SessionKey}`);
|
|
270
|
+
log(`[BOT-DISPATCH] - provider: ${ctxPayload.Provider}`);
|
|
271
|
+
log(`[BOT-DISPATCH] - surface: ${ctxPayload.Surface}`);
|
|
272
|
+
log(`[BOT-DISPATCH] - from: ${ctxPayload.From}`);
|
|
273
|
+
log(`[BOT-DISPATCH] - body length: ${ctxPayload.Body?.length ?? 0}`);
|
|
274
|
+
try {
|
|
275
|
+
const result = await core.channel.reply.dispatchReplyFromConfig({
|
|
276
|
+
ctx: ctxPayload,
|
|
277
|
+
cfg,
|
|
278
|
+
dispatcher,
|
|
279
|
+
replyOptions,
|
|
280
|
+
});
|
|
281
|
+
log(`[BOT-DISPATCH] ✅ dispatchReplyFromConfig returned`);
|
|
282
|
+
log(`[BOT-DISPATCH] - result: ${JSON.stringify(result)}`);
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
catch (dispatchErr) {
|
|
286
|
+
error(`[BOT-DISPATCH] ❌ dispatchReplyFromConfig threw`);
|
|
287
|
+
error(`[BOT-DISPATCH] - error name: ${dispatchErr instanceof Error ? dispatchErr.name : "unknown"}`);
|
|
288
|
+
error(`[BOT-DISPATCH] - error message: ${String(dispatchErr)}`);
|
|
289
|
+
error(`[BOT-DISPATCH] - error stack: ${dispatchErr instanceof Error ? dispatchErr.stack?.slice(0, 500) : "N/A"}`);
|
|
290
|
+
throw dispatchErr;
|
|
291
|
+
}
|
|
292
|
+
}),
|
|
258
293
|
});
|
|
259
294
|
log(`[BOT] ✅ Dispatcher completed for session: ${parsed.sessionId}`);
|
|
260
295
|
log(`xy: dispatch complete (session=${parsed.sessionId})`);
|
package/dist/src/channel.js
CHANGED
|
@@ -24,6 +24,14 @@ import { sendFileToUserTool } from "./tools/send-file-to-user-tool.js";
|
|
|
24
24
|
import { viewPushResultTool } from "./tools/view-push-result-tool.js";
|
|
25
25
|
import { imageReadingTool } from "./tools/image-reading-tool.js";
|
|
26
26
|
import { timestampToUtc8Tool } from "./tools/timestamp-to-utc8-tool.js";
|
|
27
|
+
import { xiaoyiCollectionTool } from "./tools/xiaoyi-collection-tool.js";
|
|
28
|
+
import { xiaoyiAddCollectionTool } from "./tools/xiaoyi-add-collection-tool.js";
|
|
29
|
+
import { xiaoyiDeleteCollectionTool } from "./tools/xiaoyi-delete-collection-tool.js";
|
|
30
|
+
import { saveMediaToGalleryTool } from "./tools/save-media-to-gallery-tool.js";
|
|
31
|
+
import { saveFileToPhoneTool } from "./tools/save-file-to-phone-tool.js";
|
|
32
|
+
import { filterToolsByDevice } from "./tools/device-tool-map.js";
|
|
33
|
+
import { getCurrentSessionContext } from "./tools/session-manager.js";
|
|
34
|
+
import { logger } from "./utils/logger.js";
|
|
27
35
|
/**
|
|
28
36
|
* Xiaoyi Channel Plugin for OpenClaw.
|
|
29
37
|
* Implements Xiaoyi A2A protocol with dual WebSocket connections.
|
|
@@ -62,7 +70,13 @@ export const xyPlugin = {
|
|
|
62
70
|
schema: xyConfigSchema,
|
|
63
71
|
},
|
|
64
72
|
outbound: xyOutbound,
|
|
65
|
-
agentTools:
|
|
73
|
+
agentTools: () => {
|
|
74
|
+
const allTools = [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchContactTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, callPhoneTool, searchMessageTool, sendMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendFileToUserTool, viewPushResultTool, imageReadingTool, timestampToUtc8Tool, xiaoyiCollectionTool, xiaoyiAddCollectionTool, xiaoyiDeleteCollectionTool, saveMediaToGalleryTool, saveFileToPhoneTool];
|
|
75
|
+
const ctx = getCurrentSessionContext();
|
|
76
|
+
const filtered = filterToolsByDevice(allTools, ctx?.deviceType);
|
|
77
|
+
logger.log(`[DEVICE-FILTER] deviceType=${ctx?.deviceType ?? "(none)"}, tools: ${allTools.length} → ${filtered.length} (${filtered.map(t => t.name).join(", ")})`);
|
|
78
|
+
return filtered;
|
|
79
|
+
},
|
|
66
80
|
messaging: {
|
|
67
81
|
normalizeTarget: (raw) => {
|
|
68
82
|
const trimmed = raw.trim();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// SENTINEL HOOK API 请求模块
|
|
2
2
|
import https from "node:https";
|
|
3
3
|
import { URL } from "node:url";
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
@@ -8,8 +8,10 @@ function generateTraceId() {
|
|
|
8
8
|
return randomBytes(16).toString("hex");
|
|
9
9
|
}
|
|
10
10
|
function buildHeaders(config) {
|
|
11
|
+
const traceId = generateTraceId();
|
|
12
|
+
console.log(`[SENTINEL HOOK] trace-id: ${traceId}`);
|
|
11
13
|
return {
|
|
12
|
-
"x-hag-trace-id":
|
|
14
|
+
"x-hag-trace-id": traceId,
|
|
13
15
|
"x-uid": config.uid,
|
|
14
16
|
"x-api-key": config.apiKey,
|
|
15
17
|
"x-request-from": config.requestFrom,
|
|
@@ -30,13 +32,13 @@ function buildRequestOptions(url, headers, timeout) {
|
|
|
30
32
|
}
|
|
31
33
|
function parseResponse(data) {
|
|
32
34
|
if (!data?.trim())
|
|
33
|
-
throw new Error("[
|
|
35
|
+
throw new Error("[SENTINEL HOOK] API response is empty");
|
|
34
36
|
const json = JSON.parse(data);
|
|
35
37
|
if (json.retCode && json.retCode !== "0") {
|
|
36
|
-
throw new Error(`[
|
|
38
|
+
throw new Error(`[SENTINEL HOOK] API error: ${json.retMsg || "unknown"}`);
|
|
37
39
|
}
|
|
38
40
|
if (!json.retCode && json.code) {
|
|
39
|
-
throw new Error(`[
|
|
41
|
+
throw new Error(`[SENTINEL HOOK] Backend error: ${json.desc || "unknown"}`);
|
|
40
42
|
}
|
|
41
43
|
return json;
|
|
42
44
|
}
|
|
@@ -47,12 +49,13 @@ export async function callCsplApi(questionText, cfg) {
|
|
|
47
49
|
questionText,
|
|
48
50
|
textSource: config.textSource,
|
|
49
51
|
action: config.action,
|
|
52
|
+
extra: JSON.stringify({ userId: config.uid }),
|
|
50
53
|
};
|
|
51
54
|
return new Promise((resolve, reject) => {
|
|
52
55
|
const options = buildRequestOptions(config.api.url, headers, config.api.timeout);
|
|
53
56
|
const req = https.request(options, (res) => {
|
|
54
57
|
if (res.statusCode && res.statusCode >= HTTP_STATUS_BAD_REQUEST) {
|
|
55
|
-
reject(new Error(`[
|
|
58
|
+
reject(new Error(`[SENTINEL HOOK] HTTP error: ${res.statusCode}`));
|
|
56
59
|
return;
|
|
57
60
|
}
|
|
58
61
|
let data = "";
|
|
@@ -62,23 +65,23 @@ export async function callCsplApi(questionText, cfg) {
|
|
|
62
65
|
res.on("end", () => {
|
|
63
66
|
try {
|
|
64
67
|
const result = parseResponse(data);
|
|
65
|
-
console.log(`[
|
|
68
|
+
console.log(`[SENTINEL HOOK] ✅ 请求成功`);
|
|
66
69
|
resolve(result);
|
|
67
70
|
}
|
|
68
71
|
catch (e) {
|
|
69
|
-
console.error(`[
|
|
72
|
+
console.error(`[SENTINEL HOOK] ❌ 请求失败: ${e instanceof Error ? e.message : String(e)}`);
|
|
70
73
|
reject(e);
|
|
71
74
|
}
|
|
72
75
|
});
|
|
73
76
|
});
|
|
74
77
|
req.on("error", (error) => {
|
|
75
|
-
console.error(`[
|
|
78
|
+
console.error(`[SENTINEL HOOK] ❌ 请求错误: ${error instanceof Error ? error.message : String(error)}`);
|
|
76
79
|
reject(error);
|
|
77
80
|
});
|
|
78
81
|
req.on("timeout", () => {
|
|
79
|
-
console.error(`[
|
|
82
|
+
console.error(`[SENTINEL HOOK] ⏰ 请求超时 (${config.api.timeout}ms)`);
|
|
80
83
|
req.destroy();
|
|
81
|
-
reject(new Error("[
|
|
84
|
+
reject(new Error("[SENTINEL HOOK] Request timeout"));
|
|
82
85
|
});
|
|
83
86
|
req.write(JSON.stringify(payload));
|
|
84
87
|
req.end();
|
package/dist/src/cspl/config.js
CHANGED
|
@@ -7,7 +7,7 @@ import { logger } from "../utils/logger.js";
|
|
|
7
7
|
let cachedConfig = null;
|
|
8
8
|
function readServiceUrl() {
|
|
9
9
|
if (!fs.existsSync(ENV_FILE_PATH)) {
|
|
10
|
-
throw new Error(`[
|
|
10
|
+
throw new Error(`[SENTINEL HOOK] Environment file not found: ${ENV_FILE_PATH}`);
|
|
11
11
|
}
|
|
12
12
|
const envData = fs.readFileSync(ENV_FILE_PATH, "utf-8");
|
|
13
13
|
for (const line of envData.split("\n")) {
|
|
@@ -22,7 +22,7 @@ function readServiceUrl() {
|
|
|
22
22
|
if (key === "SERVICE_URL" && value)
|
|
23
23
|
return value;
|
|
24
24
|
}
|
|
25
|
-
throw new Error("[
|
|
25
|
+
throw new Error("[SENTINEL HOOK] Missing SERVICE_URL in env file");
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
28
|
* 构建 CSPL 配置。uid 和 apiKey 复用 XYChannelConfig,避免重复配置。
|
|
@@ -45,6 +45,6 @@ export function getCsplConfig(cfg) {
|
|
|
45
45
|
textSource: CSPL_STATIC_CONFIG.textSource,
|
|
46
46
|
action: CSPL_STATIC_CONFIG.action,
|
|
47
47
|
};
|
|
48
|
-
logger.log("[
|
|
48
|
+
logger.log("[SENTINEL HOOK] Config loaded (uid/apiKey from XYChannelConfig)");
|
|
49
49
|
return cachedConfig;
|
|
50
50
|
}
|
package/dist/src/monitor.js
CHANGED
package/dist/src/parser.d.ts
CHANGED
|
@@ -43,6 +43,12 @@ export declare function isTasksCancelMessage(method: string): boolean;
|
|
|
43
43
|
* Looks for push_id in data parts under variables.systemVariables.push_id
|
|
44
44
|
*/
|
|
45
45
|
export declare function extractPushId(parts: A2AMessagePart[]): string | null;
|
|
46
|
+
/**
|
|
47
|
+
* Extract deviceType from message parts.
|
|
48
|
+
* Looks for deviceType in data parts under variables.systemVariables.deviceType
|
|
49
|
+
* (same level as push_id).
|
|
50
|
+
*/
|
|
51
|
+
export declare function extractDeviceType(parts: A2AMessagePart[]): string | null;
|
|
46
52
|
/**
|
|
47
53
|
* Extract Trigger event data from message parts.
|
|
48
54
|
* Looks for Trigger events with pushDataId in data parts.
|
package/dist/src/parser.js
CHANGED
|
@@ -72,6 +72,22 @@ export function extractPushId(parts) {
|
|
|
72
72
|
}
|
|
73
73
|
return null;
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Extract deviceType from message parts.
|
|
77
|
+
* Looks for deviceType in data parts under variables.systemVariables.deviceType
|
|
78
|
+
* (same level as push_id).
|
|
79
|
+
*/
|
|
80
|
+
export function extractDeviceType(parts) {
|
|
81
|
+
for (const part of parts) {
|
|
82
|
+
if (part.kind === "data" && part.data) {
|
|
83
|
+
const deviceType = part.data.variables?.systemVariables?.device_type;
|
|
84
|
+
if (deviceType && typeof deviceType === "string") {
|
|
85
|
+
return deviceType;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
75
91
|
/**
|
|
76
92
|
* Extract Trigger event data from message parts.
|
|
77
93
|
* Looks for Trigger events with pushDataId in data parts.
|
|
@@ -0,0 +1,116 @@
|
|
|
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
|
+
/**
|
|
13
|
+
* Dynamic header keys injected via extraParams and forwarded to the HTTP request.
|
|
14
|
+
* Correspond to the three fields written to .xiaoyiruntime:
|
|
15
|
+
* TASK_ID, SESSION_ID, CONVERSATION_ID
|
|
16
|
+
*/
|
|
17
|
+
const HEADER_TRACE_ID = "x-hag-trace-id";
|
|
18
|
+
const HEADER_SESSION_ID = "x-session-id";
|
|
19
|
+
const HEADER_INTERACTION_ID = "x-interaction-id";
|
|
20
|
+
/**
|
|
21
|
+
* Encode uid via SHA-256 and take first 32 hex chars.
|
|
22
|
+
*/
|
|
23
|
+
function encodeUid(uid) {
|
|
24
|
+
return createHash("sha256").update(uid).digest("hex").slice(0, 32);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Get uid from plugin config (OpenClawConfig -> plugins -> xiaoyi-channel -> config).
|
|
28
|
+
*/
|
|
29
|
+
function getUidFromConfig(config) {
|
|
30
|
+
return config?.plugins?.entries?.["xiaoyi-channel"]?.config?.uid;
|
|
31
|
+
}
|
|
32
|
+
export const xiaoyiProvider = {
|
|
33
|
+
id: "xiaoyiprovider",
|
|
34
|
+
label: "Xiaoyi Provider",
|
|
35
|
+
docsPath: "/providers/models",
|
|
36
|
+
auth: [],
|
|
37
|
+
isCacheTtlEligible: () => true,
|
|
38
|
+
/**
|
|
39
|
+
* Inject dynamic session params into extraParams so they flow
|
|
40
|
+
* through to wrapStreamFn's ctx.extraParams.
|
|
41
|
+
*
|
|
42
|
+
* Priority:
|
|
43
|
+
* 1. Session context (from AsyncLocalStorage, set by bot.ts)
|
|
44
|
+
* 2. uid-based fallback: sha256(uid).hex[:32]_timestamp
|
|
45
|
+
* 3. No uid available → return undefined (no headers injected)
|
|
46
|
+
*/
|
|
47
|
+
prepareExtraParams: (ctx) => {
|
|
48
|
+
const sessionCtx = getCurrentSessionContext();
|
|
49
|
+
if (sessionCtx) {
|
|
50
|
+
const taskId = sessionCtx.taskId;
|
|
51
|
+
const sessionId = taskId.split("&")[0];
|
|
52
|
+
const interactionId = taskId.split("&")[1] || "";
|
|
53
|
+
return {
|
|
54
|
+
...ctx.extraParams,
|
|
55
|
+
[HEADER_TRACE_ID]: taskId,
|
|
56
|
+
[HEADER_SESSION_ID]: sessionId,
|
|
57
|
+
[HEADER_INTERACTION_ID]: interactionId,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// Fallback: uid-based values
|
|
61
|
+
const uid = getUidFromConfig(ctx.config);
|
|
62
|
+
if (!uid)
|
|
63
|
+
return undefined;
|
|
64
|
+
const prefix = encodeUid(uid);
|
|
65
|
+
const ts = Date.now();
|
|
66
|
+
const fallbackValue = `${prefix}_${ts}`;
|
|
67
|
+
return {
|
|
68
|
+
...ctx.extraParams,
|
|
69
|
+
[HEADER_TRACE_ID]: fallbackValue,
|
|
70
|
+
[HEADER_SESSION_ID]: fallbackValue,
|
|
71
|
+
[HEADER_INTERACTION_ID]: fallbackValue,
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
/**
|
|
75
|
+
* Wrap the stream function to inject dynamic headers into every
|
|
76
|
+
* HTTP request to the model provider.
|
|
77
|
+
*
|
|
78
|
+
* Reads the values injected by prepareExtraParams and adds them
|
|
79
|
+
* as HTTP headers on the outgoing request.
|
|
80
|
+
*/
|
|
81
|
+
wrapStreamFn: (ctx) => {
|
|
82
|
+
const underlying = ctx.streamFn;
|
|
83
|
+
if (!underlying)
|
|
84
|
+
return underlying;
|
|
85
|
+
return async (model, context, options) => {
|
|
86
|
+
// 每次请求时从 ctx.extraParams 动态读取 header
|
|
87
|
+
const dynamicHeaders = {};
|
|
88
|
+
if (ctx.extraParams) {
|
|
89
|
+
const traceId = ctx.extraParams[HEADER_TRACE_ID];
|
|
90
|
+
const sessionId = ctx.extraParams[HEADER_SESSION_ID];
|
|
91
|
+
const interactionId = ctx.extraParams[HEADER_INTERACTION_ID];
|
|
92
|
+
if (typeof traceId === "string")
|
|
93
|
+
dynamicHeaders[HEADER_TRACE_ID] = traceId;
|
|
94
|
+
if (typeof sessionId === "string")
|
|
95
|
+
dynamicHeaders[HEADER_SESSION_ID] = sessionId;
|
|
96
|
+
if (typeof interactionId === "string")
|
|
97
|
+
dynamicHeaders[HEADER_INTERACTION_ID] = interactionId;
|
|
98
|
+
}
|
|
99
|
+
// 记录输入
|
|
100
|
+
console.log(`[xiaoyiprovider] input messages count: ${context.messages?.length ?? 0}`);
|
|
101
|
+
if (context.systemPrompt) {
|
|
102
|
+
console.log(`[xiaoyiprovider] system prompt length: ${context.systemPrompt.length}`);
|
|
103
|
+
}
|
|
104
|
+
const stream = await underlying(model, context, {
|
|
105
|
+
...options,
|
|
106
|
+
headers: {
|
|
107
|
+
...options?.headers,
|
|
108
|
+
...dynamicHeaders,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
// 异步监听输出(不阻塞 stream 返回)
|
|
112
|
+
stream.result().then((err) => console.log(`[xiaoyiprovider] error: ${err}`));
|
|
113
|
+
return stream;
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
"QueryCollection",
|
|
18
|
+
"AddCollection",
|
|
19
|
+
"DeleteCollection",
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
export function filterToolsByDevice(tools, deviceType) {
|
|
24
|
+
if (!deviceType)
|
|
25
|
+
return tools;
|
|
26
|
+
const policy = DEVICE_TOOL_POLICY[deviceType];
|
|
27
|
+
if (!policy)
|
|
28
|
+
return tools; // unrecognized device → no filtering
|
|
29
|
+
if (policy.allowlist) {
|
|
30
|
+
return tools.filter((tool) => policy.tools.includes(tool.name));
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
return tools.filter((tool) => !policy.tools.includes(tool.name));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { getXYWebSocketManager } from "../client.js";
|
|
2
|
+
import { sendCommand } from "../formatter.js";
|
|
3
|
+
import { getCurrentSessionContext } from "./session-manager.js";
|
|
4
|
+
/**
|
|
5
|
+
* XY find PC devices tool - finds all PC devices associated with the user.
|
|
6
|
+
* Returns device IDs for use in subsequent file search operations.
|
|
7
|
+
*/
|
|
8
|
+
export const findPcDevicesTool = {
|
|
9
|
+
name: "find_pc_devices",
|
|
10
|
+
label: "Find PC Devices",
|
|
11
|
+
description: `查找用户所有PC/电脑设备,获取设备ID列表。当用户说"帮我找一下PC/电脑上的xxx文件"、"帮我搜索电脑上的xxx"等涉及PC设备的请求时,先调用此工具获取设备ID,再进行后续操作。注意:操作超时时间为60秒,请勿重复调用此工具,如果超时或失败,最多重试一次。回复约束:如果工具返回没有授权或者其他报错,只需要完整描述没有授权或者其他报错内容即可,不需要主动给用户提供解决方案,请严格遵守。`,
|
|
12
|
+
parameters: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {},
|
|
15
|
+
required: [],
|
|
16
|
+
},
|
|
17
|
+
async execute(toolCallId, params) {
|
|
18
|
+
// Get session context
|
|
19
|
+
const sessionContext = getCurrentSessionContext();
|
|
20
|
+
if (!sessionContext) {
|
|
21
|
+
throw new Error("No active XY session found. Find PC devices tool can only be used during an active conversation.");
|
|
22
|
+
}
|
|
23
|
+
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
24
|
+
// Get WebSocket manager
|
|
25
|
+
const wsManager = getXYWebSocketManager(config);
|
|
26
|
+
// Build GetAllDevice command
|
|
27
|
+
const command = {
|
|
28
|
+
header: {
|
|
29
|
+
namespace: "Common",
|
|
30
|
+
name: "Action",
|
|
31
|
+
},
|
|
32
|
+
payload: {
|
|
33
|
+
cardParam: {},
|
|
34
|
+
executeParam: {
|
|
35
|
+
achieveType: "INTENT",
|
|
36
|
+
actionResponse: true,
|
|
37
|
+
bundleName: "com.huawei.hmos.aidispatchservice",
|
|
38
|
+
dimension: "",
|
|
39
|
+
executeMode: "background",
|
|
40
|
+
intentName: "GetAllDevice",
|
|
41
|
+
intentParam: {},
|
|
42
|
+
needUnlock: true,
|
|
43
|
+
permissionId: [],
|
|
44
|
+
timeOut: 5,
|
|
45
|
+
},
|
|
46
|
+
needUploadResult: true,
|
|
47
|
+
pageControlRelated: false,
|
|
48
|
+
responses: [{
|
|
49
|
+
displayText: "",
|
|
50
|
+
resultCode: "",
|
|
51
|
+
ttsText: "",
|
|
52
|
+
}],
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
// Send command and wait for response (60 second timeout)
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const timeout = setTimeout(() => {
|
|
58
|
+
wsManager.off("data-event", handler);
|
|
59
|
+
reject(new Error("查找PC设备超时(60秒)"));
|
|
60
|
+
}, 60000);
|
|
61
|
+
// Listen for data events from WebSocket
|
|
62
|
+
const handler = (event) => {
|
|
63
|
+
if (event.intentName === "GetAllDevice") {
|
|
64
|
+
clearTimeout(timeout);
|
|
65
|
+
wsManager.off("data-event", handler);
|
|
66
|
+
if (event.status === "success" && event.outputs) {
|
|
67
|
+
resolve({
|
|
68
|
+
content: [
|
|
69
|
+
{
|
|
70
|
+
type: "text",
|
|
71
|
+
text: JSON.stringify(event.outputs),
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
reject(new Error(`查找PC设备失败: ${event.status}`));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
// Register event handler
|
|
82
|
+
wsManager.on("data-event", handler);
|
|
83
|
+
// Send the command
|
|
84
|
+
sendCommand({
|
|
85
|
+
config,
|
|
86
|
+
sessionId,
|
|
87
|
+
taskId,
|
|
88
|
+
messageId,
|
|
89
|
+
command,
|
|
90
|
+
}).then(() => {
|
|
91
|
+
}).catch((error) => {
|
|
92
|
+
clearTimeout(timeout);
|
|
93
|
+
wsManager.off("data-event", handler);
|
|
94
|
+
reject(error);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
};
|