@ynhcj/xiaoyi-channel 0.0.217-next → 0.0.219-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 +52 -0
- package/dist/src/bot.js +71 -38
- package/dist/src/channel.js +2 -0
- package/dist/src/cspl/call_api.d.ts +7 -1
- package/dist/src/cspl/call_api.js +56 -16
- package/dist/src/cspl/config.d.ts +0 -1
- package/dist/src/cspl/config.js +65 -59
- package/dist/src/cspl/constants.d.ts +36 -1
- package/dist/src/cspl/constants.js +9 -0
- package/dist/src/cspl/sentinel_hook.js +32 -27
- package/dist/src/cspl/skill_scope_hook.d.ts +17 -0
- package/dist/src/cspl/skill_scope_hook.js +125 -0
- package/dist/src/cspl/upload_file.js +8 -11
- package/dist/src/cspl/utils.d.ts +26 -5
- package/dist/src/cspl/utils.js +224 -74
- package/dist/src/file-download.d.ts +1 -0
- package/dist/src/file-download.js +1 -0
- package/dist/src/formatter.d.ts +36 -0
- package/dist/src/formatter.js +74 -0
- package/dist/src/log-reporter/index.js +7 -3
- package/dist/src/outbound.d.ts +7 -0
- package/dist/src/outbound.js +62 -6
- package/dist/src/provider.js +2 -1
- package/dist/src/reply-dispatcher.d.ts +2 -0
- package/dist/src/reply-dispatcher.js +130 -48
- package/dist/src/skill-retriever/hooks.d.ts +17 -0
- package/dist/src/skill-retriever/hooks.js +113 -2
- package/dist/src/subagent-wait-state.d.ts +7 -0
- package/dist/src/subagent-wait-state.js +14 -0
- package/dist/src/task-manager.d.ts +9 -3
- package/dist/src/task-manager.js +48 -16
- package/dist/src/tools/hmos-cli.d.ts +1 -1
- package/dist/src/tools/hmos-cli.js +4 -4
- package/dist/src/tools/send-email-tool.js +1 -0
- package/dist/src/tools/xiaoyi-append-reference.d.ts +47 -0
- package/dist/src/tools/xiaoyi-append-reference.js +107 -0
- package/dist/src/tools/xiaoyi-gui-tool.js +10 -1
- package/dist/src/websocket.js +10 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { xiaoyiProvider } from "./src/provider.js";
|
|
|
3
3
|
import { xiaoyiCompactionProvider } from "./src/compaction-provider.js";
|
|
4
4
|
import { xyPlugin } from "./src/channel.js";
|
|
5
5
|
import registerSentinelHook from "./src/cspl/sentinel_hook.js";
|
|
6
|
+
import registerSkillScopeHook from "./src/cspl/skill_scope_hook.js";
|
|
6
7
|
import { setXYRuntime } from "./src/runtime.js";
|
|
7
8
|
import { markCronToolCall, clearCronToolCall, getCurrentSessionContext } from "./src/tools/session-manager.js";
|
|
8
9
|
import { configManager } from "./src/utils/config-manager.js";
|
|
@@ -14,6 +15,7 @@ import { normalizeToolRetrieverConfig } from "./src/skill-retriever/config.js";
|
|
|
14
15
|
import { registerCLIHook } from "./src/tools/hmos-cli.js";
|
|
15
16
|
import { recoverCronState } from "./src/cron-recovery.js";
|
|
16
17
|
import { writeSkillUsage } from "./src/utils/skills-logger.js";
|
|
18
|
+
import { markSubagentSpawned, markSubagentEnded, getCachedXYConfig, } from "./src/subagent-wait-state.js";
|
|
17
19
|
import { logger } from "./src/utils/logger.js";
|
|
18
20
|
/**
|
|
19
21
|
* Parse a file path string to detect if it refers to a SKILL.md file within
|
|
@@ -274,7 +276,55 @@ function registerCronRecoveryHook(api) {
|
|
|
274
276
|
logger.log(`${logTag} ═══════════════════════════════════════════`);
|
|
275
277
|
});
|
|
276
278
|
}
|
|
279
|
+
function registerSubagentHooks(api) {
|
|
280
|
+
// subagent_spawned: fires after a subagent run is successfully registered.
|
|
281
|
+
// We increment the expected completion count so that onIdle knows to wait.
|
|
282
|
+
api.on("subagent_spawned", async (_event, ctx) => {
|
|
283
|
+
const requesterSessionKey = ctx?.requesterSessionKey;
|
|
284
|
+
if (!requesterSessionKey)
|
|
285
|
+
return;
|
|
286
|
+
const count = markSubagentSpawned(requesterSessionKey);
|
|
287
|
+
if (count > 0) {
|
|
288
|
+
logger.log(`[XY-SUBAGENT] spawned, requesterSessionKey=${requesterSessionKey.slice(0, 30)}, expected=${count}`);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
// subagent_ended: fires when a subagent run terminates (complete/error/killed).
|
|
292
|
+
// This is the PRIMARY delivery tracking mechanism. When all expected
|
|
293
|
+
// subagents have ended and parent has settled, we finalize the A2A session.
|
|
294
|
+
api.on("subagent_ended", async (event, ctx) => {
|
|
295
|
+
try {
|
|
296
|
+
const requesterSessionKey = ctx?.requesterSessionKey;
|
|
297
|
+
if (!requesterSessionKey) {
|
|
298
|
+
logger.log(`[XY-SUBAGENT-END] no requesterSessionKey in ctx`);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const transition = markSubagentEnded(requesterSessionKey);
|
|
302
|
+
logger.log(`[XY-SUBAGENT-END] ended, targetSessionKey=${event?.targetSessionKey?.slice(0, 30)}, outcome=${event?.outcome}, complete=${transition?.isComplete ?? false}, shouldFinalize=${transition?.shouldFinalize ?? false}, transition=${!!transition}`);
|
|
303
|
+
if (transition?.shouldFinalize) {
|
|
304
|
+
logger.log(`[XY-SUBAGENT-END] Starting finalization...`);
|
|
305
|
+
const config = getCachedXYConfig();
|
|
306
|
+
if (!config) {
|
|
307
|
+
logger.error(`[XY-SUBAGENT-END] No cached XY config, cannot deliver final result`);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const { deliverSubagentFinalResult } = await import("./src/outbound.js");
|
|
311
|
+
logger.log(`[XY-SUBAGENT-END] Using cached XY config`);
|
|
312
|
+
await deliverSubagentFinalResult({
|
|
313
|
+
config: config,
|
|
314
|
+
state: transition.state,
|
|
315
|
+
reason: "all-subagents-ended-after-parent-settled",
|
|
316
|
+
});
|
|
317
|
+
logger.log(`[XY-SUBAGENT-END] Finalized A2A session after all subagents ended`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
logger.error(`[XY-SUBAGENT-END] Error in subagent_ended hook:`, err);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
}
|
|
277
325
|
function registerFullHooks(api) {
|
|
326
|
+
// SUBAGENT HOOKS: track subagent spawn/end lifecycle for session keep-alive
|
|
327
|
+
registerSubagentHooks(api);
|
|
278
328
|
// SKILL RETRIEVER HOOK: before_prompt_build hook
|
|
279
329
|
const pluginConfig = api.pluginConfig || {};
|
|
280
330
|
const skillRetrieverConfig = normalizeToolRetrieverConfig({
|
|
@@ -320,6 +370,8 @@ const plugin = definePluginEntry({
|
|
|
320
370
|
registerFullHooks(api);
|
|
321
371
|
// CSPL sentinel hook: before_tool_call + after_tool_call security scanning
|
|
322
372
|
registerSentinelHook(api);
|
|
373
|
+
// CSPL skill scope hook: before_install security scanning
|
|
374
|
+
registerSkillScopeHook(api);
|
|
323
375
|
// Cron detection hook: marks toolCallIds from cron sessions
|
|
324
376
|
registerCronDetectionHook(api);
|
|
325
377
|
// CLI exec hook: intercepts built-in exec for HarmonyOS CLI skill tools
|
package/dist/src/bot.js
CHANGED
|
@@ -17,6 +17,7 @@ import { toolCallNudgeManager } from "./utils/tool-call-nudge-manager.js";
|
|
|
17
17
|
import { setCsplSteerContext } from "./cspl/steer-context.js";
|
|
18
18
|
import { resolveActiveEmbeddedRunSessionId, queueAgentHarnessMessage, } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
19
19
|
import { registerTaskId, decrementTaskIdRef, hasActiveTask, } from "./task-manager.js";
|
|
20
|
+
import { registerSessionKeyMapping, getWaitState, hasWaitState, markParentSettled, cacheXYConfig, } from "./subagent-wait-state.js";
|
|
20
21
|
import { logger } from "./utils/logger.js";
|
|
21
22
|
/**
|
|
22
23
|
* Handle an incoming A2A message.
|
|
@@ -165,6 +166,7 @@ export async function handleXYMessage(params) {
|
|
|
165
166
|
const runCrossTaskContext = extractRunCrossTaskContext(parsed.parts);
|
|
166
167
|
// Resolve configuration (needed for status updates)
|
|
167
168
|
const config = resolveXYConfig(cfg);
|
|
169
|
+
cacheXYConfig(config);
|
|
168
170
|
// ✅ Resolve agent route (following feishu pattern)
|
|
169
171
|
// accountId is "default" for XY (single account mode)
|
|
170
172
|
// Use sessionId as peer.id to ensure all messages in the same session share context
|
|
@@ -178,6 +180,16 @@ export async function handleXYMessage(params) {
|
|
|
178
180
|
},
|
|
179
181
|
});
|
|
180
182
|
log.log(`[BOT] Resolved route, sessionKey=${route.sessionKey}`);
|
|
183
|
+
// Register sessionKey→A2A sessionId mapping for subagent hook translation.
|
|
184
|
+
// Hooks receive openclaw sessionKey (e.g. "agent:main:direct:xxx") but
|
|
185
|
+
// xy_channel operates on A2A sessionId. This bridge lets subagent_spawned/
|
|
186
|
+
// subagent_ended hooks find the correct wait state.
|
|
187
|
+
// IMPORTANT: Only register on the FIRST message. Steer messages have a
|
|
188
|
+
// different taskId — overwriting the mapping would break subagent wait
|
|
189
|
+
// state tracking for the original message's subagents.
|
|
190
|
+
if (!isUpdate) {
|
|
191
|
+
registerSessionKeyMapping(route.sessionKey, parsed.sessionId, parsed.taskId, parsed.messageId);
|
|
192
|
+
}
|
|
181
193
|
// Check for ACP runtime binding on this A2A conversation
|
|
182
194
|
const runtimeRoute = resolveRuntimeConversationBindingRoute({
|
|
183
195
|
route,
|
|
@@ -242,20 +254,18 @@ export async function handleXYMessage(params) {
|
|
|
242
254
|
log.error(`[BOT] Failed to patch session model override:`, patchErr);
|
|
243
255
|
}
|
|
244
256
|
}
|
|
245
|
-
// 🔑
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
});
|
|
258
|
-
}
|
|
257
|
+
// 🔑 发送初始状态更新
|
|
258
|
+
log.log(`[BOT] Sending initial status update`);
|
|
259
|
+
void sendStatusUpdate({
|
|
260
|
+
config,
|
|
261
|
+
sessionId: parsed.sessionId,
|
|
262
|
+
taskId: parsed.taskId,
|
|
263
|
+
messageId: parsed.messageId,
|
|
264
|
+
text: "任务正在处理中,请稍候~",
|
|
265
|
+
state: "working",
|
|
266
|
+
}).catch((err) => {
|
|
267
|
+
log.error(`Failed to send initial status update:`, err);
|
|
268
|
+
});
|
|
259
269
|
}
|
|
260
270
|
// Extract text and files from parts
|
|
261
271
|
const text = extractTextFromParts(parsed.parts);
|
|
@@ -281,10 +291,8 @@ export async function handleXYMessage(params) {
|
|
|
281
291
|
}
|
|
282
292
|
}
|
|
283
293
|
// ── Build message and dispatch via auto-reply pipeline ──────────
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
// does not need its own steer detection or polling logic.
|
|
287
|
-
// File download
|
|
294
|
+
// 首条消息和 steer 回退都走此统一 dispatch 路径。
|
|
295
|
+
// File download — for both first message and steer fallthrough
|
|
288
296
|
let mediaPayload = {};
|
|
289
297
|
if (!skipReg || isUpdate) {
|
|
290
298
|
const fileParts = extractFileParts(parsed.parts);
|
|
@@ -327,10 +335,8 @@ export async function handleXYMessage(params) {
|
|
|
327
335
|
// Resolve envelope format options (following feishu pattern)
|
|
328
336
|
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
329
337
|
// Build message body with speaker prefix (following feishu pattern)
|
|
330
|
-
let messageBody = textForAgent;
|
|
331
|
-
// Add speaker prefix for clarity
|
|
332
338
|
const speaker = parsed.sessionId;
|
|
333
|
-
messageBody = `${speaker}: ${
|
|
339
|
+
const messageBody = `${speaker}: ${textForAgent}`;
|
|
334
340
|
// Format agent envelope (following feishu pattern)
|
|
335
341
|
const body = core.channel.reply.formatAgentEnvelope({
|
|
336
342
|
channel: "xiaoyi-channel",
|
|
@@ -378,20 +384,30 @@ export async function handleXYMessage(params) {
|
|
|
378
384
|
// 🔑 创建dispatcher
|
|
379
385
|
log.log(`[BOT-DISPATCHER] Creating reply dispatcher, isSteer=${isUpdate}, sessionKey=${route.sessionKey}`);
|
|
380
386
|
// Cleanup: 必须在 onIdle 内部执行(参见 reply-dispatcher.ts 中 onIdleComplete 的注释)
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
//
|
|
387
|
+
// CSPL steer injections (skipRegistration=true) do NOT call registerTaskId,
|
|
388
|
+
// so they must skip decrementTaskIdRef. All other paths (original message +
|
|
389
|
+
// WebSocket steer that fell through to the /steer fast path) DO increment
|
|
390
|
+
// refCount and must decrement here.
|
|
384
391
|
let cleaned = false;
|
|
385
392
|
const cleanup = () => {
|
|
386
393
|
if (cleaned)
|
|
387
394
|
return;
|
|
388
395
|
cleaned = true;
|
|
389
|
-
log.log(`[BOT] Cleanup started, steered=${steerState.steered}`);
|
|
390
|
-
if (!
|
|
396
|
+
log.log(`[BOT] Cleanup started, steered=${steerState.steered}, skipReg=${skipReg}`);
|
|
397
|
+
if (!skipReg) {
|
|
398
|
+
// Check for pending subagent wait on this session
|
|
399
|
+
const pendingWait = getWaitState(parsed.sessionId, parsed.taskId) ??
|
|
400
|
+
(hasWaitState(parsed.sessionId) ? { deliveredCompletions: 0, expectedCompletions: 1 } : null);
|
|
401
|
+
if (pendingWait && pendingWait.deliveredCompletions < pendingWait.expectedCompletions) {
|
|
402
|
+
// Subagent wait active — skip cleanup, session stays alive
|
|
403
|
+
log.log(`[BOT] Cleanup suppressed — subagent wait active on session, taskId=${parsed.taskId}`);
|
|
404
|
+
cleaned = false;
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
391
407
|
decrementTaskIdRef(parsed.sessionId);
|
|
392
408
|
}
|
|
393
409
|
else {
|
|
394
|
-
log.log(`[BOT]
|
|
410
|
+
log.log(`[BOT] CSPL steer cleanup, skipping decrementTaskIdRef (never incremented)`);
|
|
395
411
|
}
|
|
396
412
|
log.log(`[BOT] Cleanup completed`);
|
|
397
413
|
};
|
|
@@ -429,13 +445,34 @@ export async function handleXYMessage(params) {
|
|
|
429
445
|
log.log(`[BOT-DISPATCH] withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
|
|
430
446
|
await core.channel.reply.withReplyDispatcher({
|
|
431
447
|
dispatcher,
|
|
432
|
-
onSettled: () => {
|
|
448
|
+
onSettled: async () => {
|
|
433
449
|
log.log(`[BOT] onSettled, steered=${steerState.steered}`);
|
|
434
450
|
// 🔑 When steered, skip cleanup — the first message's dispatcher is still running
|
|
435
451
|
if (steerState.steered) {
|
|
436
452
|
log.log(`[BOT] Steered dispatch settled, skipping cleanup`);
|
|
437
453
|
return;
|
|
438
454
|
}
|
|
455
|
+
// Subagent wait state: if parent has pending subagents, mark settled
|
|
456
|
+
// and defer finalization. Cleanup is suppressed so the session stays alive.
|
|
457
|
+
const pendingWait = getWaitState(parsed.sessionId, parsed.taskId);
|
|
458
|
+
if (pendingWait && !pendingWait.parentSettled) {
|
|
459
|
+
const transition = markParentSettled(parsed.sessionId, parsed.taskId);
|
|
460
|
+
if (transition?.shouldFinalize) {
|
|
461
|
+
// All completions arrived before parent settled → finalize now
|
|
462
|
+
const { deliverSubagentFinalResult } = await import("./outbound.js");
|
|
463
|
+
await deliverSubagentFinalResult({
|
|
464
|
+
config,
|
|
465
|
+
state: transition.state,
|
|
466
|
+
reason: "all-subagent-results-delivered-before-parent-settled",
|
|
467
|
+
});
|
|
468
|
+
log.log(`[BOT] Subagent wait complete on parent settled; final response delivered`);
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
log.log(`[BOT] Subagent wait active, preserving session context for completion`);
|
|
472
|
+
}
|
|
473
|
+
dispatcherUpdaters.delete(parsed.sessionId);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
439
476
|
// cleanup 已由 onIdleComplete 在 onIdle 的 finally 中执行。
|
|
440
477
|
// onSettled 不做任何清理(直接在这里清理会发生 race condition)。
|
|
441
478
|
dispatcherUpdaters.delete(parsed.sessionId);
|
|
@@ -449,7 +486,7 @@ export async function handleXYMessage(params) {
|
|
|
449
486
|
const dispatchPromise = runWithSessionContext(sessionContext, async () => {
|
|
450
487
|
const isSteerDispatch = isUpdate && !skipReg;
|
|
451
488
|
log.log(`[ALS-PROOF] bot entered dispatch scope sessionId=${sessionContext.sessionId} taskId=${sessionContext.taskId} isSteer=${isSteerDispatch}`);
|
|
452
|
-
log.log(`[BOT-DISPATCH] dispatchReplyFromConfig starting, body.length=${ctxPayload.Body?.length ?? 0}
|
|
489
|
+
log.log(`[BOT-DISPATCH] dispatchReplyFromConfig starting, body.length=${ctxPayload.Body?.length ?? 0}`);
|
|
453
490
|
try {
|
|
454
491
|
const result = await core.channel.reply.dispatchReplyFromConfig({
|
|
455
492
|
ctx: ctxPayload,
|
|
@@ -457,13 +494,7 @@ export async function handleXYMessage(params) {
|
|
|
457
494
|
dispatcher,
|
|
458
495
|
replyOptions,
|
|
459
496
|
});
|
|
460
|
-
|
|
461
|
-
if (result === undefined) {
|
|
462
|
-
log.log(`[BOT-STEER] dispatchReplyFromConfig returned undefined — steer injected via fast path OK, sessionKey=${route.sessionKey}`);
|
|
463
|
-
}
|
|
464
|
-
else {
|
|
465
|
-
log.log(`[BOT-DISPATCH] dispatchReplyFromConfig returned, resultType=${typeof result}, isSteer=${isSteerDispatch}, steered=${steerState.steered}`);
|
|
466
|
-
}
|
|
497
|
+
log.log(`[BOT-DISPATCH] dispatchReplyFromConfig returned, result=${JSON.stringify(result)}`);
|
|
467
498
|
return result;
|
|
468
499
|
}
|
|
469
500
|
catch (dispatchErr) {
|
|
@@ -514,19 +545,21 @@ export async function handleXYMessage(params) {
|
|
|
514
545
|
function buildXYMediaPayload(mediaList) {
|
|
515
546
|
const first = mediaList[0];
|
|
516
547
|
const mediaPaths = mediaList.map((media) => media.path);
|
|
548
|
+
const mediaUrls = mediaList.map((media) => media.uri);
|
|
517
549
|
const mediaTypes = mediaList.map((media) => media.mimeType).filter(Boolean);
|
|
518
550
|
return {
|
|
519
551
|
MediaPath: first?.path,
|
|
520
552
|
MediaType: first?.mimeType,
|
|
553
|
+
MediaUrl: first?.uri,
|
|
521
554
|
MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
|
|
555
|
+
MediaUrls: mediaUrls.length > 0 ? mediaUrls : undefined,
|
|
522
556
|
MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
|
|
523
557
|
};
|
|
524
558
|
}
|
|
525
559
|
// ─────────────────────────────────────────────────────────────
|
|
526
560
|
// Dispatcher updaters (cross-chain taskId bridging)
|
|
527
561
|
// ─────────────────────────────────────────────────────────────
|
|
528
|
-
// Use globalThis to survive module deduplication
|
|
529
|
-
// different copy of bot.ts, so a plain module-level Map would be two objects.
|
|
562
|
+
// Use globalThis to survive module deduplication
|
|
530
563
|
const _g = globalThis;
|
|
531
564
|
if (!_g.__xyDispatcherUpdaters)
|
|
532
565
|
_g.__xyDispatcherUpdaters = new Map();
|
package/dist/src/channel.js
CHANGED
|
@@ -28,6 +28,7 @@ import { sendCrossDeviceTaskTool } from "./tools/send-cross-device-task-tool.js"
|
|
|
28
28
|
import { displayA2UICardByPathTool } from "./tools/display-a2ui-card-bypath-tool.js";
|
|
29
29
|
import { checkPluginPrivilegeTool } from "./tools/check-plugin-privilege-tool.js";
|
|
30
30
|
import { invokeTool } from "./tools/invoke.js";
|
|
31
|
+
import { xiaoyiAppendReferenceTool } from "./tools/xiaoyi-append-reference.js";
|
|
31
32
|
const ALL_TOOLS = [
|
|
32
33
|
locationTool,
|
|
33
34
|
discoverCrossDevicesTool,
|
|
@@ -52,6 +53,7 @@ const ALL_TOOLS = [
|
|
|
52
53
|
agentAsSkillTool,
|
|
53
54
|
checkPluginPrivilegeTool,
|
|
54
55
|
invokeTool,
|
|
56
|
+
xiaoyiAppendReferenceTool,
|
|
55
57
|
];
|
|
56
58
|
/**
|
|
57
59
|
* Xiaoyi Channel Plugin for OpenClaw.
|
|
@@ -1,2 +1,8 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
|
|
1
2
|
import { ApiResponse } from './constants.js';
|
|
2
|
-
export
|
|
3
|
+
export interface CallApiPayload {
|
|
4
|
+
sceneID: string;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
export declare function callApi(payload: CallApiPayload, api: OpenClawPluginApi, sessionId: string): Promise<ApiResponse>;
|
|
8
|
+
export declare function callSkillScanApi(url_suffix: string, payload: object, api: OpenClawPluginApi, sessionId: string): Promise<ApiResponse>;
|
|
@@ -4,8 +4,13 @@
|
|
|
4
4
|
import https from 'https';
|
|
5
5
|
import http from 'http';
|
|
6
6
|
import { URL } from 'url';
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
7
12
|
import { getConfig } from './config.js';
|
|
8
|
-
import {
|
|
13
|
+
import { DEFAULT_HTTPS_PORT, DEFAULT_HTTP_PORT, HTTP_STATUS_BAD_REQUEST, API_URL_SUFFIX } from './constants.js';
|
|
9
14
|
function buildHeadersForCelia(config, sessionId) {
|
|
10
15
|
if (!config.uid || !config.apiKey || !config.skillId || !config.requestFrom) {
|
|
11
16
|
throw new Error('[SENTINEL HOOK] Missing required configuration: uid, apiKey, skillId, or requestFrom is not defined');
|
|
@@ -21,15 +26,13 @@ function buildHeadersForCelia(config, sessionId) {
|
|
|
21
26
|
}
|
|
22
27
|
function buildRequestOptions(url, headers, timeout) {
|
|
23
28
|
const urlObj = new URL(url);
|
|
24
|
-
const isHttps = urlObj.protocol === 'https:';
|
|
25
29
|
return {
|
|
26
30
|
hostname: urlObj.hostname,
|
|
27
|
-
port: urlObj.port || (
|
|
31
|
+
port: urlObj.port || (urlObj.protocol === 'https:' ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT),
|
|
28
32
|
path: urlObj.pathname,
|
|
29
33
|
method: "POST",
|
|
30
34
|
headers: headers,
|
|
31
|
-
timeout: timeout
|
|
32
|
-
rejectUnauthorized: false
|
|
35
|
+
timeout: timeout
|
|
33
36
|
};
|
|
34
37
|
}
|
|
35
38
|
function checkHttpStatus(res) {
|
|
@@ -81,22 +84,17 @@ function handleResponse(res, resolve, reject) {
|
|
|
81
84
|
}
|
|
82
85
|
});
|
|
83
86
|
}
|
|
84
|
-
export async function callApi(
|
|
87
|
+
export async function callApi(payload, api, sessionId) {
|
|
85
88
|
const config = getConfig(api);
|
|
86
89
|
const headersForCelia = buildHeadersForCelia(config, sessionId);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
action: action,
|
|
91
|
-
extra: `${JSON.stringify({ userId: config.uid })}`
|
|
92
|
-
};
|
|
93
|
-
const httpBody = JSON.stringify(payload);
|
|
90
|
+
// 确保 uid 存在于消息体中(从 config 注入)
|
|
91
|
+
const payloadWithUid = { ...payload, uid: config.uid, action: payload.sceneID, packageName: "com.huawei.hmos.vassistant", ansDone: false, userId: config.uid };
|
|
92
|
+
const httpBody = JSON.stringify(payloadWithUid);
|
|
94
93
|
const apiUrl = `${config.api.url}${API_URL_SUFFIX}`;
|
|
95
94
|
return new Promise((resolve, reject) => {
|
|
96
95
|
const options = buildRequestOptions(apiUrl, headersForCelia, config.api.timeout);
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
const req = transport.request(options, (res) => {
|
|
96
|
+
const protocol = apiUrl.startsWith('https://') ? https : http;
|
|
97
|
+
const req = protocol.request(options, (res) => {
|
|
100
98
|
handleResponse(res, resolve, reject);
|
|
101
99
|
});
|
|
102
100
|
req.on('error', (error) => {
|
|
@@ -110,3 +108,45 @@ export async function callApi(questionText, api, sessionId, action) {
|
|
|
110
108
|
req.end();
|
|
111
109
|
});
|
|
112
110
|
}
|
|
111
|
+
export async function callSkillScanApi(url_suffix, payload, api, sessionId) {
|
|
112
|
+
const config = getConfig(api);
|
|
113
|
+
const headersForCelia = buildHeadersForCelia(config, sessionId);
|
|
114
|
+
// 确保 uid 存在于消息体中(从 config 注入)
|
|
115
|
+
const payloadWithUid = { ...payload, uid: config.uid, packageName: "com.huawei.hmos.vassistant", ansDone: false, userId: config.uid };
|
|
116
|
+
const httpBody = JSON.stringify(payloadWithUid);
|
|
117
|
+
console.log(`TOOL_input headersForCelia: ${JSON.stringify(headersForCelia)}`);
|
|
118
|
+
console.log(`TOOL_input httpBody: ${JSON.stringify(httpBody)}`);
|
|
119
|
+
// 将headersForCelia和httpBody写入当前目录的txt文件
|
|
120
|
+
const logFile = path.join(__dirname, 'api_request_log.txt');
|
|
121
|
+
const logContent = [
|
|
122
|
+
`==============================`,
|
|
123
|
+
`Timestamp: ${new Date().toISOString()}`,
|
|
124
|
+
`Session ID: ${sessionId}`,
|
|
125
|
+
``,
|
|
126
|
+
`--- headersForCelia ---`,
|
|
127
|
+
JSON.stringify(headersForCelia, null, 2),
|
|
128
|
+
``,
|
|
129
|
+
`--- httpBody ---`,
|
|
130
|
+
JSON.stringify(httpBody, null, 2),
|
|
131
|
+
`==============================`
|
|
132
|
+
].join('\n');
|
|
133
|
+
fs.writeFileSync(logFile, logContent, 'utf8');
|
|
134
|
+
api.logger.info(`[ai-security-plugin][skill_scope_hook] Request log written to ${logFile}`);
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
const url = config.api.url + url_suffix;
|
|
137
|
+
const options = buildRequestOptions(url, headersForCelia, config.api.timeout);
|
|
138
|
+
const protocol = url.startsWith('https://') ? https : http;
|
|
139
|
+
const req = protocol.request(options, (res) => {
|
|
140
|
+
handleResponse(res, resolve, reject);
|
|
141
|
+
});
|
|
142
|
+
req.on('error', (error) => {
|
|
143
|
+
reject(error);
|
|
144
|
+
});
|
|
145
|
+
req.on('timeout', () => {
|
|
146
|
+
req.destroy();
|
|
147
|
+
reject(new Error('[ai-security-plugin][skill_scope_hook] Request timeout'));
|
|
148
|
+
});
|
|
149
|
+
req.write(httpBody);
|
|
150
|
+
req.end();
|
|
151
|
+
});
|
|
152
|
+
}
|
package/dist/src/cspl/config.js
CHANGED
|
@@ -9,45 +9,44 @@ const __dirname = path.dirname(__filename);
|
|
|
9
9
|
import { CONFIG_FILE_NAME, ENV_FILE_PATH, REQUIRED_ENV_VARS } from './constants.js';
|
|
10
10
|
import { logger } from '../utils/logger.js';
|
|
11
11
|
let cachedConfig = null;
|
|
12
|
-
function
|
|
13
|
-
if (!fs.existsSync(ENV_FILE_PATH)) {
|
|
14
|
-
throw new Error(`Environment file not found.`);
|
|
15
|
-
}
|
|
16
|
-
let envData;
|
|
12
|
+
function loadEnvContent() {
|
|
17
13
|
try {
|
|
18
|
-
|
|
14
|
+
return fs.readFileSync(ENV_FILE_PATH, 'utf-8');
|
|
19
15
|
}
|
|
20
16
|
catch (error) {
|
|
21
17
|
const err = error;
|
|
22
18
|
throw new Error(`Failed to read environment file. Error: ${err.message}`);
|
|
23
19
|
}
|
|
24
|
-
|
|
20
|
+
}
|
|
21
|
+
function parseEnvLine(line) {
|
|
22
|
+
const trimmedLine = line.trim();
|
|
23
|
+
if (!trimmedLine || trimmedLine.startsWith('#')) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const firstEqualIndex = trimmedLine.indexOf('=');
|
|
27
|
+
if (firstEqualIndex === -1) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const key = trimmedLine.substring(0, firstEqualIndex).trim();
|
|
31
|
+
const value = trimmedLine.substring(firstEqualIndex + 1).trim();
|
|
32
|
+
return { key, value };
|
|
33
|
+
}
|
|
34
|
+
function readEnvFile() {
|
|
35
|
+
if (!fs.existsSync(ENV_FILE_PATH)) {
|
|
36
|
+
throw new Error(`Environment file not found.`);
|
|
37
|
+
}
|
|
38
|
+
const envData = loadEnvContent();
|
|
39
|
+
const envVars = {};
|
|
25
40
|
const lines = envData.split('\n');
|
|
26
41
|
for (const line of lines) {
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
const firstEqualIndex = trimmedLine.indexOf('=');
|
|
32
|
-
if (firstEqualIndex === -1) {
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
const key = trimmedLine.substring(0, firstEqualIndex).trim();
|
|
36
|
-
const value = trimmedLine.substring(firstEqualIndex + 1).trim();
|
|
37
|
-
if (key && REQUIRED_ENV_VARS.includes(key)) {
|
|
38
|
-
env[key] = value;
|
|
42
|
+
const parsed = parseEnvLine(line);
|
|
43
|
+
if (parsed && parsed.key && REQUIRED_ENV_VARS.includes(parsed.key)) {
|
|
44
|
+
envVars[parsed.key] = parsed.value;
|
|
39
45
|
}
|
|
40
46
|
}
|
|
41
|
-
return
|
|
47
|
+
return envVars;
|
|
42
48
|
}
|
|
43
|
-
|
|
44
|
-
if (cachedConfig) {
|
|
45
|
-
return cachedConfig;
|
|
46
|
-
}
|
|
47
|
-
const configPath = path.join(__dirname, CONFIG_FILE_NAME);
|
|
48
|
-
if (!fs.existsSync(configPath)) {
|
|
49
|
-
throw new Error(`Config file not found: ${CONFIG_FILE_NAME}`);
|
|
50
|
-
}
|
|
49
|
+
function loadConfigFile(configPath) {
|
|
51
50
|
let configData;
|
|
52
51
|
try {
|
|
53
52
|
configData = fs.readFileSync(configPath, 'utf-8');
|
|
@@ -55,58 +54,65 @@ export function getConfig(api) {
|
|
|
55
54
|
catch (error) {
|
|
56
55
|
throw new Error(`Failed to read config file: ${CONFIG_FILE_NAME}.`);
|
|
57
56
|
}
|
|
58
|
-
let parsedConfig;
|
|
59
57
|
try {
|
|
60
|
-
|
|
58
|
+
return JSON.parse(configData);
|
|
61
59
|
}
|
|
62
60
|
catch (error) {
|
|
63
61
|
throw new Error(`Failed to parse config file: ${CONFIG_FILE_NAME}.`);
|
|
64
62
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (!config.requestFrom || typeof config.requestFrom !== 'string') {
|
|
79
|
-
throw new Error(`Invalid config: missing or invalid 'requestFrom' in ${CONFIG_FILE_NAME}`);
|
|
80
|
-
}
|
|
81
|
-
if (!config.textSource || typeof config.textSource !== 'string') {
|
|
82
|
-
throw new Error(`Invalid config: missing or invalid 'textSource' in ${CONFIG_FILE_NAME}`);
|
|
83
|
-
}
|
|
84
|
-
if (!config.action || typeof config.action !== 'string') {
|
|
85
|
-
throw new Error(`Invalid config: missing or invalid 'action' in ${CONFIG_FILE_NAME}`);
|
|
63
|
+
}
|
|
64
|
+
function validateConfigStructure(config) {
|
|
65
|
+
const validators = [
|
|
66
|
+
{ field: 'api', check: () => !config.api || typeof config.api !== 'object' },
|
|
67
|
+
{ field: 'api.timeout', check: () => !config.api?.timeout || typeof config.api.timeout !== 'number' },
|
|
68
|
+
{ field: 'skillId', check: () => !config.skillId || typeof config.skillId !== 'string' },
|
|
69
|
+
{ field: 'requestFrom', check: () => !config.requestFrom || typeof config.requestFrom !== 'string' },
|
|
70
|
+
{ field: 'textSource', check: () => !config.textSource || typeof config.textSource !== 'string' },
|
|
71
|
+
];
|
|
72
|
+
for (const { field, check } of validators) {
|
|
73
|
+
if (check()) {
|
|
74
|
+
throw new Error(`Invalid config: missing or invalid '${field}' in ${CONFIG_FILE_NAME}`);
|
|
75
|
+
}
|
|
86
76
|
}
|
|
87
|
-
|
|
77
|
+
}
|
|
78
|
+
function validateEnvVars() {
|
|
79
|
+
let envVars;
|
|
88
80
|
try {
|
|
89
|
-
|
|
81
|
+
envVars = readEnvFile();
|
|
90
82
|
}
|
|
91
83
|
catch (error) {
|
|
92
84
|
const err = error;
|
|
93
85
|
throw new Error(`Failed to load environment variables from env files: ${err.message}`);
|
|
94
86
|
}
|
|
95
|
-
const personalApiKey =
|
|
87
|
+
const personalApiKey = envVars['PERSONAL-API-KEY'];
|
|
96
88
|
if (!personalApiKey || typeof personalApiKey !== 'string' || personalApiKey.trim() === '') {
|
|
97
89
|
throw new Error(`Missing or empty 'PERSONAL-API-KEY' in env files`);
|
|
98
90
|
}
|
|
99
|
-
const personalUid =
|
|
91
|
+
const personalUid = envVars['PERSONAL-UID'];
|
|
100
92
|
if (!personalUid || typeof personalUid !== 'string' || personalUid.trim() === '') {
|
|
101
93
|
throw new Error(`Missing or empty 'PERSONAL-UID' in env files`);
|
|
102
94
|
}
|
|
103
|
-
const serviceUrl =
|
|
95
|
+
const serviceUrl = envVars.SERVICE_URL;
|
|
104
96
|
if (!serviceUrl || typeof serviceUrl !== 'string' || serviceUrl.trim() === '') {
|
|
105
97
|
throw new Error(`Missing or empty 'SERVICE_URL' in env files`);
|
|
106
98
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
99
|
+
return envVars;
|
|
100
|
+
}
|
|
101
|
+
export function getConfig(api) {
|
|
102
|
+
if (cachedConfig) {
|
|
103
|
+
return cachedConfig;
|
|
104
|
+
}
|
|
105
|
+
const configPath = path.join(__dirname, CONFIG_FILE_NAME);
|
|
106
|
+
if (!fs.existsSync(configPath)) {
|
|
107
|
+
throw new Error(`Config file not found: ${CONFIG_FILE_NAME}`);
|
|
108
|
+
}
|
|
109
|
+
const parsedConfig = loadConfigFile(configPath);
|
|
110
|
+
const config = parsedConfig;
|
|
111
|
+
validateConfigStructure(config);
|
|
112
|
+
const envVars = validateEnvVars();
|
|
113
|
+
config.apiKey = envVars['PERSONAL-API-KEY'].trim();
|
|
114
|
+
config.uid = envVars['PERSONAL-UID'].trim();
|
|
115
|
+
config.api.url = envVars.SERVICE_URL.trim();
|
|
110
116
|
cachedConfig = config;
|
|
111
117
|
logger.log(`[SENTINEL HOOK] Config loaded successfully`);
|
|
112
118
|
return cachedConfig;
|
|
@@ -11,7 +11,42 @@ export interface ApiPayload {
|
|
|
11
11
|
questionText: string;
|
|
12
12
|
textSource: string;
|
|
13
13
|
action: string;
|
|
14
|
-
|
|
14
|
+
}
|
|
15
|
+
export declare const RESULT_CODE_MAP: Record<number, string>;
|
|
16
|
+
export interface NewRequestPayload {
|
|
17
|
+
taskID: string;
|
|
18
|
+
sessionID: string;
|
|
19
|
+
uid: string;
|
|
20
|
+
businessID: string;
|
|
21
|
+
sceneID: string;
|
|
22
|
+
checkPoint: number;
|
|
23
|
+
interActionID: number;
|
|
24
|
+
loginType?: string;
|
|
25
|
+
reqTime?: string;
|
|
26
|
+
message: object;
|
|
27
|
+
}
|
|
28
|
+
export interface NewApiResponse {
|
|
29
|
+
data: {
|
|
30
|
+
taskID: string;
|
|
31
|
+
resultCode: number;
|
|
32
|
+
resultMessage?: string;
|
|
33
|
+
securityResult: string;
|
|
34
|
+
riskLabels?: string[];
|
|
35
|
+
riskDegree?: Array<{
|
|
36
|
+
riskLabel: string;
|
|
37
|
+
score: number;
|
|
38
|
+
}>;
|
|
39
|
+
riskLabelCount?: Array<{
|
|
40
|
+
riskLabel: string;
|
|
41
|
+
count: number;
|
|
42
|
+
}>;
|
|
43
|
+
actionRiskResult?: {
|
|
44
|
+
riskScore: number;
|
|
45
|
+
riskTag: string[];
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
retCode?: string;
|
|
49
|
+
retMsg?: string;
|
|
15
50
|
}
|
|
16
51
|
export interface ApiResponse {
|
|
17
52
|
[key: string]: any;
|