@zhin.js/service-activity-feedback 1.0.1 → 1.1.0
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/README.md +54 -16
- package/lib/ai-event-binder.d.ts +15 -5
- package/lib/ai-event-binder.d.ts.map +1 -1
- package/lib/ai-event-binder.js +148 -26
- package/lib/ai-event-binder.js.map +1 -1
- package/lib/config.d.ts +2 -2
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +2 -2
- package/lib/config.js.map +1 -1
- package/lib/executor.d.ts +17 -6
- package/lib/executor.d.ts.map +1 -1
- package/lib/executor.js +166 -21
- package/lib/executor.js.map +1 -1
- package/lib/index.d.ts +7 -3
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +4 -16
- package/lib/index.js.map +1 -1
- package/lib/orchestrator.d.ts +8 -0
- package/lib/orchestrator.d.ts.map +1 -1
- package/lib/orchestrator.js +76 -8
- package/lib/orchestrator.js.map +1 -1
- package/lib/policy.d.ts +1 -1
- package/lib/policy.d.ts.map +1 -1
- package/lib/policy.js +13 -2
- package/lib/policy.js.map +1 -1
- package/package.json +29 -9
- package/plugin.js +43 -0
- package/schema.json +128 -0
- package/src/ai-event-binder.ts +178 -32
- package/src/config.ts +3 -3
- package/src/executor.ts +200 -24
- package/src/index.ts +9 -27
- package/src/orchestrator.ts +106 -7
- package/src/policy.ts +18 -2
- package/plugin.yml +0 -2
package/src/executor.ts
CHANGED
|
@@ -1,23 +1,185 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import
|
|
1
|
+
import { enableActivityFeedbackForBot, isGenericActivityFeedbackManager, type ActivityFeedbackManager, type ActivityFeedbackPhase, type ActivityFeedbackSendPort, type EndpointWithActivityFeedback, type PlatformActivityFeedbackManager, type ResolvedActivityFeedbackPhaseConfig, type ActivityFeedbackEventContext } from '@zhin.js/agent';
|
|
2
|
+
import type { OutboundHost } from 'zhin.js';
|
|
3
3
|
|
|
4
4
|
/** IM 侧 endpoint 访问 seam(便于测试注入 fake) */
|
|
5
5
|
export interface ActivityFeedbackEndpointAccess {
|
|
6
6
|
resolve(
|
|
7
7
|
platform: string,
|
|
8
|
-
|
|
9
|
-
): { endpoint: EndpointWithActivityFeedback;
|
|
8
|
+
endpointKey: string,
|
|
9
|
+
): { endpoint: EndpointWithActivityFeedback; outbound: ActivityFeedbackSendPort } | undefined;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}): ActivityFeedbackEndpointAccess {
|
|
12
|
+
/** Slice-2: no Adapter inject — executor start/stop no-op when resolve returns undefined. */
|
|
13
|
+
export function createNoopEndpointAccess(): ActivityFeedbackEndpointAccess {
|
|
15
14
|
return {
|
|
16
|
-
resolve(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
resolve() {
|
|
16
|
+
return undefined;
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function stringifySendContent(content: unknown): string {
|
|
22
|
+
if (typeof content === 'string') return content;
|
|
23
|
+
if (!Array.isArray(content)) return content == null ? '' : String(content);
|
|
24
|
+
return content.map((segment) => {
|
|
25
|
+
if (typeof segment === 'string') return segment;
|
|
26
|
+
if (segment && typeof segment === 'object') {
|
|
27
|
+
const data = (segment as { data?: { text?: string }; text?: string }).data
|
|
28
|
+
?? (segment as { text?: string });
|
|
29
|
+
if (typeof data === 'object' && data && 'text' in data && typeof data.text === 'string') {
|
|
30
|
+
return data.text;
|
|
31
|
+
}
|
|
32
|
+
if (typeof (segment as { text?: string }).text === 'string') {
|
|
33
|
+
return (segment as { text: string }).text;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return '';
|
|
37
|
+
}).join('');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Plugin Runtime: resolve endpoints via OutboundHost → ImRuntime.sendEndpointMessage.
|
|
42
|
+
* Typing/reaction text goes through the unified outbound chain (no legacy Adapter.inject).
|
|
43
|
+
*
|
|
44
|
+
* 按 platform:endpointKey 缓存 { endpoint, outbound }:activity manager 挂在
|
|
45
|
+
* endpoint.$activityFeedback 上,start/stop 必须解析到同一个对象,否则
|
|
46
|
+
* stop 时拿不到 manager,typing 指示器永远无法停止。
|
|
47
|
+
*/
|
|
48
|
+
export function createOutboundEndpointAccess(
|
|
49
|
+
outbound: OutboundHost,
|
|
50
|
+
logger?: { debug: (msg: string, ...args: unknown[]) => void },
|
|
51
|
+
): ActivityFeedbackEndpointAccess {
|
|
52
|
+
const cache = new Map<string, {
|
|
53
|
+
endpoint: EndpointWithActivityFeedback;
|
|
54
|
+
outbound: ActivityFeedbackSendPort;
|
|
55
|
+
}>();
|
|
56
|
+
return {
|
|
57
|
+
resolve(platform, endpointKey) {
|
|
58
|
+
const key = JSON.stringify([platform, endpointKey]);
|
|
59
|
+
const cached = cache.get(key);
|
|
60
|
+
if (cached) return cached;
|
|
61
|
+
const declared = outbound.capabilities?.({ adapter: platform, endpointKey })?.operations;
|
|
62
|
+
const supports = (operation: 'recall' | 'edit' | 'reaction' | 'typing') =>
|
|
63
|
+
declared?.includes(operation) === true;
|
|
64
|
+
const recall = outbound.recall;
|
|
65
|
+
const edit = outbound.edit;
|
|
66
|
+
const addReaction = outbound.addReaction;
|
|
67
|
+
const removeReaction = outbound.removeReaction;
|
|
68
|
+
const typing = outbound.typing;
|
|
69
|
+
const endpoint = {
|
|
70
|
+
$id: endpointKey,
|
|
71
|
+
control: {
|
|
72
|
+
...(recall && supports('recall') ? {
|
|
73
|
+
recall: async (message: Parameters<typeof recall>[0]['message']) => {
|
|
74
|
+
try {
|
|
75
|
+
await recall({ adapter: platform, endpointKey, message });
|
|
76
|
+
} catch (error) {
|
|
77
|
+
logger?.debug(
|
|
78
|
+
`[ActivityFeedback] outbound recall failed (${key}):`,
|
|
79
|
+
error instanceof Error ? error.message : String(error),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
} : {}),
|
|
84
|
+
...(edit && supports('edit') ? {
|
|
85
|
+
edit: async (
|
|
86
|
+
message: Parameters<typeof edit>[0]['message'],
|
|
87
|
+
content: unknown,
|
|
88
|
+
) => {
|
|
89
|
+
try {
|
|
90
|
+
return await edit({ adapter: platform, endpointKey, message, content });
|
|
91
|
+
} catch (error) {
|
|
92
|
+
logger?.debug(
|
|
93
|
+
`[ActivityFeedback] outbound edit failed (${key}):`,
|
|
94
|
+
error instanceof Error ? error.message : String(error),
|
|
95
|
+
);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
} : {}),
|
|
100
|
+
...(addReaction && supports('reaction') ? {
|
|
101
|
+
addReaction: async (
|
|
102
|
+
message: Parameters<typeof addReaction>[0]['message'],
|
|
103
|
+
emoji: string,
|
|
104
|
+
hint?: { sceneType?: 'private' | 'group' | 'channel'; channelId?: string },
|
|
105
|
+
) => {
|
|
106
|
+
try {
|
|
107
|
+
return await addReaction({
|
|
108
|
+
adapter: platform,
|
|
109
|
+
endpointKey,
|
|
110
|
+
message,
|
|
111
|
+
emoji,
|
|
112
|
+
sceneType: hint?.sceneType,
|
|
113
|
+
channelId: hint?.channelId,
|
|
114
|
+
});
|
|
115
|
+
} catch (error) {
|
|
116
|
+
logger?.debug(
|
|
117
|
+
`[ActivityFeedback] outbound addReaction failed (${key}):`,
|
|
118
|
+
error instanceof Error ? error.message : String(error),
|
|
119
|
+
);
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
} : {}),
|
|
124
|
+
...(removeReaction && supports('reaction') ? {
|
|
125
|
+
removeReaction: async (
|
|
126
|
+
message: Parameters<typeof removeReaction>[0]['message'],
|
|
127
|
+
reactionId: string,
|
|
128
|
+
) => {
|
|
129
|
+
try {
|
|
130
|
+
await removeReaction({ adapter: platform, endpointKey, message, reactionId });
|
|
131
|
+
} catch (error) {
|
|
132
|
+
logger?.debug(
|
|
133
|
+
`[ActivityFeedback] outbound removeReaction failed (${key}):`,
|
|
134
|
+
error instanceof Error ? error.message : String(error),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
} : {}),
|
|
139
|
+
...(typing && supports('typing') ? {
|
|
140
|
+
typing: async (
|
|
141
|
+
conversation: Parameters<typeof typing>[0]['conversation'],
|
|
142
|
+
active?: boolean,
|
|
143
|
+
) => {
|
|
144
|
+
try {
|
|
145
|
+
await typing({ adapter: platform, endpointKey, conversation, active });
|
|
146
|
+
} catch (error) {
|
|
147
|
+
logger?.debug(
|
|
148
|
+
`[ActivityFeedback] outbound typing failed (${key}):`,
|
|
149
|
+
error instanceof Error ? error.message : String(error),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
} : {}),
|
|
154
|
+
},
|
|
155
|
+
} as EndpointWithActivityFeedback;
|
|
156
|
+
const sendPort: ActivityFeedbackSendPort = {
|
|
157
|
+
send: async ({ conversation, content }) => {
|
|
158
|
+
const text = stringifySendContent(content);
|
|
159
|
+
if (!text || !conversation.id) return null;
|
|
160
|
+
try {
|
|
161
|
+
const messageId = await outbound.send({
|
|
162
|
+
adapter: platform,
|
|
163
|
+
endpointKey,
|
|
164
|
+
conversation: {
|
|
165
|
+
kind: conversation.kind,
|
|
166
|
+
id: conversation.id,
|
|
167
|
+
},
|
|
168
|
+
content: text,
|
|
169
|
+
});
|
|
170
|
+
return messageId || null;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
logger?.debug(
|
|
173
|
+
`[ActivityFeedback] outbound send failed (${key}):`,
|
|
174
|
+
error instanceof Error ? error.message : String(error),
|
|
175
|
+
);
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
const resolved = { endpoint, outbound: sendPort };
|
|
181
|
+
cache.set(key, resolved);
|
|
182
|
+
return resolved;
|
|
21
183
|
},
|
|
22
184
|
};
|
|
23
185
|
}
|
|
@@ -75,7 +237,7 @@ class GenericPhaseDriver implements PhaseDriver {
|
|
|
75
237
|
constructor(
|
|
76
238
|
private readonly endpoint: EndpointWithActivityFeedback,
|
|
77
239
|
private readonly platform: string,
|
|
78
|
-
private readonly
|
|
240
|
+
private readonly outbound: ActivityFeedbackSendPort,
|
|
79
241
|
) {}
|
|
80
242
|
|
|
81
243
|
private async ensureManager(): Promise<ActivityFeedbackManager> {
|
|
@@ -85,7 +247,7 @@ class GenericPhaseDriver implements PhaseDriver {
|
|
|
85
247
|
this.manager = existing;
|
|
86
248
|
return existing;
|
|
87
249
|
}
|
|
88
|
-
this.manager = enableActivityFeedbackForBot(this.endpoint, this.platform, this.
|
|
250
|
+
this.manager = enableActivityFeedbackForBot(this.endpoint, this.platform, this.outbound);
|
|
89
251
|
return this.manager;
|
|
90
252
|
}
|
|
91
253
|
|
|
@@ -118,13 +280,13 @@ class GenericPhaseDriver implements PhaseDriver {
|
|
|
118
280
|
function createPhaseDriver(
|
|
119
281
|
endpoint: EndpointWithActivityFeedback,
|
|
120
282
|
platform: string,
|
|
121
|
-
|
|
283
|
+
outbound: ActivityFeedbackSendPort,
|
|
122
284
|
): PhaseDriver {
|
|
123
285
|
const manager = endpoint.$activityFeedback;
|
|
124
286
|
if (manager && !isGenericActivityFeedbackManager(manager)) {
|
|
125
287
|
return new PlatformPhaseDriver(manager);
|
|
126
288
|
}
|
|
127
|
-
return new GenericPhaseDriver(endpoint, platform,
|
|
289
|
+
return new GenericPhaseDriver(endpoint, platform, outbound);
|
|
128
290
|
}
|
|
129
291
|
|
|
130
292
|
export class ActivityFeedbackExecutor {
|
|
@@ -135,23 +297,37 @@ export class ActivityFeedbackExecutor {
|
|
|
135
297
|
phase: ActivityFeedbackPhase,
|
|
136
298
|
phaseConfig: ResolvedActivityFeedbackPhaseConfig,
|
|
137
299
|
): Promise<void> {
|
|
138
|
-
const resolved = this.access.resolve(ctx.platform, ctx.
|
|
300
|
+
const resolved = this.access.resolve(ctx.platform, ctx.endpointKey);
|
|
139
301
|
if (!resolved) return;
|
|
140
|
-
const driver = createPhaseDriver(resolved.endpoint, ctx.platform, resolved.
|
|
302
|
+
const driver = createPhaseDriver(resolved.endpoint, ctx.platform, resolved.outbound);
|
|
141
303
|
await driver.start(ctx, phase, phaseConfig);
|
|
142
304
|
}
|
|
143
305
|
|
|
144
306
|
async stop(ctx: ActivityFeedbackEventContext, phase: ActivityFeedbackPhase): Promise<void> {
|
|
145
|
-
const resolved = this.access.resolve(ctx.platform, ctx.
|
|
307
|
+
const resolved = this.access.resolve(ctx.platform, ctx.endpointKey);
|
|
146
308
|
if (!resolved?.endpoint.$activityFeedback) return;
|
|
147
|
-
const driver = createPhaseDriver(resolved.endpoint, ctx.platform, resolved.
|
|
309
|
+
const driver = createPhaseDriver(resolved.endpoint, ctx.platform, resolved.outbound);
|
|
148
310
|
await driver.stop(ctx, phase);
|
|
149
311
|
}
|
|
150
312
|
|
|
151
|
-
async
|
|
152
|
-
|
|
313
|
+
async updateText(
|
|
314
|
+
ctx: ActivityFeedbackEventContext,
|
|
315
|
+
phase: ActivityFeedbackPhase,
|
|
316
|
+
text: string,
|
|
317
|
+
): Promise<void> {
|
|
318
|
+
const resolved = this.access.resolve(ctx.platform, ctx.endpointKey);
|
|
153
319
|
if (!resolved) return;
|
|
154
|
-
const driver = createPhaseDriver(resolved.endpoint, ctx.platform, resolved.
|
|
155
|
-
|
|
320
|
+
const driver = createPhaseDriver(resolved.endpoint, ctx.platform, resolved.outbound);
|
|
321
|
+
const manager = resolved.endpoint.$activityFeedback;
|
|
322
|
+
if (!manager || !isGenericActivityFeedbackManager(manager)) {
|
|
323
|
+
if (phase === 'thinking') await driver.updateThinkingText(ctx, text);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const indicator = manager.getActiveIndicator(phase, ctx.options);
|
|
327
|
+
if (indicator?.update) await indicator.update(text);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async updateThinkingText(ctx: ActivityFeedbackEventContext, text: string): Promise<void> {
|
|
331
|
+
await this.updateText(ctx, 'thinking', text);
|
|
156
332
|
}
|
|
157
333
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,34 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @zhin.js/service-activity-feedback
|
|
3
|
+
*
|
|
4
|
+
* Pure module exports. Plugin Runtime entry is `plugin.ts`.
|
|
3
5
|
*/
|
|
4
|
-
import { usePlugin } from 'zhin.js';
|
|
5
|
-
import {
|
|
6
|
-
createActivityFeedbackOrchestratorFromPlugin,
|
|
7
|
-
mountActivityFeedbackService,
|
|
8
|
-
} from './ai-event-binder.js';
|
|
9
|
-
import { loadActivityFeedbackServiceConfig, type ActivityFeedbackServiceConfig } from './config.js';
|
|
10
|
-
|
|
11
|
-
const plugin = usePlugin();
|
|
12
|
-
const { root, logger } = plugin;
|
|
13
|
-
|
|
14
|
-
const configService = root.inject('config');
|
|
15
|
-
const appConfig = configService?.getPrimary<{ activityFeedback?: ActivityFeedbackServiceConfig }>() || {};
|
|
16
|
-
const serviceConfig = loadActivityFeedbackServiceConfig(appConfig.activityFeedback);
|
|
17
|
-
|
|
18
|
-
if (serviceConfig.enabled !== false) {
|
|
19
|
-
mountActivityFeedbackService(
|
|
20
|
-
plugin,
|
|
21
|
-
createActivityFeedbackOrchestratorFromPlugin(plugin, serviceConfig),
|
|
22
|
-
);
|
|
23
|
-
} else {
|
|
24
|
-
logger.debug('[ActivityFeedback] disabled by activityFeedback.enabled=false');
|
|
25
|
-
}
|
|
26
|
-
|
|
27
6
|
export {
|
|
28
|
-
|
|
7
|
+
bindActivityFeedbackToAIEventBus,
|
|
8
|
+
createActivityFeedbackAIEventHandlers,
|
|
29
9
|
createActivityFeedbackOrchestrator,
|
|
30
|
-
|
|
31
|
-
mountActivityFeedbackService,
|
|
10
|
+
createActivityFeedbackOrchestratorForRuntime,
|
|
32
11
|
} from './ai-event-binder.js';
|
|
33
12
|
export { ActivityFeedbackOrchestrator } from './orchestrator.js';
|
|
34
13
|
export { ActivityFeedbackPolicy } from './policy.js';
|
|
@@ -38,4 +17,7 @@ export {
|
|
|
38
17
|
type ActivityFeedbackServiceConfig,
|
|
39
18
|
} from './config.js';
|
|
40
19
|
export type { ActivityFeedbackEndpointAccess } from './executor.js';
|
|
41
|
-
export
|
|
20
|
+
export {
|
|
21
|
+
createNoopEndpointAccess,
|
|
22
|
+
createOutboundEndpointAccess,
|
|
23
|
+
} from './executor.js';
|
package/src/orchestrator.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
toActivityFeedbackEventContext,
|
|
3
3
|
isActivityFeedbackEnabled,
|
|
4
|
+
applySubagentActivityPrefixToConfig,
|
|
5
|
+
withSubagentActivityPrefix,
|
|
4
6
|
type AIEventPayload,
|
|
5
7
|
type ActivityFeedbackPhase,
|
|
6
8
|
} from '@zhin.js/agent';
|
|
@@ -10,6 +12,17 @@ import { ActivityFeedbackPolicy } from './policy.js';
|
|
|
10
12
|
type Logger = { debug: (msg: string, ...args: unknown[]) => void; error: (msg: string, ...args: unknown[]) => void };
|
|
11
13
|
|
|
12
14
|
export class ActivityFeedbackOrchestrator {
|
|
15
|
+
private readonly active = new Map<string, {
|
|
16
|
+
ctx: NonNullable<ReturnType<typeof toActivityFeedbackEventContext>>;
|
|
17
|
+
phase: ActivityFeedbackPhase;
|
|
18
|
+
}>();
|
|
19
|
+
private readonly transient = new Map<string, {
|
|
20
|
+
timer: ReturnType<typeof setTimeout>;
|
|
21
|
+
ctx: NonNullable<ReturnType<typeof toActivityFeedbackEventContext>>;
|
|
22
|
+
phase: ActivityFeedbackPhase;
|
|
23
|
+
}>();
|
|
24
|
+
private readonly pendingCleanup = new Set<Promise<void>>();
|
|
25
|
+
|
|
13
26
|
constructor(
|
|
14
27
|
private readonly policy: ActivityFeedbackPolicy,
|
|
15
28
|
private readonly executor: ActivityFeedbackExecutor,
|
|
@@ -18,16 +31,36 @@ export class ActivityFeedbackOrchestrator {
|
|
|
18
31
|
|
|
19
32
|
async startPhase(payload: AIEventPayload, phase: ActivityFeedbackPhase, reason: string): Promise<void> {
|
|
20
33
|
const gatePhase = phase as import('@zhin.js/agent').ActivityFeedbackGatePhase;
|
|
21
|
-
if (!isActivityFeedbackEnabled(payload, gatePhase))
|
|
34
|
+
if (!isActivityFeedbackEnabled(payload, gatePhase)) {
|
|
35
|
+
this.log.debug(
|
|
36
|
+
`[ActivityFeedback] skip ${phase} (${reason}): gate closed`
|
|
37
|
+
+ ` (eligible=${String(payload.hookContext?.activityFeedbackEligible)})`,
|
|
38
|
+
);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
22
41
|
const ctx = toActivityFeedbackEventContext(payload);
|
|
23
|
-
if (!ctx)
|
|
42
|
+
if (!ctx) {
|
|
43
|
+
this.log.debug(
|
|
44
|
+
`[ActivityFeedback] skip ${phase} (${reason}): unresolvable context`
|
|
45
|
+
+ ` (platform=${String(payload.platform)} endpoint=${String(payload.endpointKey)})`,
|
|
46
|
+
);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
24
49
|
|
|
25
50
|
try {
|
|
26
|
-
const resolution = this.policy.resolvePhase(ctx.platform, ctx.
|
|
27
|
-
if (resolution.kind !== 'active')
|
|
51
|
+
const resolution = this.policy.resolvePhase(ctx.platform, ctx.endpointKey, phase, ctx.sceneType);
|
|
52
|
+
if (resolution.kind !== 'active') {
|
|
53
|
+
this.log.debug(
|
|
54
|
+
`[ActivityFeedback] skip ${phase} (${reason}): policy=${resolution.kind}`
|
|
55
|
+
+ ` (${ctx.platform}:${ctx.endpointKey} ${ctx.sceneType})`,
|
|
56
|
+
);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
28
59
|
|
|
60
|
+
const config = applySubagentActivityPrefixToConfig(resolution.config, payload);
|
|
29
61
|
this.log.debug(`[ActivityFeedback] start ${phase} (${reason}) session=${ctx.sessionId}`);
|
|
30
|
-
await this.executor.start(ctx, phase,
|
|
62
|
+
await this.executor.start(ctx, phase, config);
|
|
63
|
+
this.active.set(this.phaseKey(ctx, phase), { ctx, phase });
|
|
31
64
|
} catch (error) {
|
|
32
65
|
this.log.error(`[ActivityFeedback] start ${phase} failed (${reason}):`, error);
|
|
33
66
|
}
|
|
@@ -36,6 +69,9 @@ export class ActivityFeedbackOrchestrator {
|
|
|
36
69
|
async stopPhase(payload: AIEventPayload, phase: ActivityFeedbackPhase, reason: string): Promise<void> {
|
|
37
70
|
const ctx = toActivityFeedbackEventContext(payload);
|
|
38
71
|
if (!ctx) return;
|
|
72
|
+
const key = this.phaseKey(ctx, phase);
|
|
73
|
+
this.clearTransient(key);
|
|
74
|
+
this.active.delete(key);
|
|
39
75
|
|
|
40
76
|
try {
|
|
41
77
|
this.log.debug(`[ActivityFeedback] stop ${phase} (${reason}) session=${ctx.sessionId}`);
|
|
@@ -46,13 +82,76 @@ export class ActivityFeedbackOrchestrator {
|
|
|
46
82
|
}
|
|
47
83
|
|
|
48
84
|
async updateThinkingText(payload: AIEventPayload, text: string): Promise<void> {
|
|
85
|
+
await this.updatePhaseText(payload, 'thinking', text);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async updatePhaseText(
|
|
89
|
+
payload: AIEventPayload,
|
|
90
|
+
phase: ActivityFeedbackPhase,
|
|
91
|
+
text: string,
|
|
92
|
+
): Promise<void> {
|
|
49
93
|
const ctx = toActivityFeedbackEventContext(payload);
|
|
50
94
|
if (!ctx || !text) return;
|
|
51
95
|
|
|
52
96
|
try {
|
|
53
|
-
await this.executor.
|
|
97
|
+
await this.executor.updateText(ctx, phase, withSubagentActivityPrefix(text, payload));
|
|
54
98
|
} catch (error) {
|
|
55
|
-
this.log.error(
|
|
99
|
+
this.log.error(`[ActivityFeedback] ${phase} update failed:`, error);
|
|
56
100
|
}
|
|
57
101
|
}
|
|
102
|
+
|
|
103
|
+
async showTransientPhase(
|
|
104
|
+
payload: AIEventPayload,
|
|
105
|
+
phase: ActivityFeedbackPhase,
|
|
106
|
+
reason: string,
|
|
107
|
+
): Promise<void> {
|
|
108
|
+
await this.startPhase(payload, phase, reason);
|
|
109
|
+
const ctx = toActivityFeedbackEventContext(payload);
|
|
110
|
+
if (!ctx) return;
|
|
111
|
+
const resolution = this.policy.resolvePhase(ctx.platform, ctx.endpointKey, phase, ctx.sceneType);
|
|
112
|
+
if (resolution.kind !== 'active') return;
|
|
113
|
+
const delay = resolution.config.removeDelay ?? 3_000;
|
|
114
|
+
const key = this.phaseKey(ctx, phase);
|
|
115
|
+
this.clearTransient(key);
|
|
116
|
+
const timer = setTimeout(() => {
|
|
117
|
+
this.transient.delete(key);
|
|
118
|
+
this.active.delete(key);
|
|
119
|
+
const cleanup = this.executor.stop(ctx, phase)
|
|
120
|
+
.catch((error) => {
|
|
121
|
+
this.log.error(`[ActivityFeedback] transient ${phase} cleanup failed:`, error);
|
|
122
|
+
})
|
|
123
|
+
.finally(() => {
|
|
124
|
+
this.pendingCleanup.delete(cleanup);
|
|
125
|
+
});
|
|
126
|
+
this.pendingCleanup.add(cleanup);
|
|
127
|
+
}, Math.max(0, delay));
|
|
128
|
+
timer.unref?.();
|
|
129
|
+
this.transient.set(key, { timer, ctx, phase });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async dispose(): Promise<void> {
|
|
133
|
+
const pending = [...this.active.values()];
|
|
134
|
+
const timers = [...this.transient.values()];
|
|
135
|
+
this.active.clear();
|
|
136
|
+
this.transient.clear();
|
|
137
|
+
for (const item of timers) clearTimeout(item.timer);
|
|
138
|
+
await Promise.allSettled([
|
|
139
|
+
...pending.map((item) => this.executor.stop(item.ctx, item.phase)),
|
|
140
|
+
...this.pendingCleanup,
|
|
141
|
+
]);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private phaseKey(
|
|
145
|
+
ctx: NonNullable<ReturnType<typeof toActivityFeedbackEventContext>>,
|
|
146
|
+
phase: ActivityFeedbackPhase,
|
|
147
|
+
): string {
|
|
148
|
+
return JSON.stringify([ctx.platform, ctx.endpointKey, ctx.sessionId, phase, ctx.messageId]);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private clearTransient(key: string): void {
|
|
152
|
+
const current = this.transient.get(key);
|
|
153
|
+
if (!current) return;
|
|
154
|
+
clearTimeout(current.timer);
|
|
155
|
+
this.transient.delete(key);
|
|
156
|
+
}
|
|
58
157
|
}
|
package/src/policy.ts
CHANGED
|
@@ -23,7 +23,7 @@ export class ActivityFeedbackPolicy {
|
|
|
23
23
|
|
|
24
24
|
resolvePhase(
|
|
25
25
|
platform: string,
|
|
26
|
-
|
|
26
|
+
endpointKey: string,
|
|
27
27
|
phase: ActivityFeedbackPhase,
|
|
28
28
|
sceneType: ActivitySceneType,
|
|
29
29
|
): PhaseResolution {
|
|
@@ -34,7 +34,8 @@ export class ActivityFeedbackPolicy {
|
|
|
34
34
|
const policy = mergeActivityFeedbackLayers(
|
|
35
35
|
this.service.defaults,
|
|
36
36
|
this.service.platforms?.[platform],
|
|
37
|
-
this.service.endpoints?.[`${platform}:${
|
|
37
|
+
this.service.endpoints?.[`${platform}:${endpointKey}`],
|
|
38
|
+
schedulePhaseLayer(this.service, phase),
|
|
38
39
|
);
|
|
39
40
|
|
|
40
41
|
if (policy?.enabled === false) {
|
|
@@ -49,3 +50,18 @@ export class ActivityFeedbackPolicy {
|
|
|
49
50
|
return { kind: 'active', config };
|
|
50
51
|
}
|
|
51
52
|
}
|
|
53
|
+
|
|
54
|
+
function schedulePhaseLayer(
|
|
55
|
+
service: ActivityFeedbackServiceConfig,
|
|
56
|
+
phase: ActivityFeedbackPhase,
|
|
57
|
+
): import('@zhin.js/agent').ActivityFeedbackConfig | undefined {
|
|
58
|
+
const scheduleKey = phase === 'schedule_start'
|
|
59
|
+
? 'start'
|
|
60
|
+
: phase === 'schedule_finish'
|
|
61
|
+
? 'finish'
|
|
62
|
+
: phase === 'schedule_error'
|
|
63
|
+
? 'error'
|
|
64
|
+
: undefined;
|
|
65
|
+
const scenes = scheduleKey ? service.schedule?.phases?.[scheduleKey] : undefined;
|
|
66
|
+
return scenes ? { phases: { [phase]: scenes } } : undefined;
|
|
67
|
+
}
|
package/plugin.yml
DELETED