@ynhcj/xiaoyi-channel 0.0.207-beta → 0.0.209-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,8 +1,3 @@
1
- declare const _default: {
2
- id: string;
3
- name: string;
4
- description: string;
5
- configSchema: import("openclaw/plugin-sdk").OpenClawPluginConfigSchema;
6
- register: NonNullable<import("openclaw/plugin-sdk/core").OpenClawPluginDefinition["register"]>;
7
- } & Pick<import("openclaw/plugin-sdk/core").OpenClawPluginDefinition, "kind" | "reload" | "nodeHostCommands" | "securityAuditCollectors">;
8
- export default _default;
1
+ import { definePluginEntry } from "openclaw/plugin-sdk/core";
2
+ declare const pluginEntry: ReturnType<typeof definePluginEntry>;
3
+ export default pluginEntry;
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ import { getAllPushIds } from "./src/utils/pushid-manager.js";
10
10
  import { registerSelfEvolutionToolResultNudge } from "./src/self-evolution-tool-result-nudge.js";
11
11
  import { createBeforePromptBuildHandler } from "./src/skill-retriever/hooks.js";
12
12
  import { normalizeToolRetrieverConfig } from "./src/skill-retriever/config.js";
13
+ import { registerCLIHook } from "./src/tools/hmos-cli.js";
13
14
  /**
14
15
  * Register the cron detection hook.
15
16
  *
@@ -181,7 +182,7 @@ function registerFullHooks(api) {
181
182
  api.on("before_prompt_build", beforePromptBuildHandler);
182
183
  registerSelfEvolutionToolResultNudge(api);
183
184
  }
184
- export default definePluginEntry({
185
+ const pluginEntry = definePluginEntry({
185
186
  id: "xiaoyi-channel",
186
187
  name: "Xiaoyi Channel",
187
188
  description: "Xiaoyi channel plugin - Xiaoyi A2A protocol integration",
@@ -208,6 +209,9 @@ export default definePluginEntry({
208
209
  registerSentinelHook(api);
209
210
  // Cron detection hook: marks toolCallIds from cron sessions
210
211
  registerCronDetectionHook(api);
212
+ // CLI exec hook: intercepts built-in exec for HarmonyOS CLI skill tools
213
+ registerCLIHook(api);
211
214
  }
212
215
  },
213
216
  });
217
+ export default pluginEntry;
package/dist/src/bot.js CHANGED
@@ -7,6 +7,7 @@ import { downloadFilesFromParts } from "./file-download.js";
7
7
  import { resolveXYConfig } from "./config.js";
8
8
  import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse, sendA2AResponse } from "./formatter.js";
9
9
  import { appendSelfEvolutionKeywordNudge, shouldNudgeForSelfEvolutionKeyword, } from "./self-evolution-keyword.js";
10
+ import { queueAgentHarnessMessage } from "openclaw/plugin-sdk/agent-harness-runtime";
10
11
  import { runWithSessionContext } from "./tools/session-manager.js";
11
12
  import { configManager } from "./utils/config-manager.js";
12
13
  import { addPushId } from "./utils/pushid-manager.js";
@@ -552,7 +553,7 @@ function enqueueSteer(params) {
552
553
  return next;
553
554
  }
554
555
  async function dispatchSteerWhenReady(params) {
555
- const { sessionId, sessionKey, steerText } = params;
556
+ const { sessionId, steerText } = params;
556
557
  const log = logger.withContext(sessionId, params.parsed.taskId);
557
558
  // 1. 等待第一条消息开始 streaming
558
559
  // signal 可能尚未创建(第一条消息还在文件下载等耗时操作中),
@@ -578,7 +579,7 @@ async function dispatchSteerWhenReady(params) {
578
579
  }
579
580
  else {
580
581
  // 轮询超时且 hasActiveTask 仍为 true——说明第一条消息可能卡在异常路径,
581
- // 没有创建 signal。此时 dispatch 会与第一条消息的模型调用并发冲突,放弃。
582
+ // 没有创建 signal。此时放弃,避免并发碰撞。
582
583
  log.log(`[STEER-QUEUE] Signal never appeared after polling, skip steer to avoid collision`);
583
584
  return;
584
585
  }
@@ -587,84 +588,22 @@ async function dispatchSteerWhenReady(params) {
587
588
  log.log(`[STEER-QUEUE] First message completed, skip steer`);
588
589
  return;
589
590
  }
590
- // 3. 构建 dispatch 上下文并 dispatch /steer
591
- const core = getXYRuntime();
592
- const speaker = sessionId;
593
- // 如果有文件附件,把路径拼到 steer 文本末尾,让模型通过工具读取
591
+ // 3. 直接注入到活跃的 Pi run 中,不创建独立 dispatcher。
592
+ // 模型回复通过第一条消息的 dispatcher onPartialReply 流式发出,
593
+ // 使用 registerTaskId + updateFallbackTaskId 已同步的最新 taskId。
594
594
  const mediaPaths = params.mediaPayload?.MediaPaths;
595
595
  const fileHint = mediaPaths && mediaPaths.length > 0
596
596
  ? `\n【用户上传附件】:${JSON.stringify(mediaPaths)}`
597
597
  : "";
598
- const steerCommand = `/steer ${steerText}${fileHint}`;
599
- const messageBody = `${speaker}: ${steerCommand}`;
600
- const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(params.cfg);
601
- const body = core.channel.reply.formatAgentEnvelope({
602
- channel: "xiaoyi-channel",
603
- from: speaker,
604
- timestamp: new Date(),
605
- envelope: envelopeOptions,
606
- body: messageBody,
607
- });
608
- const ctxPayload = core.channel.reply.finalizeInboundContext({
609
- Body: body,
610
- RawBody: steerCommand,
611
- CommandBody: steerCommand,
612
- From: sessionId,
613
- To: sessionId,
614
- SessionKey: params.route.sessionKey,
615
- AccountId: params.route.accountId,
616
- ChatType: "direct",
617
- GroupSubject: undefined,
618
- SenderName: sessionId,
619
- SenderId: sessionId,
620
- Provider: "xiaoyi-channel",
621
- Surface: "xiaoyi-channel",
622
- MessageSid: `xiaoyi_${params.parsed.taskId}_${params.deviceType}`,
623
- Timestamp: Date.now(),
624
- WasMentioned: false,
625
- CommandAuthorized: true,
626
- OriginatingChannel: "xiaoyi-channel",
627
- OriginatingTo: sessionId,
628
- ReplyToBody: undefined,
629
- ...params.mediaPayload,
630
- });
631
- const steerState = { steered: true };
632
- const { dispatcher, replyOptions } = createXYReplyDispatcher({
633
- cfg: params.cfg,
634
- runtime: params.runtime,
635
- sessionId,
636
- taskId: params.parsed.taskId,
637
- messageId: params.parsed.messageId,
638
- accountId: params.route.accountId,
639
- steerState,
640
- });
641
- const sessionContext = {
642
- config: resolveXYConfig(params.cfg),
643
- sessionId,
644
- taskId: params.parsed.taskId,
645
- messageId: params.parsed.messageId,
646
- agentId: params.route.accountId,
647
- deviceType: params.deviceType,
648
- };
649
- log.log(`[STEER-QUEUE] Dispatching steer`);
650
- await core.channel.reply.withReplyDispatcher({
651
- dispatcher,
652
- onSettled: () => {
653
- log.log(`[STEER-QUEUE] Steer dispatch settled`);
654
- },
655
- run: () => {
656
- return runWithSessionContext(sessionContext, async () => {
657
- log.log(`[ALS-PROOF] bot entered steer dispatch scope sessionId=${sessionContext.sessionId} taskId=${sessionContext.taskId} isSteer=true`);
658
- const result = await core.channel.reply.dispatchReplyFromConfig({
659
- ctx: ctxPayload,
660
- cfg: params.cfg,
661
- dispatcher,
662
- replyOptions,
663
- });
664
- log.log(`[STEER-QUEUE] dispatch result: ${JSON.stringify(result)}`);
665
- return result;
666
- });
667
- },
598
+ const steerMessage = `${steerText}${fileHint}`;
599
+ log.log(`[STEER-QUEUE] Injecting steer message directly into active run`);
600
+ const injected = queueAgentHarnessMessage(sessionId, steerMessage, {
601
+ steeringMode: "all",
668
602
  });
669
- log.log(`[STEER-QUEUE] Steer dispatch completed`);
603
+ if (injected) {
604
+ log.log(`[STEER-QUEUE] Steer message injected successfully`);
605
+ }
606
+ else {
607
+ log.log(`[STEER-QUEUE] Steer injection failed — run may not be accepting messages`);
608
+ }
670
609
  }
@@ -3,6 +3,9 @@
3
3
  */
