@ynhcj/xiaoyi-channel 1.1.25 → 1.1.27
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/src/approval-bridge.d.ts +48 -0
- package/dist/src/approval-bridge.js +382 -0
- package/dist/src/bot.js +2 -2
- package/dist/src/cron-command.d.ts +15 -0
- package/dist/src/cron-command.js +49 -0
- package/dist/src/cron-query-handler.d.ts +17 -0
- package/dist/src/cron-query-handler.js +101 -0
- package/dist/src/cspl/call_api.d.ts +2 -0
- package/dist/src/cspl/call_api.js +107 -0
- package/dist/src/cspl/configs.json +10 -0
- package/dist/src/cspl/sentinel_hook.d.ts +2 -0
- package/dist/src/cspl/sentinel_hook.js +98 -0
- package/dist/src/cspl/upload_file.d.ts +1 -0
- package/dist/src/cspl/upload_file.js +211 -0
- package/dist/src/provider.js +50 -70
- package/dist/src/sensitive-redactor.d.ts +4 -0
- package/dist/src/sensitive-redactor.js +364 -0
- package/dist/src/tools/agent-as-skill-tool.d.ts +7 -0
- package/dist/src/tools/agent-as-skill-tool.js +190 -0
- package/dist/src/tools/device-tool-map.d.ts +1 -1
- package/dist/src/tools/device-tool-map.js +11 -5
- package/dist/src/tools/discover-cross-devices-tool.d.ts +2 -0
- package/dist/src/tools/discover-cross-devices-tool.js +235 -0
- package/dist/src/tools/find-pc-devices-tool.d.ts +2 -1
- package/dist/src/tools/find-pc-devices-tool.js +85 -88
- package/dist/src/tools/image-reading-tool.js +7 -6
- package/dist/src/tools/send-cross-device-task-tool.d.ts +2 -0
- package/dist/src/tools/send-cross-device-task-tool.js +299 -0
- package/package.json +1 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approval bridge for Xiaoyi channel.
|
|
3
|
+
*
|
|
4
|
+
* 目标:
|
|
5
|
+
* 1. 把 OpenClaw 发出的 /approve 文本提示,改写成更适合 Xiaoyi 用户理解的自然语言确认提示。
|
|
6
|
+
* 2. 仅在当前 session 确实存在待审批状态时,才把用户回复的“确认 / 拒绝”等短指令回写成 /approve。
|
|
7
|
+
* 3. 每个 session 只保留最新的一条待审批记录,避免桥接状态无限累积。
|
|
8
|
+
*
|
|
9
|
+
* 注意:
|
|
10
|
+
* - 这里的 Map 只是桥接层缓存,不是真正的审批来源。
|
|
11
|
+
* - 真正的审批状态仍然由 OpenClaw gateway 维护。
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* 出站改写:
|
|
15
|
+
* - 如果是审批提示,则记录待审批状态,并输出更友好的确认文案。
|
|
16
|
+
* - 如果是 /approve 的成功或失败回执,则改写成用户更容易理解的文本。
|
|
17
|
+
*/
|
|
18
|
+
export declare function rewriteOutboundApprovalText(sessionId: any, text: any): any;
|
|
19
|
+
/**
|
|
20
|
+
* 入站改写:
|
|
21
|
+
* - 先检查当前 session 是否存在有效待审批状态。
|
|
22
|
+
* - 只有存在待审批状态时,才把“确认 / 拒绝”等短回复翻译成 /approve。
|
|
23
|
+
* - 没有待审批状态时,完全不介入,避免影响普通对话。
|
|
24
|
+
*/
|
|
25
|
+
export declare function rewriteInboundApprovalText(sessionId: any, text: any): {
|
|
26
|
+
matched: boolean;
|
|
27
|
+
rewrittenText: any;
|
|
28
|
+
decision?: undefined;
|
|
29
|
+
approvalId?: undefined;
|
|
30
|
+
} | {
|
|
31
|
+
matched: boolean;
|
|
32
|
+
decision: string;
|
|
33
|
+
approvalId: any;
|
|
34
|
+
rewrittenText: string;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* 清理指定 session 的待审批状态。
|
|
38
|
+
* 用于 clearContext / cancel 等显式结束会话的场景。
|
|
39
|
+
*/
|
|
40
|
+
export declare function clearPendingApproval(sessionId: any): void;
|
|
41
|
+
/**
|
|
42
|
+
* 仅用于本地调试或测试。
|
|
43
|
+
*/
|
|
44
|
+
export declare function getPendingApproval(sessionId: any): any;
|
|
45
|
+
/**
|
|
46
|
+
* 仅用于本地调试或测试。
|
|
47
|
+
*/
|
|
48
|
+
export declare function clearAllPendingApprovals(): void;
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approval bridge for Xiaoyi channel.
|
|
3
|
+
*
|
|
4
|
+
* 目标:
|
|
5
|
+
* 1. 把 OpenClaw 发出的 /approve 文本提示,改写成更适合 Xiaoyi 用户理解的自然语言确认提示。
|
|
6
|
+
* 2. 仅在当前 session 确实存在待审批状态时,才把用户回复的“确认 / 拒绝”等短指令回写成 /approve。
|
|
7
|
+
* 3. 每个 session 只保留最新的一条待审批记录,避免桥接状态无限累积。
|
|
8
|
+
*
|
|
9
|
+
* 注意:
|
|
10
|
+
* - 这里的 Map 只是桥接层缓存,不是真正的审批来源。
|
|
11
|
+
* - 真正的审批状态仍然由 OpenClaw gateway 维护。
|
|
12
|
+
*/
|
|
13
|
+
// 一键开关 Xiaoyi channel 的 approval bridge。
|
|
14
|
+
// false 时:
|
|
15
|
+
// 1. 不再改写 OpenClaw 发出的审批提示文本;
|
|
16
|
+
// 2. 不再缓存待审批状态;
|
|
17
|
+
// 3. 不再把用户回复的“确认/拒绝”翻译回 /approve。
|
|
18
|
+
const ENABLE_APPROVAL_BRIDGE = true;
|
|
19
|
+
const DEFAULT_APPROVAL_TTL_MS = 2 * 60 * 1000;
|
|
20
|
+
const ALLOW_ONCE_KEYWORDS = new Set([
|
|
21
|
+
"确认",
|
|
22
|
+
"同意",
|
|
23
|
+
"允许",
|
|
24
|
+
"继续",
|
|
25
|
+
"确认执行",
|
|
26
|
+
"继续执行",
|
|
27
|
+
"ok",
|
|
28
|
+
"okay",
|
|
29
|
+
"yes",
|
|
30
|
+
"y",
|
|
31
|
+
]);
|
|
32
|
+
const ALLOW_ALWAYS_KEYWORDS = new Set([
|
|
33
|
+
"总是允许",
|
|
34
|
+
"始终允许",
|
|
35
|
+
"一直允许",
|
|
36
|
+
"永久允许",
|
|
37
|
+
"以后都允许",
|
|
38
|
+
"全部允许",
|
|
39
|
+
]);
|
|
40
|
+
const DENY_KEYWORDS = new Set([
|
|
41
|
+
"拒绝",
|
|
42
|
+
"取消",
|
|
43
|
+
"不同意",
|
|
44
|
+
"停止",
|
|
45
|
+
"停止执行",
|
|
46
|
+
"deny",
|
|
47
|
+
"no",
|
|
48
|
+
"n",
|
|
49
|
+
]);
|
|
50
|
+
/**
|
|
51
|
+
* sessionId -> 当前最新待审批记录
|
|
52
|
+
*/
|
|
53
|
+
const pendingApprovals = new Map();
|
|
54
|
+
function normalizeLineBreaks(text) {
|
|
55
|
+
return text.replace(/\r\n/g, "\n");
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 只去掉首尾空白和常见标点,避免“确认。”、“ok!”这类轻微变体无法命中。
|
|
59
|
+
*/
|
|
60
|
+
function normalizeReplyText(text) {
|
|
61
|
+
return text
|
|
62
|
+
.trim()
|
|
63
|
+
.replace(/^[\s"'`“”‘’.,!?,。!?::;;()()\[\]【】]+/, "")
|
|
64
|
+
.replace(/[\s"'`“”‘’.,!?,。!?::;;()()\[\]【】]+$/, "")
|
|
65
|
+
.trim()
|
|
66
|
+
.toLowerCase();
|
|
67
|
+
}
|
|
68
|
+
function cleanupExpiredApprovals(now = Date.now()) {
|
|
69
|
+
for (const [sessionId, record] of pendingApprovals.entries()) {
|
|
70
|
+
if (record.expiresAtMs <= now) {
|
|
71
|
+
pendingApprovals.delete(sessionId);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function parseAllowedDecisions(raw) {
|
|
76
|
+
const normalized = typeof raw === "string" ? raw.trim() : "";
|
|
77
|
+
if (!normalized) {
|
|
78
|
+
return ["allow-once", "allow-always", "deny"];
|
|
79
|
+
}
|
|
80
|
+
const decisions = normalized
|
|
81
|
+
.split("|")
|
|
82
|
+
.map(item => item.trim().toLowerCase())
|
|
83
|
+
.filter(item => item === "allow-once" || item === "allow-always" || item === "deny");
|
|
84
|
+
return decisions.length > 0 ? decisions : ["allow-once", "allow-always", "deny"];
|
|
85
|
+
}
|
|
86
|
+
function extractApprovalCommandId(replyCommandId, explicitApprovalId) {
|
|
87
|
+
const normalizedReplyId = typeof replyCommandId === "string" ? replyCommandId.trim() : "";
|
|
88
|
+
const normalizedApprovalId = typeof explicitApprovalId === "string" ? explicitApprovalId.trim() : "";
|
|
89
|
+
if (normalizedApprovalId) {
|
|
90
|
+
return normalizedApprovalId;
|
|
91
|
+
}
|
|
92
|
+
if (normalizedReplyId && normalizedReplyId !== "<id>") {
|
|
93
|
+
return normalizedReplyId;
|
|
94
|
+
}
|
|
95
|
+
return "";
|
|
96
|
+
}
|
|
97
|
+
function buildDecisionHint(allowedDecisions) {
|
|
98
|
+
const hints = [];
|
|
99
|
+
if (allowedDecisions.includes("allow-once")) {
|
|
100
|
+
hints.push("“确认”");
|
|
101
|
+
}
|
|
102
|
+
if (allowedDecisions.includes("allow-always")) {
|
|
103
|
+
hints.push("“总是允许”");
|
|
104
|
+
}
|
|
105
|
+
if (allowedDecisions.includes("deny")) {
|
|
106
|
+
hints.push("“拒绝”");
|
|
107
|
+
}
|
|
108
|
+
if (hints.length === 0) {
|
|
109
|
+
return "请回复确认结果。";
|
|
110
|
+
}
|
|
111
|
+
if (hints.length === 1) {
|
|
112
|
+
return `请回复 ${hints[0]}。`;
|
|
113
|
+
}
|
|
114
|
+
if (hints.length === 2) {
|
|
115
|
+
return `请回复 ${hints[0]} 或 ${hints[1]}。`;
|
|
116
|
+
}
|
|
117
|
+
return `请回复 ${hints[0]}、${hints[1]} 或 ${hints[2]}。`;
|
|
118
|
+
}
|
|
119
|
+
function buildExecApprovalPrompt(record) {
|
|
120
|
+
const lines = ["检测到需要人工确认的高风险命令。"];
|
|
121
|
+
if (record.warningText) {
|
|
122
|
+
lines.push(`说明:${record.warningText}`);
|
|
123
|
+
}
|
|
124
|
+
lines.push(`执行位置:${record.host === "node" ? "node" : "gateway"}`);
|
|
125
|
+
if (record.cwd) {
|
|
126
|
+
lines.push(`工作目录:${record.cwd}`);
|
|
127
|
+
}
|
|
128
|
+
if (record.command) {
|
|
129
|
+
lines.push("待执行命令:");
|
|
130
|
+
lines.push("```sh");
|
|
131
|
+
lines.push(record.command);
|
|
132
|
+
lines.push("```");
|
|
133
|
+
}
|
|
134
|
+
lines.push(buildDecisionHint(record.allowedDecisions));
|
|
135
|
+
return lines.join("\n");
|
|
136
|
+
}
|
|
137
|
+
function buildPluginApprovalPrompt(record) {
|
|
138
|
+
const lines = ["检测到当前操作需要人工确认。"];
|
|
139
|
+
if (record.title) {
|
|
140
|
+
lines.push(`标题:${record.title}`);
|
|
141
|
+
}
|
|
142
|
+
if (record.description) {
|
|
143
|
+
lines.push(`说明:${record.description}`);
|
|
144
|
+
}
|
|
145
|
+
if (record.toolName) {
|
|
146
|
+
lines.push(`工具:${record.toolName}`);
|
|
147
|
+
}
|
|
148
|
+
if (record.pluginId) {
|
|
149
|
+
lines.push(`来源插件:${record.pluginId}`);
|
|
150
|
+
}
|
|
151
|
+
lines.push(buildDecisionHint(record.allowedDecisions));
|
|
152
|
+
return lines.join("\n");
|
|
153
|
+
}
|
|
154
|
+
function buildApprovalPrompt(record) {
|
|
155
|
+
return record.kind === "exec"
|
|
156
|
+
? buildExecApprovalPrompt(record)
|
|
157
|
+
: buildPluginApprovalPrompt(record);
|
|
158
|
+
}
|
|
159
|
+
function parseExecApprovalPrompt(text) {
|
|
160
|
+
const normalized = normalizeLineBreaks(text);
|
|
161
|
+
const headerMatch = normalized.match(/Approval required \(id ([^,\n]+), full ([^)]+)\)\./i);
|
|
162
|
+
const replyMatch = normalized.match(/Reply with:\s*\/approve\s+([^\s]+)\s+([A-Za-z|-]+)/i);
|
|
163
|
+
if (!headerMatch || !replyMatch) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const commandMatch = normalized.match(/Command:\n```(?:[a-zA-Z0-9_-]+)?\n([\s\S]*?)\n```/i);
|
|
167
|
+
const hostMatch = normalized.match(/^Host:\s*(.+)$/im);
|
|
168
|
+
const cwdMatch = normalized.match(/^CWD:\s*(.+)$/im);
|
|
169
|
+
const warningIndex = normalized.indexOf("Approval required");
|
|
170
|
+
const warningText = warningIndex > 0 ? normalized.slice(0, warningIndex).trim() : "";
|
|
171
|
+
const allowedDecisions = parseAllowedDecisions(replyMatch[2]);
|
|
172
|
+
const approvalId = extractApprovalCommandId(replyMatch[1], headerMatch[2]);
|
|
173
|
+
if (!approvalId) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
kind: "exec",
|
|
178
|
+
approvalId,
|
|
179
|
+
approvalSlug: headerMatch[1].trim(),
|
|
180
|
+
allowedDecisions,
|
|
181
|
+
command: commandMatch?.[1]?.trim() ?? "",
|
|
182
|
+
host: hostMatch?.[1]?.trim() === "node" ? "node" : "gateway",
|
|
183
|
+
cwd: cwdMatch?.[1]?.trim() ?? "",
|
|
184
|
+
warningText,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function parsePluginApprovalPrompt(text) {
|
|
188
|
+
const normalized = normalizeLineBreaks(text);
|
|
189
|
+
if (!/Plugin approval required/i.test(normalized)) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const idMatch = normalized.match(/^ID:\s*(.+)$/im);
|
|
193
|
+
const replyMatch = normalized.match(/Reply with:\s*\/approve\s+([^\s]+)\s+([A-Za-z|-]+)/i);
|
|
194
|
+
const titleMatch = normalized.match(/^Title:\s*(.+)$/im);
|
|
195
|
+
const descriptionMatch = normalized.match(/^Description:\s*(.+)$/im);
|
|
196
|
+
const toolMatch = normalized.match(/^Tool:\s*(.+)$/im);
|
|
197
|
+
const pluginMatch = normalized.match(/^Plugin:\s*(.+)$/im);
|
|
198
|
+
const agentMatch = normalized.match(/^Agent:\s*(.+)$/im);
|
|
199
|
+
const expiresMatch = normalized.match(/^Expires in:\s*(\d+)s$/im);
|
|
200
|
+
const allowedDecisions = parseAllowedDecisions(replyMatch?.[2]);
|
|
201
|
+
const approvalId = extractApprovalCommandId(replyMatch?.[1], idMatch?.[1]);
|
|
202
|
+
if (!approvalId) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
kind: "plugin",
|
|
207
|
+
approvalId,
|
|
208
|
+
approvalSlug: approvalId.slice(0, 8),
|
|
209
|
+
allowedDecisions,
|
|
210
|
+
title: titleMatch?.[1]?.trim() ?? "",
|
|
211
|
+
description: descriptionMatch?.[1]?.trim() ?? "",
|
|
212
|
+
toolName: toolMatch?.[1]?.trim() ?? "",
|
|
213
|
+
pluginId: pluginMatch?.[1]?.trim() ?? "",
|
|
214
|
+
agentId: agentMatch?.[1]?.trim() ?? "",
|
|
215
|
+
expiresInMs: expiresMatch ? Number(expiresMatch[1]) * 1000 : undefined,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function parseApprovalPrompt(text) {
|
|
219
|
+
return parseExecApprovalPrompt(text) ?? parsePluginApprovalPrompt(text);
|
|
220
|
+
}
|
|
221
|
+
function parseApprovalSubmissionAcknowledgement(text) {
|
|
222
|
+
const normalized = normalizeLineBreaks(text).trim();
|
|
223
|
+
const commandAckMatch = normalized.match(/^[✅✔]?\s*Approval\s+(allow-once|allow-always|deny)\s+submitted\s+for\s+(.+?)\.?$/i);
|
|
224
|
+
if (commandAckMatch) {
|
|
225
|
+
return { decision: commandAckMatch[1].toLowerCase() };
|
|
226
|
+
}
|
|
227
|
+
const pluginAckMatch = normalized.match(/^[✅✔]?\s*Plugin approval\s+(allowed once|allowed always|denied)\./i);
|
|
228
|
+
if (pluginAckMatch) {
|
|
229
|
+
const decisionLabel = pluginAckMatch[1].toLowerCase();
|
|
230
|
+
if (decisionLabel === "allowed always") {
|
|
231
|
+
return { decision: "allow-always" };
|
|
232
|
+
}
|
|
233
|
+
if (decisionLabel === "denied") {
|
|
234
|
+
return { decision: "deny" };
|
|
235
|
+
}
|
|
236
|
+
return { decision: "allow-once" };
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
function parseApprovalSubmissionFailure(text) {
|
|
241
|
+
const normalized = normalizeLineBreaks(text).trim();
|
|
242
|
+
const failureMatch = normalized.match(/^[❌x]\s*Failed to submit approval:\s*(.+)$/i);
|
|
243
|
+
if (!failureMatch) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
return failureMatch[1].trim();
|
|
247
|
+
}
|
|
248
|
+
function buildApprovalAcknowledgement(decision) {
|
|
249
|
+
if (decision === "allow-always") {
|
|
250
|
+
return "已确认并允许后续自动执行,任务会继续处理。";
|
|
251
|
+
}
|
|
252
|
+
if (decision === "deny") {
|
|
253
|
+
return "已拒绝本次执行。";
|
|
254
|
+
}
|
|
255
|
+
return "已确认本次执行,任务会继续处理。";
|
|
256
|
+
}
|
|
257
|
+
function buildApprovalFailureMessage(reason) {
|
|
258
|
+
if (/unknown or expired approval id|approval expired or not found/i.test(reason)) {
|
|
259
|
+
return "确认失败:当前审批已过期或不存在,请重新发起任务。";
|
|
260
|
+
}
|
|
261
|
+
return `确认失败:${reason}`;
|
|
262
|
+
}
|
|
263
|
+
function rememberApproval(sessionId, parsed) {
|
|
264
|
+
const now = Date.now();
|
|
265
|
+
const expiresAtMs = now + (parsed.expiresInMs ?? DEFAULT_APPROVAL_TTL_MS);
|
|
266
|
+
pendingApprovals.set(sessionId, {
|
|
267
|
+
...parsed,
|
|
268
|
+
sessionId,
|
|
269
|
+
createdAtMs: now,
|
|
270
|
+
expiresAtMs,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
function resolveApprovalDecision(record, text) {
|
|
274
|
+
const normalized = normalizeReplyText(text);
|
|
275
|
+
if (!normalized) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
if (record.allowedDecisions.includes("allow-always") && ALLOW_ALWAYS_KEYWORDS.has(normalized)) {
|
|
279
|
+
return "allow-always";
|
|
280
|
+
}
|
|
281
|
+
if (record.allowedDecisions.includes("deny") && DENY_KEYWORDS.has(normalized)) {
|
|
282
|
+
return "deny";
|
|
283
|
+
}
|
|
284
|
+
if (record.allowedDecisions.includes("allow-once") && ALLOW_ONCE_KEYWORDS.has(normalized)) {
|
|
285
|
+
return "allow-once";
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* 出站改写:
|
|
291
|
+
* - 如果是审批提示,则记录待审批状态,并输出更友好的确认文案。
|
|
292
|
+
* - 如果是 /approve 的成功或失败回执,则改写成用户更容易理解的文本。
|
|
293
|
+
*/
|
|
294
|
+
export function rewriteOutboundApprovalText(sessionId, text) {
|
|
295
|
+
if (!ENABLE_APPROVAL_BRIDGE) {
|
|
296
|
+
return text;
|
|
297
|
+
}
|
|
298
|
+
cleanupExpiredApprovals();
|
|
299
|
+
if (!sessionId || typeof text !== "string" || text.trim() === "") {
|
|
300
|
+
return text;
|
|
301
|
+
}
|
|
302
|
+
const acknowledgement = parseApprovalSubmissionAcknowledgement(text);
|
|
303
|
+
if (acknowledgement) {
|
|
304
|
+
pendingApprovals.delete(sessionId);
|
|
305
|
+
return buildApprovalAcknowledgement(acknowledgement.decision);
|
|
306
|
+
}
|
|
307
|
+
const failureReason = parseApprovalSubmissionFailure(text);
|
|
308
|
+
if (failureReason) {
|
|
309
|
+
if (/unknown or expired approval id|approval expired or not found/i.test(failureReason)) {
|
|
310
|
+
pendingApprovals.delete(sessionId);
|
|
311
|
+
}
|
|
312
|
+
return buildApprovalFailureMessage(failureReason);
|
|
313
|
+
}
|
|
314
|
+
const parsed = parseApprovalPrompt(text);
|
|
315
|
+
if (!parsed) {
|
|
316
|
+
return text;
|
|
317
|
+
}
|
|
318
|
+
rememberApproval(sessionId, parsed);
|
|
319
|
+
return buildApprovalPrompt({
|
|
320
|
+
...parsed,
|
|
321
|
+
sessionId,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* 入站改写:
|
|
326
|
+
* - 先检查当前 session 是否存在有效待审批状态。
|
|
327
|
+
* - 只有存在待审批状态时,才把“确认 / 拒绝”等短回复翻译成 /approve。
|
|
328
|
+
* - 没有待审批状态时,完全不介入,避免影响普通对话。
|
|
329
|
+
*/
|
|
330
|
+
export function rewriteInboundApprovalText(sessionId, text) {
|
|
331
|
+
if (!ENABLE_APPROVAL_BRIDGE) {
|
|
332
|
+
return { matched: false, rewrittenText: text };
|
|
333
|
+
}
|
|
334
|
+
cleanupExpiredApprovals();
|
|
335
|
+
if (!sessionId || typeof text !== "string" || text.trim() === "") {
|
|
336
|
+
return { matched: false, rewrittenText: text };
|
|
337
|
+
}
|
|
338
|
+
const record = pendingApprovals.get(sessionId);
|
|
339
|
+
if (!record) {
|
|
340
|
+
return { matched: false, rewrittenText: text };
|
|
341
|
+
}
|
|
342
|
+
const decision = resolveApprovalDecision(record, text);
|
|
343
|
+
if (!decision) {
|
|
344
|
+
return { matched: false, rewrittenText: text };
|
|
345
|
+
}
|
|
346
|
+
pendingApprovals.delete(sessionId);
|
|
347
|
+
return {
|
|
348
|
+
matched: true,
|
|
349
|
+
decision,
|
|
350
|
+
approvalId: record.approvalId,
|
|
351
|
+
rewrittenText: `/approve ${record.approvalId} ${decision}`,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* 清理指定 session 的待审批状态。
|
|
356
|
+
* 用于 clearContext / cancel 等显式结束会话的场景。
|
|
357
|
+
*/
|
|
358
|
+
export function clearPendingApproval(sessionId) {
|
|
359
|
+
if (!ENABLE_APPROVAL_BRIDGE) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (!sessionId) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
pendingApprovals.delete(sessionId);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* 仅用于本地调试或测试。
|
|
369
|
+
*/
|
|
370
|
+
export function getPendingApproval(sessionId) {
|
|
371
|
+
if (!ENABLE_APPROVAL_BRIDGE) {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
cleanupExpiredApprovals();
|
|
375
|
+
return pendingApprovals.get(sessionId) ?? null;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* 仅用于本地调试或测试。
|
|
379
|
+
*/
|
|
380
|
+
export function clearAllPendingApprovals() {
|
|
381
|
+
pendingApprovals.clear();
|
|
382
|
+
}
|
package/dist/src/bot.js
CHANGED
|
@@ -270,7 +270,7 @@ export async function handleXYMessage(params) {
|
|
|
270
270
|
SenderId: parsed.sessionId,
|
|
271
271
|
Provider: "xiaoyi-channel",
|
|
272
272
|
Surface: "xiaoyi-channel",
|
|
273
|
-
MessageSid:
|
|
273
|
+
MessageSid: `xiaoyi_${parsed.taskId}_${deviceType}`,
|
|
274
274
|
Timestamp: Date.now(),
|
|
275
275
|
WasMentioned: false,
|
|
276
276
|
CommandAuthorized: true,
|
|
@@ -532,7 +532,7 @@ async function dispatchSteerWhenReady(params) {
|
|
|
532
532
|
SenderId: sessionId,
|
|
533
533
|
Provider: "xiaoyi-channel",
|
|
534
534
|
Surface: "xiaoyi-channel",
|
|
535
|
-
MessageSid:
|
|
535
|
+
MessageSid: `xiaoyi_${params.parsed.taskId}_${params.deviceType}`,
|
|
536
536
|
Timestamp: Date.now(),
|
|
537
537
|
WasMentioned: false,
|
|
538
538
|
CommandAuthorized: true,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { XYChannelConfig, A2ACommand } from "./types.js";
|
|
2
|
+
export interface SendCommandViaPushParams {
|
|
3
|
+
config: XYChannelConfig;
|
|
4
|
+
command: A2ACommand;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Send a tool command through the push channel (for cron-triggered tool calls).
|
|
8
|
+
*
|
|
9
|
+
* Flow:
|
|
10
|
+
* 1. Push notification is sent with command embedded in data.directives
|
|
11
|
+
* 2. Device receives push → extracts directives → executes command
|
|
12
|
+
* 3. Device returns result via WebSocket (data-event / gui-agent-response / …)
|
|
13
|
+
* 4. The calling tool listens on the WebSocket manager as usual
|
|
14
|
+
*/
|
|
15
|
+
export declare function sendCommandViaPush(params: SendCommandViaPushParams): Promise<void>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Cron-triggered tool command delivery via push channel.
|
|
2
|
+
// When a cron/scheduled task executes a tool, there is no active WebSocket
|
|
3
|
+
// session to carry the command. Instead, the command is delivered through
|
|
4
|
+
// the push notification channel (agent-webhook), which reaches the device
|
|
5
|
+
// independently of any session. The device processes the command and returns
|
|
6
|
+
// results through the normal WebSocket connection, so response listening
|
|
7
|
+
// works the same as for regular tool calls.
|
|
8
|
+
import { randomUUID } from "crypto";
|
|
9
|
+
import { XYPushService } from "./push.js";
|
|
10
|
+
import { getAllPushIds } from "./utils/pushid-manager.js";
|
|
11
|
+
import { logger } from "./utils/logger.js";
|
|
12
|
+
/**
|
|
13
|
+
* Send a tool command through the push channel (for cron-triggered tool calls).
|
|
14
|
+
*
|
|
15
|
+
* Flow:
|
|
16
|
+
* 1. Push notification is sent with command embedded in data.directives
|
|
17
|
+
* 2. Device receives push → extracts directives → executes command
|
|
18
|
+
* 3. Device returns result via WebSocket (data-event / gui-agent-response / …)
|
|
19
|
+
* 4. The calling tool listens on the WebSocket manager as usual
|
|
20
|
+
*/
|
|
21
|
+
export async function sendCommandViaPush(params) {
|
|
22
|
+
const { config, command } = params;
|
|
23
|
+
const intentName = command.payload?.executeParam?.intentName ??
|
|
24
|
+
command.header?.name ??
|
|
25
|
+
"Command";
|
|
26
|
+
logger.log(`[CRON-CMD] Sending command via push, intent=${intentName}`);
|
|
27
|
+
// 1. Load push IDs, use first one
|
|
28
|
+
let pushId = config.pushId;
|
|
29
|
+
try {
|
|
30
|
+
const pushIdList = await getAllPushIds();
|
|
31
|
+
if (pushIdList.length > 0) {
|
|
32
|
+
pushId = pushIdList[0];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
logger.error("[CRON-CMD] Failed to load pushIds:", error);
|
|
37
|
+
}
|
|
38
|
+
// 2. Build and send push notification with command in directives
|
|
39
|
+
const pushService = new XYPushService(config);
|
|
40
|
+
const sessionId = randomUUID();
|
|
41
|
+
try {
|
|
42
|
+
await pushService.sendPushWithDirectives(pushId, sessionId, [command]);
|
|
43
|
+
logger.log(`[CRON-CMD] Push sent successfully, intent=${intentName}`);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
logger.error(`[CRON-CMD] Failed to send push`, error);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type CronQueryAction = "list" | "status" | "runs" | "add" | "update" | "remove" | "run";
|
|
2
|
+
export interface CronQueryEventContext {
|
|
3
|
+
action: CronQueryAction;
|
|
4
|
+
jobId?: string;
|
|
5
|
+
params?: Record<string, unknown>;
|
|
6
|
+
/** Original A2A message fields for routing the response. */
|
|
7
|
+
sessionId?: string;
|
|
8
|
+
taskId?: string;
|
|
9
|
+
messageId?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Handle a cron-query-event.
|
|
13
|
+
*
|
|
14
|
+
* Calls the Gateway cron RPC and sends the result back through sendCommand
|
|
15
|
+
* as a System.CronQuery command with the full result object in payload.ans.
|
|
16
|
+
*/
|
|
17
|
+
export declare function handleCronQueryEvent(context: CronQueryEventContext, cfg?: unknown): Promise<void>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Cron query event handler.
|
|
2
|
+
// Listens for cron-query-event from the WebSocket manager,
|
|
3
|
+
// calls Gateway cron RPC via callGatewayTool, and sends the
|
|
4
|
+
// result back to the client via sendCommand as a System.CronQuery
|
|
5
|
+
// command with the result in payload.ans.
|
|
6
|
+
import { callGatewayTool } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
7
|
+
import { sendCommand } from "./formatter.js";
|
|
8
|
+
import { resolveXYConfig } from "./config.js";
|
|
9
|
+
import { logger } from "./utils/logger.js";
|
|
10
|
+
const GATEWAY_TIMEOUT_MS = 60_000;
|
|
11
|
+
/**
|
|
12
|
+
* Handle a cron-query-event.
|
|
13
|
+
*
|
|
14
|
+
* Calls the Gateway cron RPC and sends the result back through sendCommand
|
|
15
|
+
* as a System.CronQuery command with the full result object in payload.ans.
|
|
16
|
+
*/
|
|
17
|
+
export async function handleCronQueryEvent(context, cfg) {
|
|
18
|
+
const { action, jobId, params, sessionId, taskId, messageId } = context;
|
|
19
|
+
logger.log(`[CRON-QUERY] Received event: action=${action}, jobId=${jobId ?? "(none)"}`);
|
|
20
|
+
let result;
|
|
21
|
+
let error;
|
|
22
|
+
try {
|
|
23
|
+
switch (action) {
|
|
24
|
+
case "list":
|
|
25
|
+
result = await callGatewayTool("cron.list", { timeoutMs: GATEWAY_TIMEOUT_MS }, params ?? {});
|
|
26
|
+
break;
|
|
27
|
+
case "status":
|
|
28
|
+
result = await callGatewayTool("cron.status", { timeoutMs: GATEWAY_TIMEOUT_MS }, {});
|
|
29
|
+
break;
|
|
30
|
+
case "runs":
|
|
31
|
+
result = await callGatewayTool("cron.runs", { timeoutMs: GATEWAY_TIMEOUT_MS }, {
|
|
32
|
+
jobId,
|
|
33
|
+
...params,
|
|
34
|
+
});
|
|
35
|
+
break;
|
|
36
|
+
case "add":
|
|
37
|
+
result = await callGatewayTool("cron.add", { timeoutMs: GATEWAY_TIMEOUT_MS }, params ?? {});
|
|
38
|
+
break;
|
|
39
|
+
case "update":
|
|
40
|
+
result = await callGatewayTool("cron.update", { timeoutMs: GATEWAY_TIMEOUT_MS }, {
|
|
41
|
+
jobId,
|
|
42
|
+
...params,
|
|
43
|
+
});
|
|
44
|
+
break;
|
|
45
|
+
case "remove":
|
|
46
|
+
result = await callGatewayTool("cron.remove", { timeoutMs: GATEWAY_TIMEOUT_MS }, {
|
|
47
|
+
jobId,
|
|
48
|
+
});
|
|
49
|
+
break;
|
|
50
|
+
case "run":
|
|
51
|
+
result = await callGatewayTool("cron.run", { timeoutMs: GATEWAY_TIMEOUT_MS }, {
|
|
52
|
+
jobId,
|
|
53
|
+
mode: "force",
|
|
54
|
+
...params,
|
|
55
|
+
});
|
|
56
|
+
break;
|
|
57
|
+
default:
|
|
58
|
+
error = `Unknown action: ${context.action}`;
|
|
59
|
+
logger.error(`[CRON-QUERY] ${error}`);
|
|
60
|
+
result = { error };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
error = err instanceof Error ? err.message : String(err);
|
|
65
|
+
logger.error(`[CRON-QUERY] RPC call failed for action=${action}:`, err);
|
|
66
|
+
result = { error };
|
|
67
|
+
}
|
|
68
|
+
// Log the result
|
|
69
|
+
logger.log(`[CRON-QUERY] RPC result for action=${action}: ${JSON.stringify(result, null, 2)}`);
|
|
70
|
+
// Send result back via sendCommand as System.CronQuery with payload.ans
|
|
71
|
+
if (cfg && sessionId && taskId && messageId) {
|
|
72
|
+
try {
|
|
73
|
+
const config = resolveXYConfig(cfg);
|
|
74
|
+
const command = {
|
|
75
|
+
header: {
|
|
76
|
+
namespace: "AgentEvent",
|
|
77
|
+
name: "CronQuery",
|
|
78
|
+
},
|
|
79
|
+
payload: {
|
|
80
|
+
action,
|
|
81
|
+
ans: result,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
await sendCommand({
|
|
85
|
+
config,
|
|
86
|
+
sessionId,
|
|
87
|
+
taskId,
|
|
88
|
+
messageId,
|
|
89
|
+
command,
|
|
90
|
+
final: true,
|
|
91
|
+
});
|
|
92
|
+
logger.log(`[CRON-QUERY] Sent response via sendCommand, action=${action}`);
|
|
93
|
+
}
|
|
94
|
+
catch (sendErr) {
|
|
95
|
+
logger.error(`[CRON-QUERY] Failed to send response via sendCommand:`, sendErr);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
logger.warn(`[CRON-QUERY] Missing cfg/sessionId/taskId/messageId, skipping sendCommand`);
|
|
100
|
+
}
|
|
101
|
+
}
|