@ynhcj/xiaoyi-channel 0.0.217-beta → 0.0.218-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.d.ts +3 -3
- package/dist/index.js +133 -3
- package/dist/src/bot.js +111 -30
- package/dist/src/channel.js +2 -0
- package/dist/src/compaction-provider.d.ts +50 -0
- package/dist/src/compaction-provider.js +202 -0
- package/dist/src/cron-query-handler.d.ts +20 -0
- package/dist/src/cron-query-handler.js +481 -91
- package/dist/src/cron-recovery.d.ts +25 -0
- package/dist/src/cron-recovery.js +587 -0
- package/dist/src/cspl/call_api.d.ts +7 -1
- package/dist/src/cspl/call_api.js +51 -6
- package/dist/src/cspl/sentinel_hook.js +3 -3
- 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 +5 -2
- package/dist/src/cspl/utils.d.ts +16 -6
- package/dist/src/cspl/utils.js +113 -7
- 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/monitor.js +49 -8
- package/dist/src/outbound.d.ts +7 -0
- package/dist/src/outbound.js +62 -6
- package/dist/src/provider.js +90 -2
- package/dist/src/reply-dispatcher.d.ts +2 -0
- package/dist/src/reply-dispatcher.js +127 -48
- package/dist/src/skill-retriever/hooks.d.ts +17 -0
- package/dist/src/skill-retriever/hooks.js +123 -3
- package/dist/src/skill-retriever/tool-search.js +13 -11
- package/dist/src/steer-queue.d.ts +1 -0
- package/dist/src/steer-queue.js +9 -0
- 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/call-phone-tool.d.ts +0 -1
- package/dist/src/tools/call-phone-tool.js +5 -8
- 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 +3 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
declare const
|
|
3
|
-
export default
|
|
1
|
+
import { type OpenClawPluginDefinition } from "openclaw/plugin-sdk/core";
|
|
2
|
+
declare const plugin: OpenClawPluginDefinition;
|
|
3
|
+
export default plugin;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { definePluginEntry } from "openclaw/plugin-sdk/core";
|
|
2
2
|
import { xiaoyiProvider } from "./src/provider.js";
|
|
3
|
+
import { xiaoyiCompactionProvider } from "./src/compaction-provider.js";
|
|
3
4
|
import { xyPlugin } from "./src/channel.js";
|
|
4
5
|
import registerSentinelHook from "./src/cspl/sentinel_hook.js";
|
|
6
|
+
import registerSkillScopeHook from "./src/cspl/skill_scope_hook.js";
|
|
5
7
|
import { setXYRuntime } from "./src/runtime.js";
|
|
6
8
|
import { markCronToolCall, clearCronToolCall, getCurrentSessionContext } from "./src/tools/session-manager.js";
|
|
7
9
|
import { configManager } from "./src/utils/config-manager.js";
|
|
@@ -11,7 +13,10 @@ import { registerSelfEvolutionToolResultNudge } from "./src/self-evolution-tool-
|
|
|
11
13
|
import { createBeforePromptBuildHandler } from "./src/skill-retriever/hooks.js";
|
|
12
14
|
import { normalizeToolRetrieverConfig } from "./src/skill-retriever/config.js";
|
|
13
15
|
import { registerCLIHook } from "./src/tools/hmos-cli.js";
|
|
16
|
+
import { recoverCronState } from "./src/cron-recovery.js";
|
|
14
17
|
import { writeSkillUsage } from "./src/utils/skills-logger.js";
|
|
18
|
+
import { markSubagentSpawned, markSubagentEnded, getCachedXYConfig, } from "./src/subagent-wait-state.js";
|
|
19
|
+
import { logger } from "./src/utils/logger.js";
|
|
15
20
|
/**
|
|
16
21
|
* Parse a file path string to detect if it refers to a SKILL.md file within
|
|
17
22
|
* a skills directory. Returns the skill name (parent directory) if so.
|
|
@@ -205,7 +210,121 @@ function readJobIdFromResult(result) {
|
|
|
205
210
|
}
|
|
206
211
|
return undefined;
|
|
207
212
|
}
|
|
213
|
+
// ── Gateway startup: cron state recovery ────────────────────────────────────
|
|
214
|
+
/**
|
|
215
|
+
* Register the gateway_start hook for cron state recovery.
|
|
216
|
+
*
|
|
217
|
+
* On gateway startup, checks .openclaw/cron/ for legacy JSON/JSONL files
|
|
218
|
+
* and migrates them into the SQLite state database:
|
|
219
|
+
* - Reads legacy jobs.json + jobs-state.json → imports into cron_jobs table
|
|
220
|
+
* - Reads legacy runs/*.jsonl → imports into cron_run_logs table
|
|
221
|
+
* - Archives migrated files with .migrated suffix
|
|
222
|
+
*
|
|
223
|
+
* Pattern follows legacy-store-migration.ts and legacy-run-log-migration.ts:
|
|
224
|
+
* check → load → import into SQLite → archive old files.
|
|
225
|
+
*/
|
|
226
|
+
function registerCronRecoveryHook(api) {
|
|
227
|
+
api.on("gateway_start", async (_event, _ctx) => {
|
|
228
|
+
const logTag = "[CRON-RECOVERY-HOOK]";
|
|
229
|
+
const startTime = Date.now();
|
|
230
|
+
logger.log(`${logTag} ═══════════════════════════════════════════`);
|
|
231
|
+
logger.log(`${logTag} gateway_start fired — checking for legacy cron files`);
|
|
232
|
+
logger.log(`${logTag} Timestamp: ${new Date().toISOString()}`);
|
|
233
|
+
logger.log(`${logTag} Plugin registration mode: ${api.registrationMode ?? "unknown"}`);
|
|
234
|
+
let result;
|
|
235
|
+
try {
|
|
236
|
+
result = await recoverCronState();
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
240
|
+
const errStack = err instanceof Error ? err.stack : undefined;
|
|
241
|
+
logger.error(`${logTag} cron state recovery threw: ${errMsg}`);
|
|
242
|
+
if (errStack)
|
|
243
|
+
logger.error(`${logTag} Stack: ${errStack}`);
|
|
244
|
+
// Don't let a recovery failure block gateway startup.
|
|
245
|
+
logger.log(`${logTag} Recovery failed after ${Date.now() - startTime}ms — gateway startup continues`);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const elapsed = Date.now() - startTime;
|
|
249
|
+
if (result.recovered) {
|
|
250
|
+
logger.log(`${logTag} ✅ Migration performed successfully in ${elapsed}ms:` +
|
|
251
|
+
` storeMigrated=${result.storeMigrated}, ` +
|
|
252
|
+
` runLogFilesImported=${result.runLogFilesImported}`);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
logger.log(`${logTag} ℹ️ No legacy cron files migrated in ${elapsed}ms ` +
|
|
256
|
+
`(nothing to migrate or database unavailable)`);
|
|
257
|
+
}
|
|
258
|
+
// Log diagnostics summary
|
|
259
|
+
const warnings = result.diagnostics.filter((d) => d.includes("skipping") || d.includes("locked") || d.includes("unavailable"));
|
|
260
|
+
const errors = result.diagnostics.filter((d) => d.includes("error") || d.includes("failed") || d.includes("Failed"));
|
|
261
|
+
if (warnings.length > 0) {
|
|
262
|
+
logger.warn(`${logTag} ${warnings.length} warning(s) from migration:`);
|
|
263
|
+
for (const w of warnings) {
|
|
264
|
+
logger.warn(`${logTag} ⚠ ${w}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (errors.length > 0) {
|
|
268
|
+
logger.error(`${logTag} ${errors.length} error(s) from migration:`);
|
|
269
|
+
for (const e of errors) {
|
|
270
|
+
logger.error(`${logTag} ✗ ${e}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (warnings.length === 0 && errors.length === 0) {
|
|
274
|
+
logger.log(`${logTag} All diagnostics clean, no warnings or errors`);
|
|
275
|
+
}
|
|
276
|
+
logger.log(`${logTag} ═══════════════════════════════════════════`);
|
|
277
|
+
});
|
|
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
|
+
}
|
|
208
325
|
function registerFullHooks(api) {
|
|
326
|
+
// SUBAGENT HOOKS: track subagent spawn/end lifecycle for session keep-alive
|
|
327
|
+
registerSubagentHooks(api);
|
|
209
328
|
// SKILL RETRIEVER HOOK: before_prompt_build hook
|
|
210
329
|
const pluginConfig = api.pluginConfig || {};
|
|
211
330
|
const skillRetrieverConfig = normalizeToolRetrieverConfig({
|
|
@@ -216,10 +335,13 @@ function registerFullHooks(api) {
|
|
|
216
335
|
timeoutMs: pluginConfig.skillRetrieverTimeoutMs ?? 1000,
|
|
217
336
|
});
|
|
218
337
|
const beforePromptBuildHandler = createBeforePromptBuildHandler(skillRetrieverConfig);
|
|
219
|
-
api.on("before_prompt_build",
|
|
338
|
+
api.on("before_prompt_build", async (event, ctx) => {
|
|
339
|
+
logger.log(`[BEFORE_PROMPT_BUILD] hook fired, sessionKey=${ctx.sessionKey || "undefined"}, sessionId=${ctx.sessionId || "undefined"}`);
|
|
340
|
+
return beforePromptBuildHandler(event, ctx);
|
|
341
|
+
});
|
|
220
342
|
registerSelfEvolutionToolResultNudge(api);
|
|
221
343
|
}
|
|
222
|
-
const
|
|
344
|
+
const plugin = definePluginEntry({
|
|
223
345
|
id: "xiaoyi-channel",
|
|
224
346
|
name: "Xiaoyi Channel",
|
|
225
347
|
description: "Xiaoyi channel plugin - Xiaoyi A2A protocol integration",
|
|
@@ -227,6 +349,10 @@ const pluginEntry = definePluginEntry({
|
|
|
227
349
|
// Always register the provider so wrapStreamFn/prepareExtraParams work
|
|
228
350
|
// in ALL registration modes (not just "full").
|
|
229
351
|
api.registerProvider(xiaoyiProvider);
|
|
352
|
+
// Register the compaction provider so openclaw's safeguard hook uses
|
|
353
|
+
// our summarization path (which injects x-hag-trace-id) instead of the
|
|
354
|
+
// built-in LLM path that bypasses wrapStreamFn.
|
|
355
|
+
api.registerCompactionProvider(xiaoyiCompactionProvider);
|
|
230
356
|
if (api.registrationMode === "cli-metadata") {
|
|
231
357
|
return;
|
|
232
358
|
}
|
|
@@ -244,13 +370,17 @@ const pluginEntry = definePluginEntry({
|
|
|
244
370
|
registerFullHooks(api);
|
|
245
371
|
// CSPL sentinel hook: before_tool_call + after_tool_call security scanning
|
|
246
372
|
registerSentinelHook(api);
|
|
373
|
+
// CSPL skill scope hook: before_install security scanning
|
|
374
|
+
registerSkillScopeHook(api);
|
|
247
375
|
// Cron detection hook: marks toolCallIds from cron sessions
|
|
248
376
|
registerCronDetectionHook(api);
|
|
249
377
|
// CLI exec hook: intercepts built-in exec for HarmonyOS CLI skill tools
|
|
250
378
|
registerCLIHook(api);
|
|
379
|
+
// Cron recovery hook: prunes stale cron-push-map and pushData on gateway startup
|
|
380
|
+
registerCronRecoveryHook(api);
|
|
251
381
|
// Skills diagnostic hook: log skill usage (detected via SKILL.md reads)
|
|
252
382
|
registerSkillsDiagnosticHook(api);
|
|
253
383
|
}
|
|
254
384
|
},
|
|
255
385
|
});
|
|
256
|
-
export default
|
|
386
|
+
export default plugin;
|
package/dist/src/bot.js
CHANGED
|
@@ -15,7 +15,9 @@ import { selfEvolutionManager } from "./utils/self-evolution-manager.js";
|
|
|
15
15
|
import { saveRuntimeInfo } from "./utils/runtime-manager.js";
|
|
16
16
|
import { toolCallNudgeManager } from "./utils/tool-call-nudge-manager.js";
|
|
17
17
|
import { setCsplSteerContext } from "./cspl/steer-context.js";
|
|
18
|
+
import { resolveActiveEmbeddedRunSessionId, queueAgentHarnessMessage, } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
18
19
|
import { registerTaskId, decrementTaskIdRef, hasActiveTask, } from "./task-manager.js";
|
|
20
|
+
import { registerSessionKeyMapping, getWaitState, hasWaitState, markParentSettled, cacheXYConfig, } from "./subagent-wait-state.js";
|
|
19
21
|
import { logger } from "./utils/logger.js";
|
|
20
22
|
/**
|
|
21
23
|
* Handle an incoming A2A message.
|
|
@@ -164,6 +166,7 @@ export async function handleXYMessage(params) {
|
|
|
164
166
|
const runCrossTaskContext = extractRunCrossTaskContext(parsed.parts);
|
|
165
167
|
// Resolve configuration (needed for status updates)
|
|
166
168
|
const config = resolveXYConfig(cfg);
|
|
169
|
+
cacheXYConfig(config);
|
|
167
170
|
// ✅ Resolve agent route (following feishu pattern)
|
|
168
171
|
// accountId is "default" for XY (single account mode)
|
|
169
172
|
// Use sessionId as peer.id to ensure all messages in the same session share context
|
|
@@ -177,6 +180,16 @@ export async function handleXYMessage(params) {
|
|
|
177
180
|
},
|
|
178
181
|
});
|
|
179
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
|
+
}
|
|
180
193
|
// Check for ACP runtime binding on this A2A conversation
|
|
181
194
|
const runtimeRoute = resolveRuntimeConversationBindingRoute({
|
|
182
195
|
route,
|
|
@@ -241,20 +254,18 @@ export async function handleXYMessage(params) {
|
|
|
241
254
|
log.error(`[BOT] Failed to patch session model override:`, patchErr);
|
|
242
255
|
}
|
|
243
256
|
}
|
|
244
|
-
// 🔑
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
});
|
|
257
|
-
}
|
|
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
|
+
});
|
|
258
269
|
}
|
|
259
270
|
// Extract text and files from parts
|
|
260
271
|
const text = extractTextFromParts(parsed.parts);
|
|
@@ -280,10 +291,8 @@ export async function handleXYMessage(params) {
|
|
|
280
291
|
}
|
|
281
292
|
}
|
|
282
293
|
// ── Build message and dispatch via auto-reply pipeline ──────────
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
// does not need its own steer detection or polling logic.
|
|
286
|
-
// File download
|
|
294
|
+
// 首条消息和 steer 回退都走此统一 dispatch 路径。
|
|
295
|
+
// File download — for both first message and steer fallthrough
|
|
287
296
|
let mediaPayload = {};
|
|
288
297
|
if (!skipReg || isUpdate) {
|
|
289
298
|
const fileParts = extractFileParts(parsed.parts);
|
|
@@ -299,13 +308,35 @@ export async function handleXYMessage(params) {
|
|
|
299
308
|
textForAgent = `${textForAgent}${fileHint}`;
|
|
300
309
|
log.log(`[BOT] Steer: appended file paths to text`);
|
|
301
310
|
}
|
|
311
|
+
// 🔑 Direct steer: bypass dispatchReplyFromConfig entirely and inject the
|
|
312
|
+
// message directly into the active embedded agent run. This avoids the
|
|
313
|
+
// per-session ReplyOperation lock in admitReplyTurn which would otherwise
|
|
314
|
+
// block the steer message until the first message completes — making steer
|
|
315
|
+
// indistinguishable from a followup.
|
|
316
|
+
if (isUpdate && !skipReg && route.sessionKey) {
|
|
317
|
+
const activeSessionId = resolveActiveEmbeddedRunSessionId(route.sessionKey);
|
|
318
|
+
if (activeSessionId) {
|
|
319
|
+
log.log(`[BOT-STEER] Direct steer attempt: activeSessionId=${activeSessionId}, textLen=${textForAgent.length}`);
|
|
320
|
+
const queued = queueAgentHarnessMessage(activeSessionId, textForAgent, {
|
|
321
|
+
steeringMode: "all",
|
|
322
|
+
});
|
|
323
|
+
if (queued) {
|
|
324
|
+
log.log(`[BOT-STEER] Direct steer succeeded — message injected into active run`);
|
|
325
|
+
// Steer message taskId refCount is no longer needed since we skip the dispatcher.
|
|
326
|
+
decrementTaskIdRef(parsed.sessionId);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
log.log(`[BOT-STEER] Direct steer failed (queued=false), falling through to dispatchReplyFromConfig`);
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
log.log(`[BOT-STEER] No active embedded run session for key=${route.sessionKey}, falling through to dispatchReplyFromConfig`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
302
335
|
// Resolve envelope format options (following feishu pattern)
|
|
303
336
|
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
304
337
|
// Build message body with speaker prefix (following feishu pattern)
|
|
305
|
-
let messageBody = textForAgent;
|
|
306
|
-
// Add speaker prefix for clarity
|
|
307
338
|
const speaker = parsed.sessionId;
|
|
308
|
-
messageBody = `${speaker}: ${
|
|
339
|
+
const messageBody = `${speaker}: ${textForAgent}`;
|
|
309
340
|
// Format agent envelope (following feishu pattern)
|
|
310
341
|
const body = core.channel.reply.formatAgentEnvelope({
|
|
311
342
|
channel: "xiaoyi-channel",
|
|
@@ -314,12 +345,20 @@ export async function handleXYMessage(params) {
|
|
|
314
345
|
envelope: envelopeOptions,
|
|
315
346
|
body: messageBody,
|
|
316
347
|
});
|
|
348
|
+
// 🔑 Steer messages use /steer prefix + CommandSource "native" to trigger
|
|
349
|
+
// the native slash command fast path, which calls handleSteerCommand →
|
|
350
|
+
// queueEmbeddedAgentMessageWithOutcomeAsync before admitReplyTurn blocks.
|
|
351
|
+
const steerCommandBody = isUpdate ? `/steer ${textForAgent}` : textForAgent;
|
|
352
|
+
if (isUpdate) {
|
|
353
|
+
log.log(`[BOT-STEER] Dispatching via /steer fast path, sessionKey=${route.sessionKey}, cmdLen=${steerCommandBody.length}`);
|
|
354
|
+
}
|
|
317
355
|
// ✅ Finalize inbound context (following feishu pattern)
|
|
318
356
|
// Use route.accountId and route.sessionKey instead of parsed fields
|
|
319
357
|
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
320
358
|
Body: body,
|
|
321
|
-
RawBody:
|
|
322
|
-
CommandBody:
|
|
359
|
+
RawBody: steerCommandBody,
|
|
360
|
+
CommandBody: steerCommandBody,
|
|
361
|
+
...(isUpdate ? { CommandSource: "native" } : {}),
|
|
323
362
|
From: parsed.sessionId,
|
|
324
363
|
To: parsed.sessionId, // ✅ Simplified: use sessionId as target (context is managed by SessionKey)
|
|
325
364
|
SessionKey: route.sessionKey, // ✅ Use route.sessionKey
|
|
@@ -343,15 +382,33 @@ export async function handleXYMessage(params) {
|
|
|
343
382
|
// response and cleanup — the first message's dispatcher handles those.
|
|
344
383
|
const steerState = { steered: isUpdate };
|
|
345
384
|
// 🔑 创建dispatcher
|
|
346
|
-
log.log(`[BOT-DISPATCHER] Creating reply dispatcher`);
|
|
385
|
+
log.log(`[BOT-DISPATCHER] Creating reply dispatcher, isSteer=${isUpdate}, sessionKey=${route.sessionKey}`);
|
|
347
386
|
// Cleanup: 必须在 onIdle 内部执行(参见 reply-dispatcher.ts 中 onIdleComplete 的注释)
|
|
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.
|
|
348
391
|
let cleaned = false;
|
|
349
392
|
const cleanup = () => {
|
|
350
393
|
if (cleaned)
|
|
351
394
|
return;
|
|
352
395
|
cleaned = true;
|
|
353
|
-
log.log(`[BOT] Cleanup started`);
|
|
354
|
-
|
|
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
|
+
}
|
|
407
|
+
decrementTaskIdRef(parsed.sessionId);
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
log.log(`[BOT] CSPL steer cleanup, skipping decrementTaskIdRef (never incremented)`);
|
|
411
|
+
}
|
|
355
412
|
log.log(`[BOT] Cleanup completed`);
|
|
356
413
|
};
|
|
357
414
|
const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval, updateFallbackTaskId } = createXYReplyDispatcher({
|
|
@@ -388,13 +445,34 @@ export async function handleXYMessage(params) {
|
|
|
388
445
|
log.log(`[BOT-DISPATCH] withReplyDispatcher starting, sessionKey=${route.sessionKey}`);
|
|
389
446
|
await core.channel.reply.withReplyDispatcher({
|
|
390
447
|
dispatcher,
|
|
391
|
-
onSettled: () => {
|
|
448
|
+
onSettled: async () => {
|
|
392
449
|
log.log(`[BOT] onSettled, steered=${steerState.steered}`);
|
|
393
450
|
// 🔑 When steered, skip cleanup — the first message's dispatcher is still running
|
|
394
451
|
if (steerState.steered) {
|
|
395
452
|
log.log(`[BOT] Steered dispatch settled, skipping cleanup`);
|
|
396
453
|
return;
|
|
397
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
|
+
}
|
|
398
476
|
// cleanup 已由 onIdleComplete 在 onIdle 的 finally 中执行。
|
|
399
477
|
// onSettled 不做任何清理(直接在这里清理会发生 race condition)。
|
|
400
478
|
dispatcherUpdaters.delete(parsed.sessionId);
|
|
@@ -406,7 +484,8 @@ export async function handleXYMessage(params) {
|
|
|
406
484
|
// signal init complete to release the global dispatch gate
|
|
407
485
|
// for the next session.
|
|
408
486
|
const dispatchPromise = runWithSessionContext(sessionContext, async () => {
|
|
409
|
-
|
|
487
|
+
const isSteerDispatch = isUpdate && !skipReg;
|
|
488
|
+
log.log(`[ALS-PROOF] bot entered dispatch scope sessionId=${sessionContext.sessionId} taskId=${sessionContext.taskId} isSteer=${isSteerDispatch}`);
|
|
410
489
|
log.log(`[BOT-DISPATCH] dispatchReplyFromConfig starting, body.length=${ctxPayload.Body?.length ?? 0}`);
|
|
411
490
|
try {
|
|
412
491
|
const result = await core.channel.reply.dispatchReplyFromConfig({
|
|
@@ -466,19 +545,21 @@ export async function handleXYMessage(params) {
|
|
|
466
545
|
function buildXYMediaPayload(mediaList) {
|
|
467
546
|
const first = mediaList[0];
|
|
468
547
|
const mediaPaths = mediaList.map((media) => media.path);
|
|
548
|
+
const mediaUrls = mediaList.map((media) => media.uri);
|
|
469
549
|
const mediaTypes = mediaList.map((media) => media.mimeType).filter(Boolean);
|
|
470
550
|
return {
|
|
471
551
|
MediaPath: first?.path,
|
|
472
552
|
MediaType: first?.mimeType,
|
|
553
|
+
MediaUrl: first?.uri,
|
|
473
554
|
MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
|
|
555
|
+
MediaUrls: mediaUrls.length > 0 ? mediaUrls : undefined,
|
|
474
556
|
MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
|
|
475
557
|
};
|
|
476
558
|
}
|
|
477
559
|
// ─────────────────────────────────────────────────────────────
|
|
478
560
|
// Dispatcher updaters (cross-chain taskId bridging)
|
|
479
561
|
// ─────────────────────────────────────────────────────────────
|
|
480
|
-
// Use globalThis to survive module deduplication
|
|
481
|
-
// different copy of bot.ts, so a plain module-level Map would be two objects.
|
|
562
|
+
// Use globalThis to survive module deduplication
|
|
482
563
|
const _g = globalThis;
|
|
483
564
|
if (!_g.__xyDispatcherUpdaters)
|
|
484
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.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
interface CompactionConfig {
|
|
2
|
+
uid: string;
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
modelName: string;
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Snapshot of the resolved session context, captured from the last normal
|
|
9
|
+
* LLM request before compaction is triggered. When available, the
|
|
10
|
+
* CompactionProvider reuses the same A2A traceId/sessionId so the
|
|
11
|
+
* compaction summarization request is linked to the same user session
|
|
12
|
+
* in backend tracing.
|
|
13
|
+
*/
|
|
14
|
+
interface CompactionSessionSnapshot {
|
|
15
|
+
traceId: string;
|
|
16
|
+
sessionId: string;
|
|
17
|
+
interactionId: string;
|
|
18
|
+
deviceType?: string;
|
|
19
|
+
appVer?: string;
|
|
20
|
+
sdkApiVersion?: string;
|
|
21
|
+
/** Auth headers from options.headers in the last normal request. */
|
|
22
|
+
requestHeaders?: Record<string, string>;
|
|
23
|
+
/** API key from the last normal request (for Bearer token auth). */
|
|
24
|
+
apiKey?: string;
|
|
25
|
+
/** Model-level headers from resolveDynamicModel (streamSimple adds these). */
|
|
26
|
+
modelHeaders?: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
/** Store config captured from prepareExtraParams so summarize() can read it. */
|
|
29
|
+
export declare function setCompactionConfig(config: CompactionConfig | null): void;
|
|
30
|
+
/**
|
|
31
|
+
* Store a snapshot of the resolved session headers captured from the
|
|
32
|
+
* most recent wrapStreamFn call. This lets the CompactionProvider reuse
|
|
33
|
+
* the A2A traceId/sessionId during compaction summarization.
|
|
34
|
+
*/
|
|
35
|
+
export declare function setCompactionSessionSnapshot(snapshot: CompactionSessionSnapshot | null): void;
|
|
36
|
+
export declare const xiaoyiCompactionProvider: {
|
|
37
|
+
id: string;
|
|
38
|
+
label: string;
|
|
39
|
+
summarize(params: {
|
|
40
|
+
messages: unknown[];
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
customInstructions?: string;
|
|
43
|
+
summarizationInstructions?: {
|
|
44
|
+
identifierPolicy?: string;
|
|
45
|
+
identifierInstructions?: string;
|
|
46
|
+
};
|
|
47
|
+
previousSummary?: string;
|
|
48
|
+
}): Promise<string>;
|
|
49
|
+
};
|
|
50
|
+
export {};
|