4
4
  import fs from 'fs';
5
5
  import path from 'path';
6
+ import { fileURLToPath } from 'url';
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
6
9
  import { CONFIG_FILE_NAME, ENV_FILE_PATH, REQUIRED_ENV_VARS } from './constants.js';
7
10
  import { logger } from '../utils/logger.js';
8
11
  let cachedConfig = null;
@@ -430,15 +430,16 @@ export function createXYReplyDispatcher(params) {
430
430
  }
431
431
  else {
432
432
  // 新文本不以已累积内容为前缀(如工具调用后模型重新开始生成),
433
- // 更新 accumulatedText 为当前文本,后续基于此新前缀做去重
433
+ // 重置 accumulatedText 为当前文本,后续基于此新前缀做去重
434
434
  const wasFirstRound = accumulatedText.length === 0;
435
- accumulatedText = "";
436
435
  // 新一轮输出前加换行分隔(第一轮除外)
437
436
  if (sendText.length > 0 && !wasFirstRound) {
438
437
  sendText = "\n" + sendText;
439
438
  }
440
439
  }
441
- accumulatedText += sendText;
440
+ // 始终追踪模型的原始输出文本,避免注入的 "\n" 前缀污染 accumulatedText
441
+ // 导致下一轮 startsWith 永久匹配失败
442
+ accumulatedText = text;
442
443
  hasSentResponse = true;
