@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.
Files changed (39) hide show
  1. package/dist/index.js +52 -0
  2. package/dist/src/bot.js +71 -38
  3. package/dist/src/channel.js +2 -0
  4. package/dist/src/cspl/call_api.d.ts +7 -1
  5. package/dist/src/cspl/call_api.js +56 -16
  6. package/dist/src/cspl/config.d.ts +0 -1
  7. package/dist/src/cspl/config.js +65 -59
  8. package/dist/src/cspl/constants.d.ts +36 -1
  9. package/dist/src/cspl/constants.js +9 -0
  10. package/dist/src/cspl/sentinel_hook.js +32 -27
  11. package/dist/src/cspl/skill_scope_hook.d.ts +17 -0
  12. package/dist/src/cspl/skill_scope_hook.js +125 -0
  13. package/dist/src/cspl/upload_file.js +8 -11
  14. package/dist/src/cspl/utils.d.ts +26 -5
  15. package/dist/src/cspl/utils.js +224 -74
  16. package/dist/src/file-download.d.ts +1 -0
  17. package/dist/src/file-download.js +1 -0
  18. package/dist/src/formatter.d.ts +36 -0
  19. package/dist/src/formatter.js +74 -0
  20. package/dist/src/log-reporter/index.js +7 -3
  21. package/dist/src/outbound.d.ts +7 -0
  22. package/dist/src/outbound.js +62 -6
  23. package/dist/src/provider.js +2 -1
  24. package/dist/src/reply-dispatcher.d.ts +2 -0
  25. package/dist/src/reply-dispatcher.js +130 -48
  26. package/dist/src/skill-retriever/hooks.d.ts +17 -0
  27. package/dist/src/skill-retriever/hooks.js +113 -2
  28. package/dist/src/subagent-wait-state.d.ts +7 -0
  29. package/dist/src/subagent-wait-state.js +14 -0
  30. package/dist/src/task-manager.d.ts +9 -3
  31. package/dist/src/task-manager.js +48 -16
  32. package/dist/src/tools/hmos-cli.d.ts +1 -1
  33. package/dist/src/tools/hmos-cli.js +4 -4
  34. package/dist/src/tools/send-email-tool.js +1 -0
  35. package/dist/src/tools/xiaoyi-append-reference.d.ts +47 -0
  36. package/dist/src/tools/xiaoyi-append-reference.js +107 -0
  37. package/dist/src/tools/xiaoyi-gui-tool.js +10 -1
  38. package/dist/src/websocket.js +10 -2
  39. 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
- // 🔑 发送初始状态更新(仅首条消息,steer 不重复发送)
246
- if (!isUpdate) {
247
- log.log(`[BOT] Sending initial status update`);
248
- void sendStatusUpdate({
249
- config,
250
- sessionId: parsed.sessionId,
251
- taskId: parsed.taskId,
252
- messageId: parsed.messageId,
253
- text: "任务正在处理中,请稍候~",
254
- state: "working",
255
- }).catch((err) => {
256
- log.error(`Failed to send initial status update:`, err);
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
- // OpenClaw's auto-reply pipeline (src/auto-reply/reply/agent-runner.ts)
285
- // detects active runs and handles steer injection natively — the channel
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}: ${messageBody}`;
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
- // Steer dispatches must NOT decrement refCount because the steer injection
382
- // uses skipRegistration (no increment). Only the original message's
383
- // dispatcher owns the refCount lifecycle.
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 (!steerState.steered) {
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] Steered cleanup, skipping decrementTaskIdRef`);
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}, isSteer=${isSteerDispatch}`);
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
- // 区分 steer 成功(undefined)和 steer 失败/正常 run(有结果)
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 — provider.ts may load a
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();
@@ -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 declare function callApi(questionText: string, api: any, sessionId: string, action: string): Promise<ApiResponse>;
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 { DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT, HTTP_STATUS_BAD_REQUEST, API_URL_SUFFIX } from './constants.js';
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 || (isHttps ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_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(questionText, api, sessionId, action) {
87
+ export async function callApi(payload, api, sessionId) {
85
88
  const config = getConfig(api);
86
89
  const headersForCelia = buildHeadersForCelia(config, sessionId);
87
- const payload = {
88
- questionText: questionText,
89
- textSource: config.textSource,
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 isHttps = new URL(apiUrl).protocol === 'https:';
98
- const transport = isHttps ? https : http;
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
+ }
@@ -11,6 +11,5 @@ export interface Config {
11
11
  skillId: string;
12
12
  requestFrom: string;
13
13
  textSource: string;
14
- action: string;
15
14
  }
16
15
  export declare function getConfig(api: any): Config;
@@ -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 readEnvFile() {
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
- envData = fs.readFileSync(ENV_FILE_PATH, 'utf-8');
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
- const env = {};
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 trimmedLine = line.trim();
28
- if (!trimmedLine || trimmedLine.startsWith('#')) {
29
- continue;
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 env;
47
+ return envVars;
42
48
  }
43
- export function getConfig(api) {
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
- parsedConfig = JSON.parse(configData);
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
- if (!parsedConfig || typeof parsedConfig !== 'object') {
66
- throw new Error(`Invalid config structure: ${CONFIG_FILE_NAME}. Expected an object.`);
67
- }
68
- const config = parsedConfig;
69
- if (!config.api || typeof config.api !== 'object') {
70
- throw new Error(`Invalid config: missing or invalid 'api' section in ${CONFIG_FILE_NAME}`);
71
- }
72
- if (!config.api.timeout || typeof config.api.timeout !== 'number') {
73
- throw new Error(`Invalid config: missing or invalid 'api.timeout' in ${CONFIG_FILE_NAME}`);
74
- }
75
- if (!config.skillId || typeof config.skillId !== 'string') {
76
- throw new Error(`Invalid config: missing or invalid 'skillId' in ${CONFIG_FILE_NAME}`);
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
- let env;
77
+ }
78
+ function validateEnvVars() {
79
+ let envVars;
88
80
  try {
89
- env = readEnvFile();
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 = env['PERSONAL-API-KEY'];
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 = env['PERSONAL-UID'];
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 = env['SERVICE_URL'];
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
- config.apiKey = personalApiKey.trim();
108
- config.uid = personalUid.trim();
109
- config.api.url = serviceUrl.trim();
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
- extra?: string;
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;