@ynhcj/xiaoyi-channel 1.1.19 → 1.1.21
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.d.ts +0 -5
- package/dist/index.js +107 -10
- package/dist/src/channel.js +2 -1
- 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/provider.js +319 -27
- 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.js +4 -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/login-token-tool.d.ts +5 -0
- package/dist/src/tools/login-token-tool.js +136 -0
- 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-self-evolution-skill-tool.d.ts +1 -0
- package/dist/src/tools/save-self-evolution-skill-tool.js +412 -0
- package/dist/src/tools/session-manager.js +2 -0
- package/dist/src/utils/runtime-manager.js +24 -2
- 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 +16 -0
- package/dist/src/utils/tool-call-nudge-manager.js +47 -0
- package/dist/src/websocket.js +18 -0
- package/package.json +2 -2
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
const SKILL_ID = "celia_find_skills";
|
|
5
|
+
const PLUGIN_LOG_PREFIX = "[skill-retriever]";
|
|
6
|
+
export function extractUserQuery(fullPrompt) {
|
|
7
|
+
const lastNewlineIndex = fullPrompt.lastIndexOf("\n");
|
|
8
|
+
if (lastNewlineIndex === -1) {
|
|
9
|
+
return fullPrompt.trim();
|
|
10
|
+
}
|
|
11
|
+
const afterLastNewline = fullPrompt.slice(lastNewlineIndex + 1).trim();
|
|
12
|
+
if (!afterLastNewline || afterLastNewline === "```") {
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
return afterLastNewline;
|
|
16
|
+
}
|
|
17
|
+
function expandPath(filePath) {
|
|
18
|
+
if (filePath.startsWith("~")) {
|
|
19
|
+
return path.join(os.homedir(), filePath.slice(1).replace(/^\/+/, ""));
|
|
20
|
+
}
|
|
21
|
+
return filePath;
|
|
22
|
+
}
|
|
23
|
+
export function readEnvFile(filePath) {
|
|
24
|
+
const expandedPath = expandPath(filePath);
|
|
25
|
+
const envDict = {};
|
|
26
|
+
try {
|
|
27
|
+
const content = fs.readFileSync(expandedPath, "utf-8");
|
|
28
|
+
for (const line of content.split("\n")) {
|
|
29
|
+
const trimmed = line.trim();
|
|
30
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const eqIndex = trimmed.indexOf("=");
|
|
34
|
+
if (eqIndex > 0) {
|
|
35
|
+
let key = trimmed.substring(0, eqIndex).trim();
|
|
36
|
+
const value = trimmed.substring(eqIndex + 1).trim();
|
|
37
|
+
key = key.replace(/-/g, "_");
|
|
38
|
+
envDict[key] = value;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// File not found or read error - return empty config
|
|
44
|
+
}
|
|
45
|
+
return envDict;
|
|
46
|
+
}
|
|
47
|
+
export function getInstalledSkills() {
|
|
48
|
+
const skillsDir = expandPath("~/.openclaw/workspace/skills");
|
|
49
|
+
const installedSkills = [];
|
|
50
|
+
try {
|
|
51
|
+
if (fs.existsSync(skillsDir) && fs.statSync(skillsDir).isDirectory()) {
|
|
52
|
+
const entries = fs.readdirSync(skillsDir);
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
const entryPath = path.join(skillsDir, entry);
|
|
55
|
+
if (fs.statSync(entryPath).isDirectory()) {
|
|
56
|
+
installedSkills.push(entry);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Directory doesn't exist or read error - return empty list
|
|
63
|
+
}
|
|
64
|
+
return installedSkills;
|
|
65
|
+
}
|
|
66
|
+
function formatSkillData(rawSkills, installedSkills) {
|
|
67
|
+
const formattedSkills = [];
|
|
68
|
+
for (const skill of rawSkills) {
|
|
69
|
+
const isInstalled = installedSkills.includes(skill.skillId);
|
|
70
|
+
formattedSkills.push({
|
|
71
|
+
skillId: skill.skillId,
|
|
72
|
+
skillName: skill.skillName,
|
|
73
|
+
skillDesc: skill.skillDesc,
|
|
74
|
+
downloadPath: skill.packUrl,
|
|
75
|
+
status: isInstalled ? "已安装" : "未安装",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return formattedSkills;
|
|
79
|
+
}
|
|
80
|
+
export async function searchTools(options) {
|
|
81
|
+
const { query, maxTools = 5, includeUninstalledOnly = true, envFilePath = "~/.openclaw/.xiaoyienv", serviceUrl: configServiceUrl, apiKey: configApiKey, uid: configUid, timeoutMs = 1000, } = options;
|
|
82
|
+
const envConfig = readEnvFile(envFilePath);
|
|
83
|
+
const hasRequiredConfig = !!envConfig.SERVICE_URL && !!envConfig.PERSONAL_API_KEY && !!envConfig.PERSONAL_UID;
|
|
84
|
+
const serviceUrl = configServiceUrl ?? envConfig.SERVICE_URL;
|
|
85
|
+
const apiKey = configApiKey ?? envConfig.PERSONAL_API_KEY;
|
|
86
|
+
const uid = configUid ?? envConfig.PERSONAL_UID;
|
|
87
|
+
if (!serviceUrl || !apiKey || !uid) {
|
|
88
|
+
console.warn(`${PLUGIN_LOG_PREFIX} Missing required configuration. serviceUrl: "${serviceUrl}", apiKey: "${apiKey ? '(set)' : '(missing)'} ", uid: "${uid ? '(set)' : '(missing)'}"`);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const traceId = crypto.randomUUID();
|
|
92
|
+
const apiUrl = `${serviceUrl}/celia-claw/v1/rest-api/skill/execute`;
|
|
93
|
+
const headers = {
|
|
94
|
+
"Content-Type": "application/json",
|
|
95
|
+
"x-skill-id": SKILL_ID,
|
|
96
|
+
"x-hag-trace-id": traceId,
|
|
97
|
+
"x-uid": uid,
|
|
98
|
+
"x-api-key": apiKey,
|
|
99
|
+
"x-request-from": "openclaw",
|
|
100
|
+
};
|
|
101
|
+
const payload = { query };
|
|
102
|
+
try {
|
|
103
|
+
const response = await fetch(apiUrl, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers,
|
|
106
|
+
body: JSON.stringify(payload),
|
|
107
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
108
|
+
});
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
console.warn(`${PLUGIN_LOG_PREFIX} HTTP error: ${response.status} ${response.statusText}`);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
console.log(`${PLUGIN_LOG_PREFIX} Received response, status: ${response.status}`);
|
|
114
|
+
const responseData = await response.json();
|
|
115
|
+
if (responseData.errorCode === "0" &&
|
|
116
|
+
responseData.content &&
|
|
117
|
+
responseData.content.skills) {
|
|
118
|
+
const rawSkills = responseData.content.skills;
|
|
119
|
+
const installedSkills = getInstalledSkills();
|
|
120
|
+
const formattedData = formatSkillData(rawSkills, installedSkills);
|
|
121
|
+
const topTools = formattedData.slice(0, 2);
|
|
122
|
+
const allInstalled = topTools.every((tool) => tool.status === "已安装");
|
|
123
|
+
if (allInstalled) {
|
|
124
|
+
console.log(`${PLUGIN_LOG_PREFIX} [DEBUG] All top 2 skills are installed, returning null`);
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
let filteredTools = topTools.filter((tool) => tool.status === "未安装");
|
|
128
|
+
console.log(`${PLUGIN_LOG_PREFIX} [DEBUG] After filtering uninstalled: ${filteredTools.length}, ids: ${filteredTools.map((t) => t.skillId).join(", ")}`);
|
|
129
|
+
return {
|
|
130
|
+
tools: filteredTools,
|
|
131
|
+
query,
|
|
132
|
+
timestamp: Date.now(),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
console.warn(`${PLUGIN_LOG_PREFIX} Invalid response format: ${JSON.stringify(responseData).slice(0, 200)}`);
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
const errorName = error instanceof Error ? error.name : "Unknown";
|
|
140
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
141
|
+
const errorCause = error instanceof Error && error.cause ? JSON.stringify(error.cause) : "N/A";
|
|
142
|
+
const errorStack = error instanceof Error ? error.stack?.split("\n").slice(0, 3).join(" | ") : "N/A";
|
|
143
|
+
console.warn(`${PLUGIN_LOG_PREFIX} [ERROR] Fetch failed - name: ${errorName}, message: ${errorMessage}, cause: ${errorCause}, stack: ${errorStack}`);
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export function formatToolsForContext(result, includeInstallUrl = true) {
|
|
148
|
+
if (!result.tools || result.tools.length === 0) {
|
|
149
|
+
return "";
|
|
150
|
+
}
|
|
151
|
+
const toolDescriptions = [];
|
|
152
|
+
for (const tool of result.tools) {
|
|
153
|
+
let description = `### ${tool.skillName}\n`;
|
|
154
|
+
description += `name: ${tool.skillId}\n`;
|
|
155
|
+
description += `description: ${tool.skillDesc}\n`;
|
|
156
|
+
toolDescriptions.push(description);
|
|
157
|
+
}
|
|
158
|
+
return toolDescriptions.join("\n\n");
|
|
159
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface ToolRetrieverConfig {
|
|
2
|
+
enabled: boolean;
|
|
3
|
+
maxTools: number;
|
|
4
|
+
includeUninstalledOnly: boolean;
|
|
5
|
+
envFilePath: string;
|
|
6
|
+
serviceUrl?: string;
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
uid?: string;
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface RawSkill {
|
|
12
|
+
skillId: string;
|
|
13
|
+
skillName: string;
|
|
14
|
+
skillDesc: string;
|
|
15
|
+
packUrl: string;
|
|
16
|
+
}
|
|
17
|
+
export interface FormattedSkill {
|
|
18
|
+
skillId: string;
|
|
19
|
+
skillName: string;
|
|
20
|
+
skillDesc: string;
|
|
21
|
+
downloadPath: string;
|
|
22
|
+
status: "已安装" | "未安装";
|
|
23
|
+
}
|
|
24
|
+
export interface ToolSearchResult {
|
|
25
|
+
tools: FormattedSkill[];
|
|
26
|
+
query: string;
|
|
27
|
+
timestamp: number;
|
|
28
|
+
}
|
|
29
|
+
export interface EnvConfig {
|
|
30
|
+
PERSONAL_API_KEY?: string;
|
|
31
|
+
PERSONAL_UID?: string;
|
|
32
|
+
SERVICE_URL?: string;
|
|
33
|
+
[key: string]: string | undefined;
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -20,6 +20,8 @@ import { saveMediaToGalleryTool } from "./save-media-to-gallery-tool.js";
|
|
|
20
20
|
import { searchFileTool } from "./search-file-tool.js";
|
|
21
21
|
import { uploadFileTool } from "./upload-file-tool.js";
|
|
22
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";
|
|
23
25
|
import { sendStatusUpdate } from "../formatter.js";
|
|
24
26
|
import { getCurrentSessionContext } from "./session-manager.js";
|
|
25
27
|
import { getCurrentTaskId, getCurrentMessageId } from "../task-manager.js";
|
|
@@ -49,6 +51,8 @@ const deviceToolRegistry = new Map([
|
|
|
49
51
|
[searchFileTool.name, searchFileTool],
|
|
50
52
|
[uploadFileTool.name, uploadFileTool],
|
|
51
53
|
[saveFileToPhoneTool.name, saveFileToPhoneTool],
|
|
54
|
+
[sendEmailTool.name, sendEmailTool],
|
|
55
|
+
[searchEmailTool.name, searchEmailTool],
|
|
52
56
|
]);
|
|
53
57
|
/**
|
|
54
58
|
* call_device_tool - 通用端工具调度器。
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const getEmailToolSchemaTool: {
|
|
2
|
+
name: string;
|
|
3
|
+
label: string;
|
|
4
|
+
description: string;
|
|
5
|
+
parameters: {
|
|
6
|
+
type: "object";
|
|
7
|
+
properties: {};
|
|
8
|
+
required: string[];
|
|
9
|
+
};
|
|
10
|
+
execute(_toolCallId: string, _params: any): Promise<{
|
|
11
|
+
content: {
|
|
12
|
+
type: "text";
|
|
13
|
+
text: string;
|
|
14
|
+
}[];
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createSchemaTool } from "./schema-tool-factory.js";
|
|
2
|
+
import { sendEmailTool } from "./send-email-tool.js";
|
|
3
|
+
import { searchEmailTool } from "./search-email-tool.js";
|
|
4
|
+
export const getEmailToolSchemaTool = createSchemaTool({
|
|
5
|
+
name: "get_email_tool_schema",
|
|
6
|
+
label: "Get Email Tool Schema",
|
|
7
|
+
description: "获取可在用户设备上发送邮件、检索邮件的相关端工具列表。",
|
|
8
|
+
tools: [sendEmailTool, searchEmailTool],
|
|
9
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Login Token tool - 自动获取用户授权信息
|
|
2
|
+
import { v4 as uuidv4 } from "uuid";
|
|
3
|
+
import { getXYWebSocketManager } from "../client.js";
|
|
4
|
+
import { getCurrentSessionContext } from "./session-manager.js";
|
|
5
|
+
import { getCurrentTaskId, getCurrentMessageId } from "../task-manager.js";
|
|
6
|
+
import { readFileSync, existsSync } from "fs";
|
|
7
|
+
import { logger } from "../utils/logger.js";
|
|
8
|
+
const TOKEN_FILE_PATH = "/home/sandbox/.openclaw/.xiaoyitoken.json";
|
|
9
|
+
const POLL_INTERVAL_MS = 5000; // 5 seconds
|
|
10
|
+
const TIMEOUT_MS = 60000; // 1 minute
|
|
11
|
+
const TOKEN_VALIDITY_MS = 5 * 60 * 1000; // 5 minutes
|
|
12
|
+
/**
|
|
13
|
+
* get_login_token 工具
|
|
14
|
+
* 当 skill 依赖用户获取鉴权信息时,此工具协助用户快速获取鉴权信息。
|
|
15
|
+
*/
|
|
16
|
+
export const loginTokenTool = {
|
|
17
|
+
name: "get_login_token",
|
|
18
|
+
label: "Get Login Token",
|
|
19
|
+
description: "获取用户授权信息。当skill需要用户鉴权时调用此工具,工具会向用户端发送授权请求,等待用户完成授权后返回结果。请勿重复调用此工具。",
|
|
20
|
+
parameters: {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
clientId: {
|
|
24
|
+
type: "string",
|
|
25
|
+
description: "账号服务唯一标识,在执行具体skill过程中会提供",
|
|
26
|
+
},
|
|
27
|
+
skillName: {
|
|
28
|
+
type: "string",
|
|
29
|
+
description: "具体skill的名称",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
required: ["clientId", "skillName"],
|
|
33
|
+
},
|
|
34
|
+
async execute(toolCallId, params) {
|
|
35
|
+
const { clientId, skillName } = params;
|
|
36
|
+
if (!clientId || typeof clientId !== "string" || clientId.trim() === "") {
|
|
37
|
+
throw new Error("Missing required parameter: clientId must be a non-empty string");
|
|
38
|
+
}
|
|
39
|
+
if (!skillName || typeof skillName !== "string" || skillName.trim() === "") {
|
|
40
|
+
throw new Error("Missing required parameter: skillName must be a non-empty string");
|
|
41
|
+
}
|
|
42
|
+
const sessionContext = getCurrentSessionContext();
|
|
43
|
+
if (!sessionContext) {
|
|
44
|
+
throw new Error("No active XY session found. Login token tool can only be used during an active conversation.");
|
|
45
|
+
}
|
|
46
|
+
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
47
|
+
const currentTaskId = getCurrentTaskId(sessionId) ?? taskId;
|
|
48
|
+
const currentMessageId = getCurrentMessageId(sessionId) ?? messageId;
|
|
49
|
+
// (1) Build and send getLoginToken artifact
|
|
50
|
+
const artifactId = uuidv4();
|
|
51
|
+
const artifact = {
|
|
52
|
+
taskId: currentTaskId,
|
|
53
|
+
kind: "artifact-update",
|
|
54
|
+
append: false,
|
|
55
|
+
lastChunk: true,
|
|
56
|
+
final: false,
|
|
57
|
+
artifact: {
|
|
58
|
+
artifactId,
|
|
59
|
+
parts: [
|
|
60
|
+
{
|
|
61
|
+
kind: "getLoginToken",
|
|
62
|
+
clientId: clientId.trim(),
|
|
63
|
+
skillName: skillName.trim(),
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
const jsonRpcResponse = {
|
|
69
|
+
jsonrpc: "2.0",
|
|
70
|
+
id: currentMessageId,
|
|
71
|
+
result: artifact,
|
|
72
|
+
};
|
|
73
|
+
const wsManager = getXYWebSocketManager(config);
|
|
74
|
+
const outboundMessage = {
|
|
75
|
+
msgType: "agent_response",
|
|
76
|
+
agentId: config.agentId,
|
|
77
|
+
sessionId,
|
|
78
|
+
taskId: currentTaskId,
|
|
79
|
+
msgDetail: JSON.stringify(jsonRpcResponse),
|
|
80
|
+
};
|
|
81
|
+
logger.log(`[LOGIN_TOKEN] Sending getLoginToken artifact for clientId=${clientId}, skillName=${skillName}`);
|
|
82
|
+
await wsManager.sendMessage(sessionId, outboundMessage);
|
|
83
|
+
logger.log(`[LOGIN_TOKEN] Artifact sent successfully`);
|
|
84
|
+
// (2) Poll .xiaoyitoken.json every 5 seconds
|
|
85
|
+
const startTime = Date.now();
|
|
86
|
+
return new Promise((resolve) => {
|
|
87
|
+
const poll = () => {
|
|
88
|
+
const elapsed = Date.now() - startTime;
|
|
89
|
+
if (elapsed >= TIMEOUT_MS) {
|
|
90
|
+
// (4) Timeout after 1 minute
|
|
91
|
+
logger.log(`[LOGIN_TOKEN] Timeout: failed to get login token for clientId=${clientId}`);
|
|
92
|
+
resolve({
|
|
93
|
+
content: [
|
|
94
|
+
{
|
|
95
|
+
type: "text",
|
|
96
|
+
text: "获取用户授权失败",
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
if (existsSync(TOKEN_FILE_PATH)) {
|
|
104
|
+
const content = readFileSync(TOKEN_FILE_PATH, "utf-8");
|
|
105
|
+
const tokens = JSON.parse(content);
|
|
106
|
+
const match = tokens.find((t) => t.clientId === clientId.trim());
|
|
107
|
+
if (match) {
|
|
108
|
+
const tokenTime = Number(match.timestamp);
|
|
109
|
+
const diff = Date.now() - tokenTime;
|
|
110
|
+
if (diff <= TOKEN_VALIDITY_MS) {
|
|
111
|
+
// (3) Found valid token
|
|
112
|
+
logger.log(`[LOGIN_TOKEN] Successfully got login token for clientId=${clientId}`);
|
|
113
|
+
resolve({
|
|
114
|
+
content: [
|
|
115
|
+
{
|
|
116
|
+
type: "text",
|
|
117
|
+
text: "获取用户授权成功",
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
logger.log(`[LOGIN_TOKEN] Error reading token file: ${err}`);
|
|
128
|
+
}
|
|
129
|
+
// Not found or not valid, poll again after 5 seconds
|
|
130
|
+
setTimeout(poll, POLL_INTERVAL_MS);
|
|
131
|
+
};
|
|
132
|
+
// Start polling after 5 seconds
|
|
133
|
+
setTimeout(poll, POLL_INTERVAL_MS);
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// QueryAppMessage tool implementation
|
|
2
|
+
import { getXYWebSocketManager } from "../client.js";
|
|
3
|
+
import { sendCommand } from "../formatter.js";
|
|
4
|
+
import { getCurrentSessionContext } from "./session-manager.js";
|
|
5
|
+
class ToolInputError extends Error {
|
|
6
|
+
status = 400;
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "ToolInputError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 查询指定时间范围内的设备通知消息。
|
|
14
|
+
*/
|
|
15
|
+
export const queryAppMessageTool = {
|
|
16
|
+
name: "query_app_message",
|
|
17
|
+
label: "Query App Message",
|
|
18
|
+
description: `获取指定时间范围内的设备通知消息。适用于需要查询历史通知、按应用筛选通知、或仅查看未读通知的场景。支持按时间范围、应用包名、已读/未读状态进行过滤。
|
|
19
|
+
注意:
|
|
20
|
+
a. 操作超时时间为60秒,请勿重复调用此工具
|
|
21
|
+
b. 如果遇到各类调用失败场景,最多只能重试一次,不可以重复调用多次。
|
|
22
|
+
c. 调用工具前需认真检查调用参数是否满足工具要求
|
|
23
|
+
|
|
24
|
+
回复约束:如果工具返回没有授权或者其他报错,只需要完整描述没有授权或者其他报错内容即可,不需要主动给用户提供解决方案,例如告诉用户如何授权,如何解决报错等都是不需要的,请严格遵守。`,
|
|
25
|
+
parameters: {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {
|
|
28
|
+
startTime: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "查询通知的起始时间(ISO 8601 字符串)。若endTime为空,则默认值为24小时前。",
|
|
31
|
+
},
|
|
32
|
+
endTime: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "查询通知的结束时间(ISO 8601 字符串)。默认值为当前时间。",
|
|
35
|
+
},
|
|
36
|
+
packageName: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description: "按应用名称过滤通知(例如「微信」「小红书」)。默认值为所有应用。",
|
|
39
|
+
},
|
|
40
|
+
state: {
|
|
41
|
+
type: "integer",
|
|
42
|
+
description: "通知的已读/未读状态。0 = 全部,1 = 仅未读。默认值为 0。",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
required: [],
|
|
46
|
+
},
|
|
47
|
+
async execute(_toolCallId, params) {
|
|
48
|
+
const sessionContext = getCurrentSessionContext();
|
|
49
|
+
if (!sessionContext) {
|
|
50
|
+
throw new Error("No active XY session found.");
|
|
51
|
+
}
|
|
52
|
+
const { config, sessionId, taskId, messageId } = sessionContext;
|
|
53
|
+
const wsManager = getXYWebSocketManager(config);
|
|
54
|
+
const intentParam = {};
|
|
55
|
+
if (params.startTime !== undefined)
|
|
56
|
+
intentParam.startTime = params.startTime;
|
|
57
|
+
if (params.endTime !== undefined)
|
|
58
|
+
intentParam.endTime = params.endTime;
|
|
59
|
+
if (params.packageName !== undefined)
|
|
60
|
+
intentParam.packageName = params.packageName;
|
|
61
|
+
if (params.state !== undefined) {
|
|
62
|
+
if (params.state !== 0 && params.state !== 1) {
|
|
63
|
+
throw new ToolInputError("state 参数只能为 0(全部)或 1(仅未读)");
|
|
64
|
+
}
|
|
65
|
+
intentParam.state = params.state;
|
|
66
|
+
}
|
|
67
|
+
const command = {
|
|
68
|
+
header: {
|
|
69
|
+
namespace: "Common",
|
|
70
|
+
name: "Action",
|
|
71
|
+
},
|
|
72
|
+
payload: {
|
|
73
|
+
cardParam: {},
|
|
74
|
+
executeParam: {
|
|
75
|
+
executeMode: "background",
|
|
76
|
+
intentName: "QueryAppMessage",
|
|
77
|
+
bundleName: "com.huawei.hmos.vassistant",
|
|
78
|
+
needUnlock: true,
|
|
79
|
+
actionResponse: true,
|
|
80
|
+
appType: "OHOS_APP",
|
|
81
|
+
timeOut: 5,
|
|
82
|
+
intentParam,
|
|
83
|
+
permissionId: [],
|
|
84
|
+
achieveType: "INTENT",
|
|
85
|
+
},
|
|
86
|
+
responses: [
|
|
87
|
+
{
|
|
88
|
+
resultCode: "",
|
|
89
|
+
displayText: "",
|
|
90
|
+
ttsText: "",
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
needUploadResult: true,
|
|
94
|
+
noHalfPage: false,
|
|
95
|
+
pageControlRelated: false,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
const timeout = setTimeout(() => {
|
|
100
|
+
wsManager.off("data-event", handler);
|
|
101
|
+
reject(new Error("查询通知消息超时(60秒)"));
|
|
102
|
+
}, 60000);
|
|
103
|
+
const handler = (event) => {
|
|
104
|
+
if (event.intentName === "QueryAppMessage") {
|
|
105
|
+
clearTimeout(timeout);
|
|
106
|
+
wsManager.off("data-event", handler);
|
|
107
|
+
if (event.status === "success" && event.outputs) {
|
|
108
|
+
resolve({
|
|
109
|
+
content: [
|
|
110
|
+
{
|
|
111
|
+
type: "text",
|
|
112
|
+
text: JSON.stringify(event.outputs),
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
reject(new Error(`查询通知消息失败: ${event.status}`));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
wsManager.on("data-event", handler);
|
|
123
|
+
sendCommand({
|
|
124
|
+
config,
|
|
125
|
+
sessionId,
|
|
126
|
+
taskId,
|
|
127
|
+
messageId,
|
|
128
|
+
command,
|
|
129
|
+
})
|
|
130
|
+
.then(() => { })
|
|
131
|
+
.catch((error) => {
|
|
132
|
+
clearTimeout(timeout);
|
|
133
|
+
wsManager.off("data-event", handler);
|
|
134
|
+
reject(error);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
};
|