@ynhcj/xiaoyi-channel 0.0.78-beta → 0.0.80-beta
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 +5 -4
- 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/provider.js +9 -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/upload-file-tool.js +2 -2
- package/dist/src/tools/xiaoyi-add-collection-tool.js +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -20,13 +20,13 @@ const plugin = {
|
|
|
20
20
|
setXYRuntime(api.runtime);
|
|
21
21
|
api.registerChannel({ plugin: xyPlugin });
|
|
22
22
|
api.registerProvider(xiaoyiProvider);
|
|
23
|
-
//
|
|
23
|
+
// SENTINEL HOOK after_tool_call hook: 监听工具结果,发送至安全检测 API 进行安全检测
|
|
24
24
|
// 如果响应为 REJECT,注入 steer 消息中止当前对话
|
|
25
25
|
api.on("after_tool_call", async (event, ctx) => {
|
|
26
26
|
if (!ALLOWED_TOOLS.includes(event.toolName)) {
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
|
-
console.log(`[
|
|
29
|
+
console.log(`[SENTINEL HOOK] after_tool_call triggered: toolName=${event.toolName}, sessionKey=${ctx.sessionKey ?? "none"}`);
|
|
30
30
|
try {
|
|
31
31
|
const resultText = extractResultText(event, event.toolName);
|
|
32
32
|
const resultLength = resultText.length;
|
|
@@ -35,6 +35,7 @@ const plugin = {
|
|
|
35
35
|
}
|
|
36
36
|
// 构造 sentinel_hook 格式的 payload: { tool, output: [{ content }] }
|
|
37
37
|
const questionText = {
|
|
38
|
+
subSceneID: 'TOOL_OUTPUT',
|
|
38
39
|
tool: event.toolName,
|
|
39
40
|
output: [{ content: "" }],
|
|
40
41
|
};
|
|
@@ -49,13 +50,13 @@ const plugin = {
|
|
|
49
50
|
}
|
|
50
51
|
const response = await callCsplApi(finalJson, api.config);
|
|
51
52
|
const result = parseSecurityResult(response);
|
|
52
|
-
console.log(`[
|
|
53
|
+
console.log(`[SENTINEL HOOK] Security result: status=${result.status}`);
|
|
53
54
|
if (result.status === "REJECT") {
|
|
54
55
|
await tryInjectSteer(ctx.sessionKey, STEER_ABORT_MESSAGE);
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
catch (err) {
|
|
58
|
-
api.logger.error(`[
|
|
59
|
+
api.logger.error(`[SENTINEL HOOK] after_tool_call error: ${err}`);
|
|
59
60
|
}
|
|
60
61
|
});
|
|
61
62
|
},
|
|
@@ -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/provider.js
CHANGED
|
@@ -101,6 +101,12 @@ export const xiaoyiProvider = {
|
|
|
101
101
|
if (context.systemPrompt) {
|
|
102
102
|
console.log(`[xiaoyiprovider] system prompt length: ${context.systemPrompt.length}`);
|
|
103
103
|
}
|
|
104
|
+
// 在发送给模型前,删除 systemPrompt 中 ## Tooling 与 TOOLS.md 声明之间的内容
|
|
105
|
+
if (context.systemPrompt) {
|
|
106
|
+
const before = context.systemPrompt.length;
|
|
107
|
+
context.systemPrompt = context.systemPrompt.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");
|
|
108
|
+
console.log(`[xiaoyiprovider] system prompt trimmed: ${before} -> ${context.systemPrompt.length}`);
|
|
109
|
+
}
|
|
104
110
|
const stream = await underlying(model, context, {
|
|
105
111
|
...options,
|
|
106
112
|
headers: {
|
|
@@ -109,7 +115,9 @@ export const xiaoyiProvider = {
|
|
|
109
115
|
},
|
|
110
116
|
});
|
|
111
117
|
// 异步监听输出(不阻塞 stream 返回)
|
|
112
|
-
stream.result().then((
|
|
118
|
+
stream.result().then((result) => {
|
|
119
|
+
console.log(`[xiaoyiprovider] stream completed, usage: input=${result.usage?.input} output=${result.usage?.output}`);
|
|
120
|
+
}, (err) => console.log(`[xiaoyiprovider] stream error: ${JSON.stringify(err)}`));
|
|
113
121
|
return stream;
|
|
114
122
|
};
|
|
115
123
|
},
|
|
@@ -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
|
+
};
|
|
@@ -18,10 +18,10 @@ export const uploadFileTool = {
|
|
|
18
18
|
label: "Upload File",
|
|
19
19
|
description: `工具能力描述:将手机本地文件上传并获取可公网访问的 URL。
|
|
20
20
|
|
|
21
|
-
前置工具调用:此工具使用前必须先调用 search_file 或者
|
|
21
|
+
前置工具调用:此工具使用前必须先调用 search_file 或者 query_collection 工具获取文件的 uri=
|
|
22
22
|
|
|
23
23
|
工具参数说明:
|
|
24
|
-
a. 入参中的fileInfos数组,每个元素必须包含mediaUri字段(对应于search_file工具或者
|
|
24
|
+
a. 入参中的fileInfos数组,每个元素必须包含mediaUri字段(对应于search_file工具或者query_collection返回结果中的uri),必须与search_file或者query_collection结果中对应的uri完全保持一致,不要自行修改。
|
|
25
25
|
b. fileInfos中的timeout字段是可选的,表示上传文件超时时间,单位是毫秒,默认是20000(20秒)。
|
|
26
26
|
c. fileInfos 是文件在手机本地的信息数组(从 search_file 工具响应中获取)。限制:每次最多支持传入 5 条文件信息。
|
|
27
27
|
|
|
@@ -28,6 +28,7 @@ export const xiaoyiAddCollectionTool = {
|
|
|
28
28
|
● dataType:必填字段,数据类型为string,功能描述是标识数据类型。HYPER_LINK标识网页,TEXT标识文本,IMAGE标识图片,FILE标识文件。
|
|
29
29
|
● title:非必填字段,数据类型为string,功能描述是标识文件类型数据的文件名称。适用于FILE类型。
|
|
30
30
|
说明:如果dataType为HYPER_LINK或TEXT,则content字段必填且不能为空;如果dataType为IMAGE或FILE,则uri字段必填且不能为空。当用户希望收藏海报、截图等图片类数据时,请将数据以图片IMAGE的形式存入到小艺帮记;当用户希望收藏电子书、笔记、报告、素材、文档、合同、协议、简历、证书、报表、日志、安装包、压缩包等描述的文件时,请将数据以文件FILE的形式存入到小艺帮记。
|
|
31
|
+
当你成功收藏这个数据到小艺帮记后,请在最后显示"已成功把数据添加到[小艺帮记](vassistant://voice/main?page=CollectionPage&jumpHomePageTab=myCollection)",
|
|
31
32
|
注意:
|
|
32
33
|
a. 操作超时时间为60秒,请勿重复调用此工具
|
|
33
34
|
b. 如果遇到各类调用失败场景,最多只能重试一次,不可以重复调用多次。
|