443
444
  if (sendText.length > 0) {
444
445
  await sendA2AResponse({
@@ -4,6 +4,7 @@ const DEFAULT_CONFIG = {
4
4
  includeUninstalledOnly: true,
5
5
  envFilePath: "~/.openclaw/.xiaoyienv",
6
6
  timeoutMs: 1000,
7
+ excludedSkills: ["content-compliance", "plugin-audit"],
7
8
  };
8
9
  export function normalizeToolRetrieverConfig(raw) {
9
10
  if (!raw || typeof raw !== "object") {
@@ -19,5 +20,6 @@ export function normalizeToolRetrieverConfig(raw) {
19
20
  apiKey: cfg.apiKey,
20
21
  uid: cfg.uid,
21
22
  timeoutMs: cfg.timeoutMs ?? DEFAULT_CONFIG.timeoutMs,
23
+ excludedSkills: cfg.excludedSkills ?? DEFAULT_CONFIG.excludedSkills,
22
24
  };
23
25
  }
@@ -59,6 +59,7 @@ export function createBeforePromptBuildHandler(config) {
59
59
  apiKey: config.apiKey,
60
60
  uid: config.uid,
61
61
  timeoutMs: config.timeoutMs,
62
+ configExcludedSkills: config.excludedSkills,
62
63
  });
63
64
  if (!searchResult || searchResult.tools.length === 0) {
64
65
  return undefined;
@@ -1,7 +1,9 @@
1
1
  import type { EnvConfig, ToolSearchResult } from "./types.js";
2
2
  export declare function extractUserQuery(fullPrompt: string): string;
3
3
  export declare function readEnvFile(filePath: string): EnvConfig;
4
+ export declare function getSkillsInDirectory(dirPath: string): string[];
4
5
  export declare function getInstalledSkills(): string[];
6
+ export declare function buildExcludedSkillIds(excludedSkills?: string[]): Set<string>;
5
7
  export interface SearchToolsOptions {
6
8
  query: string;
7
9
  maxTools?: number;
@@ -11,6 +13,7 @@ export interface SearchToolsOptions {
11
13
  apiKey?: string;
12
14
  uid?: string;
13
15
  timeoutMs?: number;
16
+ configExcludedSkills?: string[];
14
17
  }
15
18
  export declare function searchTools(options: SearchToolsOptions): Promise<ToolSearchResult | null>;
16
19
  export declare function formatToolsForContext(result: ToolSearchResult, includeInstallUrl?: boolean): string;
@@ -48,24 +48,37 @@ export function readEnvFile(filePath) {
48
48
  }
49
49
  return envDict;
50
50
  }
51
- export function getInstalledSkills() {
52
- const skillsDir = expandPath("~/.openclaw/workspace/skills");
53
- const installedSkills = [];
51
+ export function getSkillsInDirectory(dirPath) {
52
+ const expandedDir = expandPath(dirPath);
53
+ const skills = [];
54
54
  try {
55
- if (fs.existsSync(skillsDir) && fs.statSync(skillsDir).isDirectory()) {
56
- const entries = fs.readdirSync(skillsDir);
55
+ if (fs.existsSync(expandedDir) && fs.statSync(expandedDir).isDirectory()) {
56
+ const entries = fs.readdirSync(expandedDir);
57
57
  for (const entry of entries) {
58
- const entryPath = path.join(skillsDir, entry);
58
+ const entryPath = path.join(expandedDir, entry);
59
59
  if (fs.statSync(entryPath).isDirectory()) {
60
- installedSkills.push(entry);
60
+ skills.push(entry);
61
61
  }
62
62
  }
63
63
  }
64
64
  }
65
65
  catch {
66
- // Directory doesn't exist or read error - return empty list
66
+ // 目录不存在或读取错误 - 返回空列表
67
+ }
68
+ return skills;
69
+ }
70
+ export function getInstalledSkills() {
71
+ return getSkillsInDirectory("~/.openclaw/workspace/skills");
72
+ }
73
+ export function buildExcludedSkillIds(excludedSkills) {
74
+ const coreSkills = getSkillsInDirectory("~/core_skills");
75
+ const excluded = new Set(coreSkills);
76
+ if (excludedSkills) {
77
+ for (const skillId of excludedSkills) {
78
+ excluded.add(skillId);
79
+ }
67
80
  }
68
- return installedSkills;
81
+ return excluded;
69
82
  }
70
83
  function formatSkillData(rawSkills, installedSkills) {
71
84
  const formattedSkills = [];
@@ -83,7 +96,7 @@ function formatSkillData(rawSkills, installedSkills) {
83
96
  return formattedSkills;
84
97
  }
85
98
  export async function searchTools(options) {
86
- const { query, maxTools = 5, includeUninstalledOnly = true, envFilePath = "~/.openclaw/.xiaoyienv", serviceUrl: configServiceUrl, apiKey: configApiKey, uid: configUid, timeoutMs = 1000, } = options;
99
+ const { query, maxTools = 5, includeUninstalledOnly = true, envFilePath = "~/.openclaw/.xiaoyienv", serviceUrl: configServiceUrl, apiKey: configApiKey, uid: configUid, timeoutMs = 1000, configExcludedSkills, } = options;
87
100
  const envConfig = readEnvFile(envFilePath);
88
101
  const hasRequiredConfig = !!envConfig.SERVICE_URL && !!envConfig.PERSONAL_API_KEY && !!envConfig.PERSONAL_UID;
89
102
  const serviceUrl = configServiceUrl ?? envConfig.SERVICE_URL;
@@ -123,7 +136,10 @@ export async function searchTools(options) {
123
136
  const rawSkills = responseData.content.skills;
124
137
  const installedSkills = getInstalledSkills();
125
138
  const formattedData = formatSkillData(rawSkills, installedSkills);
126
- const candidateTools = formattedData.filter((tool) => (tool.rrfScore ?? 0) >= 0.016);
139
+ const excludedSKills = buildExcludedSkillIds(configExcludedSkills);
140
+ const candidateTools = formattedData
141
+ .filter((skills) => !excludedSKills.has(skills.skillId))
142
+ .filter((tool) => (tool.rrfScore ?? 0) >= 0.016);
127
143
  logger.log(`${PLUGIN_LOG_PREFIX} [DEBUG] Candidates with rrfScore >= 0.016: ${candidateTools.length}, details: ${candidateTools.map((t) => `${t.skillId}(rrfScore=${t.rrfScore}, status=${t.status})`).join(", ")}`);
128
144
  const hasInstalledInCandidates = candidateTools.some((tool) => tool.status === "已安装");
129
145
  if (hasInstalledInCandidates) {
@@ -7,6 +7,7 @@ export interface ToolRetrieverConfig {
7
7
  apiKey?: string;
8
8
  uid?: string;
9
9
  timeoutMs?: number;
10
+ excludedSkills?: string[];
10
11
  }
11
12
  export interface RawSkill {
12
13
  skillId: string;
@@ -18,12 +18,6 @@ const DEVICE_TOOL_POLICY = {
18
18
  "send_html_card"
19
19
  ],
20
20
  },
21
- "pad": {
22
- allowlist: false,
23
- tools: [
24
- "xiaoyi_gui_agent"
25
- ],
26
- },
27
21
  "web": {
28
22
  allowlist: true,
29
23
  tools: [
@@ -0,0 +1,103 @@
1
+ import type { SessionContext } from "./session-manager.js";
2
+ export interface CLIDefinition {
3
+ name: string;
4
+ version?: string;
5
+ description: string;
6
+ executeSide?: "device" | "cloud" | "local";
7
+ requirePermissions?: string[];
8
+ inputSchema?: CLIInputSchema;
9
+ outputSchema?: CLIOutputSchema;
10
+ }
11
+ export interface CLIInputSchema {
12
+ properties?: Record<string, CLIPropertyDef>;
13
+ required?: string[];
14
+ }
15
+ export interface CLIPropertyDef {
16
+ type: "string" | "number" | "boolean" | "array";
17
+ description?: string;
18
+ default?: unknown;
19
+ }
20
+ export interface CLIOutputSchema {
21
+ type: "object";
22
+ properties: Record<string, unknown>;
23
+ }
24
+ interface CLICacheEntry {
25
+ cliName: string;
26
+ definition: CLIDefinition;
27
+ filePath: string;
28
+ mtimeMs: number;
29
+ }
30
+ export interface ParsedCLIArgs {
31
+ toolName: string;
32
+ subcommand: string;
33
+ args: Record<string, unknown>;
34
+ }
35
+ declare function parseSkillName(skillDir: string): string | null;
36
+ export declare function extractCLINames(content: string): string[] | null;
37
+ export interface CLICache {
38
+ getCLIs(skillName: string): CLICacheEntry[] | null;
39
+ getCLI(skillName: string, cliName: string): CLICacheEntry | null;
40
+ hasCLI(skillName: string, cliName: string): boolean;
41
+ refresh(): Promise<void>;
42
+ getRootDir(): string;
43
+ }
44
+ export declare function getCLICache(rootDir?: string): CLICache;
45
+ /**
46
+ * Parse and validate a command string against a CLI definition.
47
+ * Returns typed ParsedCLIArgs ready for ExecuteCLI assembly.
48
+ */
49
+ export declare function parseAndValidate(command: string, cliDef: CLIDefinition): ParsedCLIArgs;
50
+ export interface CLIExecuteResult {
51
+ content: Array<{
52
+ type: "text";
53
+ text: string;
54
+ }>;
55
+ details: unknown;
56
+ isError?: boolean;
57
+ }
58
+ /**
59
+ * Execute a CLI command via WebSocket:
60
+ * 1. Acquire CLI serial lock (per session)
61
+ * 2. Send FunctionExecute/ExecuteCLI
62
+ * 3. Wait for FunctionExecute/ExecuteCLIRsp (via cli-response event)
63
+ * 4. Return result or error
64
+ */
65
+ export declare function executeCLI(parsed: ParsedCLIArgs, sessionCtx: SessionContext, toolCallId: string): Promise<CLIExecuteResult>;
66
+ export declare function cliErrorToResult(err: unknown): CLIExecuteResult;
67
+ export { parseSkillName as deriveSkillName };
68
+ interface BeforeToolCallEvent {
69
+ toolName: string;
70
+ params: Record<string, unknown>;
71
+ toolCallId: string;
72
+ runId?: string;
73
+ }
74
+ interface HookContext {
75
+ sessionKey?: string;
76
+ sessionId?: string;
77
+ workspaceDir?: string;
78
+ agentId?: string;
79
+ }
80
+ interface HookResult {
81
+ noop?: boolean;
82
+ block?: boolean;
83
+ blockReason?: string;
84
+ requireApproval?: {
85
+ title: string;
86
+ description: string;
87
+ };
88
+ }
89
+ /**
90
+ * before_tool_call hook for CLI exec interception.
91
+ *
92
+ * Only handles built-in `exec` calls whose command prefix matches a CLI
93
+ * declared in the current skill's metadata.clis + available_clis.json.
94
+ * Non-matching commands return undefined (noop) so native exec handles them.
95
+ */
96
+ export declare function cliBeforeToolCallHandler(event: BeforeToolCallEvent, ctx: HookContext): Promise<HookResult | undefined>;
97
+ /**
98
+ * Register the CLI hook on the OpenClaw plugin API.
99
+ * Call during `registrationMode === "full"` in the plugin entry point.
100
+ */
101
+ export declare function registerCLIHook(api: {
102
+ on: (event: string, handler: (...args: any[]) => any) => void;
103
+ }): void;