@ynhcj/xiaoyi-channel 0.0.200-beta → 0.0.200-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 -8
- package/dist/index.js +47 -4
- package/dist/src/bot.d.ts +0 -5
- package/dist/src/bot.js +98 -174
- package/dist/src/channel.js +83 -64
- package/dist/src/client.d.ts +5 -0
- package/dist/src/client.js +10 -0
- package/dist/src/cspl/call_api.d.ts +1 -1
- package/dist/src/cspl/call_api.js +10 -10
- package/dist/src/cspl/config.d.ts +0 -1
- package/dist/src/cspl/config.js +68 -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 +33 -28
- package/dist/src/cspl/upload_file.js +5 -6
- package/dist/src/cspl/utils.d.ts +20 -9
- package/dist/src/cspl/utils.js +119 -75
- package/dist/src/formatter.js +21 -31
- package/dist/src/log-reporter/index.d.ts +1 -1
- package/dist/src/log-reporter/index.js +164 -35
- package/dist/src/log-reporter/openclaw-parser.d.ts +18 -0
- package/dist/src/log-reporter/openclaw-parser.js +94 -0
- package/dist/src/log-reporter/path-resolver.d.ts +5 -0
- package/dist/src/log-reporter/path-resolver.js +50 -0
- package/dist/src/log-reporter/reporter.d.ts +4 -4
- package/dist/src/log-reporter/reporter.js +52 -13
- package/dist/src/log-reporter/scanner.d.ts +7 -3
- package/dist/src/log-reporter/scanner.js +3 -7
- package/dist/src/log-reporter/types.d.ts +26 -27
- package/dist/src/log-reporter/uploader.d.ts +4 -3
- package/dist/src/log-reporter/uploader.js +32 -9
- package/dist/src/monitor.js +19 -9
- package/dist/src/outbound.js +5 -0
- package/dist/src/provider.js +50 -20
- package/dist/src/reply-dispatcher.d.ts +3 -0
- package/dist/src/reply-dispatcher.js +158 -159
- package/dist/src/self-evolution-handler.js +2 -1
- package/dist/src/skill-retriever/config.js +2 -0
- package/dist/src/skill-retriever/hooks.js +1 -0
- package/dist/src/skill-retriever/tool-search.d.ts +3 -0
- package/dist/src/skill-retriever/tool-search.js +27 -11
- package/dist/src/skill-retriever/types.d.ts +1 -0
- package/dist/src/subagent-wait-state.d.ts +66 -0
- package/dist/src/subagent-wait-state.js +226 -0
- package/dist/src/task-manager.d.ts +5 -6
- package/dist/src/task-manager.js +5 -9
- package/dist/src/tools/agent-as-skill-tool.d.ts +41 -2
- package/dist/src/tools/agent-as-skill-tool.js +144 -146
- package/dist/src/tools/calendar-tool.d.ts +24 -2
- package/dist/src/tools/calendar-tool.js +115 -117
- package/dist/src/tools/call-device-tool.d.ts +20 -6
- package/dist/src/tools/call-device-tool.js +116 -138
- package/dist/src/tools/call-phone-tool.d.ts +21 -2
- package/dist/src/tools/call-phone-tool.js +112 -114
- package/dist/src/tools/check-plugin-privilege-tool.d.ts +16 -2
- package/dist/src/tools/check-plugin-privilege-tool.js +141 -143
- package/dist/src/tools/create-alarm-tool.d.ts +39 -2
- package/dist/src/tools/create-alarm-tool.js +229 -231
- package/dist/src/tools/create-all-tools.js +0 -2
- package/dist/src/tools/delete-alarm-tool.d.ts +15 -2
- package/dist/src/tools/delete-alarm-tool.js +134 -136
- package/dist/src/tools/device-tool-map.js +0 -7
- package/dist/src/tools/discover-cross-devices-tool.d.ts +16 -2
- package/dist/src/tools/discover-cross-devices-tool.js +121 -124
- package/dist/src/tools/display-a2ui-card-bypath-tool.d.ts +21 -2
- package/dist/src/tools/display-a2ui-card-bypath-tool.js +53 -56
- package/dist/src/tools/get-alarm-tool-schema.d.ts +1 -2
- package/dist/src/tools/get-alarm-tool-schema.js +10 -16
- package/dist/src/tools/get-calendar-tool-schema.d.ts +1 -2
- package/dist/src/tools/get-calendar-tool-schema.js +8 -12
- package/dist/src/tools/get-collection-tool-schema.d.ts +1 -2
- package/dist/src/tools/get-collection-tool-schema.js +9 -11
- package/dist/src/tools/get-contact-tool-schema.d.ts +1 -2
- package/dist/src/tools/get-contact-tool-schema.js +10 -16
- package/dist/src/tools/get-device-file-tool-schema.d.ts +1 -2
- package/dist/src/tools/get-device-file-tool-schema.js +9 -13
- package/dist/src/tools/get-email-tool-schema.d.ts +1 -2
- package/dist/src/tools/get-email-tool-schema.js +8 -11
- package/dist/src/tools/get-note-tool-schema.d.ts +1 -2
- package/dist/src/tools/get-note-tool-schema.js +9 -14
- package/dist/src/tools/get-photo-tool-schema.d.ts +1 -2
- package/dist/src/tools/get-photo-tool-schema.js +9 -12
- package/dist/src/tools/hmos-cli.d.ts +109 -0
- package/dist/src/tools/hmos-cli.js +558 -0
- package/dist/src/tools/image-reading-tool.d.ts +28 -2
- package/dist/src/tools/image-reading-tool.js +76 -76
- package/dist/src/tools/invoke.d.ts +88 -0
- package/dist/src/tools/invoke.js +1209 -0
- package/dist/src/tools/location-tool.d.ts +11 -2
- package/dist/src/tools/location-tool.js +93 -92
- package/dist/src/tools/login-token-tool.d.ts +20 -2
- package/dist/src/tools/login-token-tool.js +123 -125
- package/dist/src/tools/modify-alarm-tool.d.ts +47 -2
- package/dist/src/tools/modify-alarm-tool.js +250 -252
- package/dist/src/tools/modify-note-tool.d.ts +20 -2
- package/dist/src/tools/modify-note-tool.js +107 -109
- package/dist/src/tools/note-tool.d.ts +20 -2
- package/dist/src/tools/note-tool.js +106 -108
- package/dist/src/tools/query-app-message-tool.d.ts +28 -2
- package/dist/src/tools/query-app-message-tool.js +111 -113
- package/dist/src/tools/query-memory-data-tool.d.ts +28 -2
- package/dist/src/tools/query-memory-data-tool.js +112 -114
- package/dist/src/tools/query-todo-task-tool.d.ts +24 -2
- package/dist/src/tools/query-todo-task-tool.js +106 -108
- package/dist/src/tools/save-file-to-phone-tool.d.ts +24 -2
- package/dist/src/tools/save-file-to-phone-tool.js +130 -132
- package/dist/src/tools/save-media-to-gallery-tool.d.ts +24 -2
- package/dist/src/tools/save-media-to-gallery-tool.js +137 -139
- package/dist/src/tools/save-self-evolution-skill-tool.d.ts +54 -2
- package/dist/src/tools/save-self-evolution-skill-tool.js +194 -194
- package/dist/src/tools/search-alarm-tool.d.ts +34 -2
- package/dist/src/tools/search-alarm-tool.js +174 -176
- package/dist/src/tools/search-calendar-tool.d.ts +24 -2
- package/dist/src/tools/search-calendar-tool.js +148 -150
- package/dist/src/tools/search-contact-tool.d.ts +16 -2
- package/dist/src/tools/search-contact-tool.js +101 -103
- package/dist/src/tools/search-email-tool.d.ts +21 -2
- package/dist/src/tools/search-email-tool.js +110 -112
- package/dist/src/tools/search-file-tool.d.ts +16 -2
- package/dist/src/tools/search-file-tool.js +104 -106
- package/dist/src/tools/search-message-tool.d.ts +16 -2
- package/dist/src/tools/search-message-tool.js +103 -105
- package/dist/src/tools/search-note-tool.d.ts +16 -2
- package/dist/src/tools/search-note-tool.js +98 -100
- package/dist/src/tools/search-photo-gallery-tool.d.ts +21 -2
- package/dist/src/tools/search-photo-gallery-tool.js +35 -37
- package/dist/src/tools/send-cross-device-task-tool.d.ts +35 -2
- package/dist/src/tools/send-cross-device-task-tool.js +140 -143
- package/dist/src/tools/send-email-tool.d.ts +24 -2
- package/dist/src/tools/send-email-tool.js +108 -110
- package/dist/src/tools/send-file-to-user-tool.d.ts +20 -2
- package/dist/src/tools/send-file-to-user-tool.js +174 -176
- package/dist/src/tools/send-html-card-tool.d.ts +20 -2
- package/dist/src/tools/send-html-card-tool.js +85 -87
- package/dist/src/tools/send-message-tool.d.ts +20 -2
- package/dist/src/tools/send-message-tool.js +122 -124
- package/dist/src/tools/session-manager.d.ts +7 -50
- package/dist/src/tools/session-manager.js +40 -231
- package/dist/src/tools/upload-file-tool.d.ts +20 -2
- package/dist/src/tools/upload-file-tool.js +80 -82
- package/dist/src/tools/upload-photo-tool.d.ts +20 -2
- package/dist/src/tools/upload-photo-tool.js +68 -70
- package/dist/src/tools/xiaoyi-add-collection-tool.d.ts +32 -2
- package/dist/src/tools/xiaoyi-add-collection-tool.js +146 -148
- package/dist/src/tools/xiaoyi-collection-tool.d.ts +20 -2
- package/dist/src/tools/xiaoyi-collection-tool.js +114 -116
- package/dist/src/tools/xiaoyi-delete-collection-tool.d.ts +15 -2
- package/dist/src/tools/xiaoyi-delete-collection-tool.js +127 -129
- package/dist/src/tools/xiaoyi-gui-tool.d.ts +16 -2
- package/dist/src/tools/xiaoyi-gui-tool.js +92 -95
- package/dist/src/utils/logger.js +3 -14
- package/dist/src/utils/skills-logger.d.ts +5 -0
- package/dist/src/utils/skills-logger.js +76 -0
- package/dist/src/websocket.d.ts +1 -1
- package/dist/src/websocket.js +11 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
@@ -3,13 +3,51 @@ import { xiaoyiProvider } from "./src/provider.js";
|
|
|
3
3
|
import { xyPlugin } from "./src/channel.js";
|
|
4
4
|
import registerSentinelHook from "./src/cspl/sentinel_hook.js";
|
|
5
5
|
import { setXYRuntime } from "./src/runtime.js";
|
|
6
|
-
import { markCronToolCall, clearCronToolCall,
|
|
6
|
+
import { markCronToolCall, clearCronToolCall, getCurrentSessionContext } from "./src/tools/session-manager.js";
|
|
7
7
|
import { configManager } from "./src/utils/config-manager.js";
|
|
8
8
|
import { setJobPushId } from "./src/utils/cron-push-map.js";
|
|
9
9
|
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";
|
|
14
|
+
import { writeSkillUsage } from "./src/utils/skills-logger.js";
|
|
15
|
+
/**
|
|
16
|
+
* Parse a file path string to detect if it refers to a SKILL.md file within
|
|
17
|
+
* a skills directory. Returns the skill name (parent directory) if so.
|
|
18
|
+
*
|
|
19
|
+
* Matches paths like:
|
|
20
|
+
* ~/.openclaw/workspace/skills/my-skill/SKILL.md
|
|
21
|
+
* /home/user/core_skills/my-skill/SKILL.md
|
|
22
|
+
* skills/my-skill/SKILL.md
|
|
23
|
+
*/
|
|
24
|
+
function extractSkillNameFromPath(filePath) {
|
|
25
|
+
if (typeof filePath !== "string" || !filePath)
|
|
26
|
+
return null;
|
|
27
|
+
// Normalize common path prefixes
|
|
28
|
+
const normalized = filePath.replace(/^~\//, "/home/").replace(/\\/g, "/");
|
|
29
|
+
// Match: .../skills/<skillName>/SKILL.md or .../skills/<skillName>/...
|
|
30
|
+
// Also match: .../core_skills/<skillName>/SKILL.md
|
|
31
|
+
const match = normalized.match(/\/(?:core_)?skills\/([^/]+)\/SKILL\.md$/i);
|
|
32
|
+
return match ? match[1] : null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Register the skills diagnostic event listener via after_tool_call hook.
|
|
36
|
+
*
|
|
37
|
+
* When openclaw fires a `skill.used` diagnostic event, the skill's SKILL.md
|
|
38
|
+
* is typically read by the model first. We detect SKILL.md reads through
|
|
39
|
+
* the `after_tool_call` hook and write the skill name to the skills log.
|
|
40
|
+
*/
|
|
41
|
+
function registerSkillsDiagnosticHook(api) {
|
|
42
|
+
api.on("after_tool_call", async (event, _ctx) => {
|
|
43
|
+
if (event.toolName !== "read")
|
|
44
|
+
return;
|
|
45
|
+
const skillName = extractSkillNameFromPath(event.params?.path);
|
|
46
|
+
if (skillName) {
|
|
47
|
+
writeSkillUsage(skillName);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
13
51
|
/**
|
|
14
52
|
* Register the cron detection hook.
|
|
15
53
|
*
|
|
@@ -54,10 +92,10 @@ async function captureCronAddMapping(event, ctx) {
|
|
|
54
92
|
return;
|
|
55
93
|
}
|
|
56
94
|
console.log(`[CRONMAP] extracted jobId=${jobId}`);
|
|
57
|
-
const sessionCtx =
|
|
95
|
+
const sessionCtx = getCurrentSessionContext();
|
|
58
96
|
const sessionId = sessionCtx?.sessionId;
|
|
59
97
|
if (!sessionId) {
|
|
60
|
-
console.log(`[CRONMAP] skip: no sessionId (
|
|
98
|
+
console.log(`[CRONMAP] skip: no sessionId in ALS scope (ctxFound=${!!sessionCtx})`);
|
|
61
99
|
return;
|
|
62
100
|
}
|
|
63
101
|
const pushId = await resolvePushId(sessionId);
|
|
@@ -181,7 +219,7 @@ function registerFullHooks(api) {
|
|
|
181
219
|
api.on("before_prompt_build", beforePromptBuildHandler);
|
|
182
220
|
registerSelfEvolutionToolResultNudge(api);
|
|
183
221
|
}
|
|
184
|
-
|
|
222
|
+
const pluginEntry = definePluginEntry({
|
|
185
223
|
id: "xiaoyi-channel",
|
|
186
224
|
name: "Xiaoyi Channel",
|
|
187
225
|
description: "Xiaoyi channel plugin - Xiaoyi A2A protocol integration",
|
|
@@ -208,6 +246,11 @@ export default definePluginEntry({
|
|
|
208
246
|
registerSentinelHook(api);
|
|
209
247
|
// Cron detection hook: marks toolCallIds from cron sessions
|
|
210
248
|
registerCronDetectionHook(api);
|
|
249
|
+
// CLI exec hook: intercepts built-in exec for HarmonyOS CLI skill tools
|
|
250
|
+
registerCLIHook(api);
|
|
251
|
+
// Skills diagnostic hook: log skill usage (detected via SKILL.md reads)
|
|
252
|
+
registerSkillsDiagnosticHook(api);
|
|
211
253
|
}
|
|
212
254
|
},
|
|
213
255
|
});
|
|
256
|
+
export default pluginEntry;
|
package/dist/src/bot.d.ts
CHANGED
|
@@ -24,8 +24,3 @@ export interface HandleXYMessageParams {
|
|
|
24
24
|
* Runtime is expected to be validated before calling this function.
|
|
25
25
|
*/
|
|
26
26
|
export declare function handleXYMessage(params: HandleXYMessageParams): Promise<void>;
|
|
27
|
-
/**
|
|
28
|
-
* 由 provider.ts 在 wrapStreamFn 调用时触发。
|
|
29
|
-
* 这是模型 API 被调用的精确时刻,此时 isStreaming 一定为 true。
|
|
30
|
-
*/
|
|
31
|
-
export declare function notifyModelStreaming(sessionId: string): void;
|
package/dist/src/bot.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveRuntimeConversationBindingRoute } from "openclaw/plugin-sdk/conversation-runtime";
|
|
1
2
|
import { updateSessionStoreEntry, updateSessionStore, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
|
2
3
|
import { getXYRuntime } from "./runtime.js";
|
|
3
4
|
import { createXYReplyDispatcher } from "./reply-dispatcher.js";
|
|
@@ -6,7 +7,8 @@ import { downloadFilesFromParts } from "./file-download.js";
|
|
|
6
7
|
import { resolveXYConfig } from "./config.js";
|
|
7
8
|
import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse, sendA2AResponse } from "./formatter.js";
|
|
8
9
|
import { appendSelfEvolutionKeywordNudge, shouldNudgeForSelfEvolutionKeyword, } from "./self-evolution-keyword.js";
|
|
9
|
-
import {
|
|
10
|
+
import { queueAgentHarnessMessage } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
11
|
+
import { runWithSessionContext } from "./tools/session-manager.js";
|
|
10
12
|
import { configManager } from "./utils/config-manager.js";
|
|
11
13
|
import { addPushId } from "./utils/pushid-manager.js";
|
|
12
14
|
import { getPushDataById } from "./utils/pushdata-manager.js";
|
|
@@ -113,6 +115,13 @@ export async function handleXYMessage(params) {
|
|
|
113
115
|
// Steer injections skip taskId registration to avoid overwriting the active taskId
|
|
114
116
|
if (!skipReg) {
|
|
115
117
|
registerTaskId(parsed.sessionId, parsed.taskId, parsed.messageId);
|
|
118
|
+
// 🔑 steer 场景:同步更新活跃 dispatcher 的 fallback taskId/messageId
|
|
119
|
+
if (isUpdate) {
|
|
120
|
+
const updater = dispatcherUpdaters.get(parsed.sessionId);
|
|
121
|
+
if (updater) {
|
|
122
|
+
updater(parsed.taskId, parsed.messageId);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
116
125
|
// Extract and update push_id if present
|
|
117
126
|
const pushId = extractPushId(parsed.parts);
|
|
118
127
|
if (pushId) {
|
|
@@ -169,21 +178,23 @@ export async function handleXYMessage(params) {
|
|
|
169
178
|
},
|
|
170
179
|
});
|
|
171
180
|
log.log(`[BOT] Resolved route, sessionKey=${route.sessionKey}`);
|
|
172
|
-
//
|
|
181
|
+
// Check for ACP runtime binding on this A2A conversation
|
|
182
|
+
const runtimeRoute = resolveRuntimeConversationBindingRoute({
|
|
183
|
+
route,
|
|
184
|
+
channel: "xiaoyi-channel",
|
|
185
|
+
accountId,
|
|
186
|
+
conversationId: parsed.sessionId,
|
|
187
|
+
});
|
|
188
|
+
route = runtimeRoute.route;
|
|
189
|
+
if (runtimeRoute.bindingRecord) {
|
|
190
|
+
log.log(runtimeRoute.boundSessionKey
|
|
191
|
+
? `[BOT] routed via bound conversation ${parsed.sessionId} -> ${runtimeRoute.boundSessionKey}`
|
|
192
|
+
: `[BOT] plugin-bound conversation ${parsed.sessionId}`);
|
|
193
|
+
}
|
|
194
|
+
// ALS only: no registerSession. The sessionContext built below is handed
|
|
195
|
+
// to runWithSessionContext() inside withReplyDispatcher.run, which is the
|
|
196
|
+
// single wrap point for the whole agent turn.
|
|
173
197
|
if (!skipReg) {
|
|
174
|
-
registerSession(route.sessionKey, {
|
|
175
|
-
config,
|
|
176
|
-
sessionId: parsed.sessionId,
|
|
177
|
-
distributionSessionId,
|
|
178
|
-
taskId: parsed.taskId,
|
|
179
|
-
messageId: parsed.messageId,
|
|
180
|
-
agentId: route.accountId,
|
|
181
|
-
deviceType,
|
|
182
|
-
appVer: appVer ?? undefined,
|
|
183
|
-
sdkApiVersion: sdkApiVersion ?? undefined,
|
|
184
|
-
modelName,
|
|
185
|
-
runCrossTaskContext: runCrossTaskContext ?? undefined,
|
|
186
|
-
});
|
|
187
198
|
// 🔑 Sync A2A modelName to OpenClaw session store so that session_status
|
|
188
199
|
// reports the correct model. Without this, session_status returns the
|
|
189
200
|
// configured default model instead of the A2A-specified one.
|
|
@@ -299,9 +310,6 @@ export async function handleXYMessage(params) {
|
|
|
299
310
|
return;
|
|
300
311
|
}
|
|
301
312
|
// ── First message (non-steer) path below ──────────────────────
|
|
302
|
-
// 🔑 立即创建 streaming 信号——必须在文件下载等耗时操作之前,
|
|
303
|
-
// 否则 steer 消息的 dispatchSteerWhenReady 会找不到信号而跳过等待。
|
|
304
|
-
createStreamingSignal(parsed.sessionId);
|
|
305
313
|
// File download — only for real user messages, steer injections have no files
|
|
306
314
|
let mediaPayload = {};
|
|
307
315
|
if (!skipReg) {
|
|
@@ -354,7 +362,17 @@ export async function handleXYMessage(params) {
|
|
|
354
362
|
const steerState = { steered: false };
|
|
355
363
|
// 🔑 创建dispatcher
|
|
356
364
|
log.log(`[BOT-DISPATCHER] Creating reply dispatcher`);
|
|
357
|
-
|
|
365
|
+
// Cleanup: 必须在 onIdle 内部执行(参见 reply-dispatcher.ts 中 onIdleComplete 的注释)
|
|
366
|
+
let cleaned = false;
|
|
367
|
+
const cleanup = () => {
|
|
368
|
+
if (cleaned)
|
|
369
|
+
return;
|
|
370
|
+
cleaned = true;
|
|
371
|
+
log.log(`[BOT] Cleanup started`);
|
|
372
|
+
decrementTaskIdRef(parsed.sessionId);
|
|
373
|
+
log.log(`[BOT] Cleanup completed`);
|
|
374
|
+
};
|
|
375
|
+
const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval, updateFallbackTaskId } = createXYReplyDispatcher({
|
|
358
376
|
cfg,
|
|
359
377
|
runtime,
|
|
360
378
|
sessionId: parsed.sessionId,
|
|
@@ -362,7 +380,10 @@ export async function handleXYMessage(params) {
|
|
|
362
380
|
messageId: parsed.messageId,
|
|
363
381
|
accountId: route.accountId,
|
|
364
382
|
steerState,
|
|
383
|
+
onIdleComplete: cleanup,
|
|
365
384
|
});
|
|
385
|
+
// 🔑 注册 dispatcher 的 fallback taskId 更新函数,供 steer 路径调用
|
|
386
|
+
dispatcherUpdaters.set(parsed.sessionId, updateFallbackTaskId);
|
|
366
387
|
// Steer injections don't need status intervals
|
|
367
388
|
if (!skipReg) {
|
|
368
389
|
startStatusInterval();
|
|
@@ -376,6 +397,8 @@ export async function handleXYMessage(params) {
|
|
|
376
397
|
messageId: parsed.messageId,
|
|
377
398
|
agentId: route.accountId,
|
|
378
399
|
deviceType,
|
|
400
|
+
appVer: appVer ?? undefined,
|
|
401
|
+
sdkApiVersion: sdkApiVersion ?? undefined,
|
|
379
402
|
modelName,
|
|
380
403
|
runCrossTaskContext: runCrossTaskContext ?? undefined,
|
|
381
404
|
};
|
|
@@ -384,15 +407,14 @@ export async function handleXYMessage(params) {
|
|
|
384
407
|
dispatcher,
|
|
385
408
|
onSettled: () => {
|
|
386
409
|
log.log(`[BOT] onSettled, steered=${steerState.steered}`);
|
|
387
|
-
// 🔑 When steered, skip
|
|
410
|
+
// 🔑 When steered, skip cleanup — the first message's dispatcher is still running
|
|
388
411
|
if (steerState.steered) {
|
|
389
412
|
log.log(`[BOT] Steered dispatch settled, skipping cleanup`);
|
|
390
413
|
return;
|
|
391
414
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
log.log(`[BOT] Cleanup completed`);
|
|
415
|
+
// cleanup 已由 onIdleComplete 在 onIdle 的 finally 中执行。
|
|
416
|
+
// onSettled 不做任何清理(直接在这里清理会发生 race condition)。
|
|
417
|
+
dispatcherUpdaters.delete(parsed.sessionId);
|
|
396
418
|
},
|
|
397
419
|
run: () => {
|
|
398
420
|
// 🔐 Use AsyncLocalStorage to provide session context to tools.
|
|
@@ -401,6 +423,7 @@ export async function handleXYMessage(params) {
|
|
|
401
423
|
// signal init complete to release the global dispatch gate
|
|
402
424
|
// for the next session.
|
|
403
425
|
const dispatchPromise = runWithSessionContext(sessionContext, async () => {
|
|
426
|
+
log.log(`[ALS-PROOF] bot entered dispatch scope sessionId=${sessionContext.sessionId} taskId=${sessionContext.taskId} isSteer=false`);
|
|
404
427
|
log.log(`[BOT-DISPATCH] dispatchReplyFromConfig starting, body.length=${ctxPayload.Body?.length ?? 0}`);
|
|
405
428
|
try {
|
|
406
429
|
const result = await core.channel.reply.dispatchReplyFromConfig({
|
|
@@ -433,7 +456,7 @@ export async function handleXYMessage(params) {
|
|
|
433
456
|
errLog.error("Failed to handle XY message:", err);
|
|
434
457
|
runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
|
|
435
458
|
errLog.log(`[BOT] Error occurred, attempting cleanup`);
|
|
436
|
-
// 🔑 错误时也要清理taskId
|
|
459
|
+
// 🔑 错误时也要清理taskId(session 走 ALS,作用域退出自动清理)
|
|
437
460
|
try {
|
|
438
461
|
const params = message.params;
|
|
439
462
|
const sessionId = params?.sessionId;
|
|
@@ -441,18 +464,6 @@ export async function handleXYMessage(params) {
|
|
|
441
464
|
errLog.log(`[BOT] Cleaning up after error`);
|
|
442
465
|
// 清理 taskId
|
|
443
466
|
decrementTaskIdRef(sessionId);
|
|
444
|
-
// 清理 session
|
|
445
|
-
const core = getXYRuntime();
|
|
446
|
-
const route = core.channel.routing.resolveAgentRoute({
|
|
447
|
-
cfg,
|
|
448
|
-
channel: "xiaoyi-channel",
|
|
449
|
-
accountId,
|
|
450
|
-
peer: {
|
|
451
|
-
kind: "direct",
|
|
452
|
-
id: sessionId,
|
|
453
|
-
},
|
|
454
|
-
});
|
|
455
|
-
unregisterSession(route.sessionKey);
|
|
456
467
|
errLog.log(`[BOT] Cleanup completed after error`);
|
|
457
468
|
}
|
|
458
469
|
}
|
|
@@ -480,42 +491,22 @@ function buildXYMediaPayload(mediaList) {
|
|
|
480
491
|
MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
|
|
481
492
|
};
|
|
482
493
|
}
|
|
494
|
+
// ─────────────────────────────────────────────────────────────
|
|
495
|
+
// Steer 串行队列
|
|
496
|
+
// ─────────────────────────────────────────────────────────────
|
|
483
497
|
// Use globalThis to survive module deduplication — provider.ts may load a
|
|
484
498
|
// different copy of bot.ts, so a plain module-level Map would be two objects.
|
|
485
499
|
const _g = globalThis;
|
|
486
|
-
if (!_g.__xyStreamingSignals)
|
|
487
|
-
_g.__xyStreamingSignals = new Map();
|
|
488
500
|
if (!_g.__xySteerQueues)
|
|
489
501
|
_g.__xySteerQueues = new Map();
|
|
490
|
-
|
|
502
|
+
if (!_g.__xyDispatcherUpdaters)
|
|
503
|
+
_g.__xyDispatcherUpdaters = new Map();
|
|
491
504
|
const steerQueues = _g.__xySteerQueues;
|
|
492
|
-
|
|
493
|
-
* 由 provider.ts 在 wrapStreamFn 调用时触发。
|
|
494
|
-
* 这是模型 API 被调用的精确时刻,此时 isStreaming 一定为 true。
|
|
495
|
-
*/
|
|
496
|
-
export function notifyModelStreaming(sessionId) {
|
|
497
|
-
const log = logger.withContext(sessionId, "");
|
|
498
|
-
const signal = streamingSignals.get(sessionId);
|
|
499
|
-
if (signal) {
|
|
500
|
-
// 不删除 signal——后续 steer 需要靠它判断模型已在 streaming。
|
|
501
|
-
// 清理由第一条消息的 onSettled 兜底。
|
|
502
|
-
signal.notify();
|
|
503
|
-
log.log(`[STEER-QUEUE] Model streaming signal fired`);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
function createStreamingSignal(sessionId) {
|
|
507
|
-
const log = logger.withContext(sessionId, "");
|
|
508
|
-
let resolve;
|
|
509
|
-
const promise = new Promise(r => { resolve = r; });
|
|
510
|
-
const signal = { promise, notify: resolve };
|
|
511
|
-
streamingSignals.set(sessionId, signal);
|
|
512
|
-
log.log(`[STEER-QUEUE] Streaming signal created`);
|
|
513
|
-
return signal;
|
|
514
|
-
}
|
|
505
|
+
const dispatcherUpdaters = _g.__xyDispatcherUpdaters;
|
|
515
506
|
/**
|
|
516
507
|
* 将 steer 消息放入 per-session 串行队列。
|
|
517
|
-
*
|
|
518
|
-
* 多个 steer
|
|
508
|
+
* 通过指数退避轮询注入(queueAgentHarnessMessage),直到 Pi agent 接受消息或 run 结束。
|
|
509
|
+
* 多个 steer 按到达顺序串行处理。
|
|
519
510
|
*/
|
|
520
511
|
function enqueueSteer(params) {
|
|
521
512
|
const { sessionId } = params;
|
|
@@ -534,119 +525,52 @@ function enqueueSteer(params) {
|
|
|
534
525
|
});
|
|
535
526
|
return next;
|
|
536
527
|
}
|
|
528
|
+
/**
|
|
529
|
+
* Poll the active embedded run with exponential backoff until the steer message
|
|
530
|
+
* is accepted or the run completes.
|
|
531
|
+
*
|
|
532
|
+
* Replaces the previous signal-based approach (notifyModelStreaming) which fired
|
|
533
|
+
* from wrapStreamFn too early — before the Pi agent had set state.isStreaming=true.
|
|
534
|
+
* queueAgentHarnessMessage internally checks isStreaming(), so we retry until the
|
|
535
|
+
* Pi agent is ready to accept messages.
|
|
536
|
+
*/
|
|
537
537
|
async function dispatchSteerWhenReady(params) {
|
|
538
|
-
const { sessionId,
|
|
538
|
+
const { sessionId, steerText } = params;
|
|
539
539
|
const log = logger.withContext(sessionId, params.parsed.taskId);
|
|
540
|
-
// 1. 等待第一条消息开始 streaming
|
|
541
|
-
// signal 可能尚未创建(第一条消息还在文件下载等耗时操作中),
|
|
542
|
-
// 轮询等待直到 signal 出现,最長等待 ~5 秒。
|
|
543
|
-
let signal = streamingSignals.get(sessionId);
|
|
544
|
-
if (!signal) {
|
|
545
|
-
log.log(`[STEER-QUEUE] Signal not yet created, polling`);
|
|
546
|
-
for (let i = 0; i < 50; i++) {
|
|
547
|
-
await new Promise(r => setTimeout(r, 100));
|
|
548
|
-
signal = streamingSignals.get(sessionId);
|
|
549
|
-
if (signal)
|
|
550
|
-
break;
|
|
551
|
-
if (!hasActiveTask(sessionId)) {
|
|
552
|
-
log.log(`[STEER-QUEUE] First message completed while waiting, skip steer`);
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
if (signal) {
|
|
558
|
-
log.log(`[STEER-QUEUE] Waiting for streaming signal`);
|
|
559
|
-
await signal.promise;
|
|
560
|
-
log.log(`[STEER-QUEUE] Streaming signal received`);
|
|
561
|
-
}
|
|
562
|
-
else {
|
|
563
|
-
// 轮询超时且 hasActiveTask 仍为 true——说明第一条消息可能卡在异常路径,
|
|
564
|
-
// 没有创建 signal。此时 dispatch 会与第一条消息的模型调用并发冲突,放弃。
|
|
565
|
-
log.log(`[STEER-QUEUE] Signal never appeared after polling, skip steer to avoid collision`);
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
// 2. 第一条消息已结束 → 放弃
|
|
569
|
-
if (!hasActiveTask(sessionId)) {
|
|
570
|
-
log.log(`[STEER-QUEUE] First message completed, skip steer`);
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
// 3. 构建 dispatch 上下文并 dispatch /steer
|
|
574
|
-
const core = getXYRuntime();
|
|
575
|
-
const speaker = sessionId;
|
|
576
|
-
// 如果有文件附件,把路径拼到 steer 文本末尾,让模型通过工具读取
|
|
577
540
|
const mediaPaths = params.mediaPayload?.MediaPaths;
|
|
578
541
|
const fileHint = mediaPaths && mediaPaths.length > 0
|
|
579
542
|
? `\n【用户上传附件】:${JSON.stringify(mediaPaths)}`
|
|
580
543
|
: "";
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
});
|
|
614
|
-
const steerState = { steered: true };
|
|
615
|
-
const { dispatcher, replyOptions } = createXYReplyDispatcher({
|
|
616
|
-
cfg: params.cfg,
|
|
617
|
-
runtime: params.runtime,
|
|
618
|
-
sessionId,
|
|
619
|
-
taskId: params.parsed.taskId,
|
|
620
|
-
messageId: params.parsed.messageId,
|
|
621
|
-
accountId: params.route.accountId,
|
|
622
|
-
steerState,
|
|
623
|
-
});
|
|
624
|
-
const sessionContext = {
|
|
625
|
-
config: resolveXYConfig(params.cfg),
|
|
626
|
-
sessionId,
|
|
627
|
-
taskId: params.parsed.taskId,
|
|
628
|
-
messageId: params.parsed.messageId,
|
|
629
|
-
agentId: params.route.accountId,
|
|
630
|
-
deviceType: params.deviceType,
|
|
631
|
-
};
|
|
632
|
-
log.log(`[STEER-QUEUE] Dispatching steer`);
|
|
633
|
-
await core.channel.reply.withReplyDispatcher({
|
|
634
|
-
dispatcher,
|
|
635
|
-
onSettled: () => {
|
|
636
|
-
log.log(`[STEER-QUEUE] Steer dispatch settled`);
|
|
637
|
-
},
|
|
638
|
-
run: () => {
|
|
639
|
-
return runWithSessionContext(sessionContext, async () => {
|
|
640
|
-
const result = await core.channel.reply.dispatchReplyFromConfig({
|
|
641
|
-
ctx: ctxPayload,
|
|
642
|
-
cfg: params.cfg,
|
|
643
|
-
dispatcher,
|
|
644
|
-
replyOptions,
|
|
645
|
-
});
|
|
646
|
-
log.log(`[STEER-QUEUE] dispatch result: ${JSON.stringify(result)}`);
|
|
647
|
-
return result;
|
|
648
|
-
});
|
|
649
|
-
},
|
|
650
|
-
});
|
|
651
|
-
log.log(`[STEER-QUEUE] Steer dispatch completed`);
|
|
544
|
+
const steerMessage = `${steerText}${fileHint}`;
|
|
545
|
+
log.log(`[STEER-QUEUE] Injecting steer message directly into active run`);
|
|
546
|
+
// Exponential backoff polling until:
|
|
547
|
+
// 1. Injection succeeds (queueAgentHarnessMessage returns true)
|
|
548
|
+
// 2. The original run has completed (hasActiveTask returns false)
|
|
549
|
+
// 3. Maximum total wait time exceeded
|
|
550
|
+
const MAX_WAIT_MS = 30_000;
|
|
551
|
+
const MAX_BACKOFF_MS = 4_000;
|
|
552
|
+
const BASE_DELAY_MS = 500;
|
|
553
|
+
const startedAt = Date.now();
|
|
554
|
+
let attempt = 0;
|
|
555
|
+
while (Date.now() - startedAt < MAX_WAIT_MS) {
|
|
556
|
+
// Termination: first message's run has already finished
|
|
557
|
+
if (!hasActiveTask(sessionId)) {
|
|
558
|
+
log.log(`[STEER-QUEUE] First message completed, skip steer`);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const injected = queueAgentHarnessMessage(sessionId, steerMessage, {
|
|
562
|
+
steeringMode: "all",
|
|
563
|
+
});
|
|
564
|
+
if (injected) {
|
|
565
|
+
log.log(`[STEER-QUEUE] Steer injected successfully on attempt ${attempt}`);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
// Exponential backoff: min(BASE_DELAY_MS * 2^attempt, MAX_BACKOFF_MS)
|
|
569
|
+
const delay = Math.min(BASE_DELAY_MS * Math.pow(2, attempt), MAX_BACKOFF_MS);
|
|
570
|
+
log.log(`[STEER-QUEUE] Attempt ${attempt}: run not accepting, retrying in ${delay}ms`);
|
|
571
|
+
await new Promise(r => setTimeout(r, delay));
|
|
572
|
+
attempt++;
|
|
573
|
+
}
|
|
574
|
+
// Max wait exceeded — model may still be in provider retry loop or unresponsive
|
|
575
|
+
log.log(`[STEER-QUEUE] Steer injection failed after ${MAX_WAIT_MS}ms timeout`);
|
|
652
576
|
}
|