@zhin.js/service-activity-feedback 1.0.2 → 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 +45 -13
- package/lib/ai-event-binder.d.ts +2 -6
- package/lib/ai-event-binder.d.ts.map +1 -1
- package/lib/ai-event-binder.js +136 -33
- 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 +6 -9
- package/lib/executor.d.ts.map +1 -1
- package/lib/executor.js +107 -76
- package/lib/executor.js.map +1 -1
- package/lib/index.d.ts +2 -2
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -2
- 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 +9 -10
- package/plugin.js +43 -0
- package/schema.json +121 -5
- package/src/ai-event-binder.ts +145 -44
- package/src/config.ts +3 -3
- package/src/executor.ts +140 -94
- package/src/index.ts +0 -4
- package/src/orchestrator.ts +106 -7
- package/src/policy.ts +18 -2
- package/plugin.ts +0 -51
package/src/executor.ts
CHANGED
|
@@ -1,26 +1,12 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import
|
|
3
|
-
import type { OutboundHost } from '@zhin.js/plugin-runtime';
|
|
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';
|
|
4
3
|
|
|
5
4
|
/** IM 侧 endpoint 访问 seam(便于测试注入 fake) */
|
|
6
5
|
export interface ActivityFeedbackEndpointAccess {
|
|
7
6
|
resolve(
|
|
8
7
|
platform: string,
|
|
9
|
-
|
|
10
|
-
): { endpoint: EndpointWithActivityFeedback;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function createRootEndpointAccess(root: {
|
|
14
|
-
injectAdapter(platform: string): Adapter | undefined;
|
|
15
|
-
}): ActivityFeedbackEndpointAccess {
|
|
16
|
-
return {
|
|
17
|
-
resolve(platform, endpointId) {
|
|
18
|
-
const adapter = root.injectAdapter(platform);
|
|
19
|
-
const endpoint = adapter?.endpoints?.get(endpointId) as EndpointWithActivityFeedback | undefined;
|
|
20
|
-
if (!endpoint || !adapter) return undefined;
|
|
21
|
-
return { endpoint, adapter };
|
|
22
|
-
},
|
|
23
|
-
};
|
|
8
|
+
endpointKey: string,
|
|
9
|
+
): { endpoint: EndpointWithActivityFeedback; outbound: ActivityFeedbackSendPort } | undefined;
|
|
24
10
|
}
|
|
25
11
|
|
|
26
12
|
/** Slice-2: no Adapter inject — executor start/stop no-op when resolve returns undefined. */
|
|
@@ -55,7 +41,7 @@ function stringifySendContent(content: unknown): string {
|
|
|
55
41
|
* Plugin Runtime: resolve endpoints via OutboundHost → ImRuntime.sendEndpointMessage.
|
|
56
42
|
* Typing/reaction text goes through the unified outbound chain (no legacy Adapter.inject).
|
|
57
43
|
*
|
|
58
|
-
* 按 platform:
|
|
44
|
+
* 按 platform:endpointKey 缓存 { endpoint, outbound }:activity manager 挂在
|
|
59
45
|
* endpoint.$activityFeedback 上,start/stop 必须解析到同一个对象,否则
|
|
60
46
|
* stop 时拿不到 manager,typing 指示器永远无法停止。
|
|
61
47
|
*/
|
|
@@ -63,76 +49,125 @@ export function createOutboundEndpointAccess(
|
|
|
63
49
|
outbound: OutboundHost,
|
|
64
50
|
logger?: { debug: (msg: string, ...args: unknown[]) => void },
|
|
65
51
|
): ActivityFeedbackEndpointAccess {
|
|
66
|
-
const cache = new Map<string, {
|
|
52
|
+
const cache = new Map<string, {
|
|
53
|
+
endpoint: EndpointWithActivityFeedback;
|
|
54
|
+
outbound: ActivityFeedbackSendPort;
|
|
55
|
+
}>();
|
|
67
56
|
return {
|
|
68
|
-
resolve(platform,
|
|
69
|
-
const key =
|
|
57
|
+
resolve(platform, endpointKey) {
|
|
58
|
+
const key = JSON.stringify([platform, endpointKey]);
|
|
70
59
|
const cached = cache.get(key);
|
|
71
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;
|
|
72
69
|
const endpoint = {
|
|
73
|
-
$id:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
+
} : {}),
|
|
91
154
|
},
|
|
92
|
-
$addReaction: outbound.addReaction
|
|
93
|
-
? async (
|
|
94
|
-
messageId: string,
|
|
95
|
-
emoji: string,
|
|
96
|
-
hint?: { sceneType?: 'private' | 'group' | 'channel'; channelId?: string },
|
|
97
|
-
) => outbound.addReaction!({
|
|
98
|
-
adapter: platform,
|
|
99
|
-
endpointId,
|
|
100
|
-
messageId,
|
|
101
|
-
emoji,
|
|
102
|
-
sceneType: hint?.sceneType,
|
|
103
|
-
channelId: hint?.channelId,
|
|
104
|
-
})
|
|
105
|
-
: undefined,
|
|
106
|
-
$removeReaction: outbound.removeReaction
|
|
107
|
-
? async (messageId: string, reactionId: string) => {
|
|
108
|
-
await outbound.removeReaction!({
|
|
109
|
-
adapter: platform,
|
|
110
|
-
endpointId,
|
|
111
|
-
messageId,
|
|
112
|
-
reactionId,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
: undefined,
|
|
116
155
|
} as EndpointWithActivityFeedback;
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
id
|
|
121
|
-
content?: unknown;
|
|
122
|
-
}) => {
|
|
123
|
-
const text = stringifySendContent(options.content);
|
|
124
|
-
if (!text || !options.id) return null;
|
|
156
|
+
const sendPort: ActivityFeedbackSendPort = {
|
|
157
|
+
send: async ({ conversation, content }) => {
|
|
158
|
+
const text = stringifySendContent(content);
|
|
159
|
+
if (!text || !conversation.id) return null;
|
|
125
160
|
try {
|
|
126
161
|
const messageId = await outbound.send({
|
|
127
162
|
adapter: platform,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
163
|
+
endpointKey,
|
|
164
|
+
conversation: {
|
|
165
|
+
kind: conversation.kind,
|
|
166
|
+
id: conversation.id,
|
|
167
|
+
},
|
|
131
168
|
content: text,
|
|
132
169
|
});
|
|
133
|
-
|
|
134
|
-
// keeps the phase active until stop (recall is a no-op here).
|
|
135
|
-
return messageId || `outbound:${Date.now()}`;
|
|
170
|
+
return messageId || null;
|
|
136
171
|
} catch (error) {
|
|
137
172
|
logger?.debug(
|
|
138
173
|
`[ActivityFeedback] outbound send failed (${key}):`,
|
|
@@ -141,11 +176,8 @@ export function createOutboundEndpointAccess(
|
|
|
141
176
|
return null;
|
|
142
177
|
}
|
|
143
178
|
},
|
|
144
|
-
endpoints: {
|
|
145
|
-
get: (id: string) => (id === endpointId ? endpoint : undefined),
|
|
146
|
-
},
|
|
147
179
|
};
|
|
148
|
-
const resolved = { endpoint,
|
|
180
|
+
const resolved = { endpoint, outbound: sendPort };
|
|
149
181
|
cache.set(key, resolved);
|
|
150
182
|
return resolved;
|
|
151
183
|
},
|
|
@@ -205,7 +237,7 @@ class GenericPhaseDriver implements PhaseDriver {
|
|
|
205
237
|
constructor(
|
|
206
238
|
private readonly endpoint: EndpointWithActivityFeedback,
|
|
207
239
|
private readonly platform: string,
|
|
208
|
-
private readonly
|
|
240
|
+
private readonly outbound: ActivityFeedbackSendPort,
|
|
209
241
|
) {}
|
|
210
242
|
|
|
211
243
|
private async ensureManager(): Promise<ActivityFeedbackManager> {
|
|
@@ -215,7 +247,7 @@ class GenericPhaseDriver implements PhaseDriver {
|
|
|
215
247
|
this.manager = existing;
|
|
216
248
|
return existing;
|
|
217
249
|
}
|
|
218
|
-
this.manager = enableActivityFeedbackForBot(this.endpoint, this.platform, this.
|
|
250
|
+
this.manager = enableActivityFeedbackForBot(this.endpoint, this.platform, this.outbound);
|
|
219
251
|
return this.manager;
|
|
220
252
|
}
|
|
221
253
|
|
|
@@ -248,13 +280,13 @@ class GenericPhaseDriver implements PhaseDriver {
|
|
|
248
280
|
function createPhaseDriver(
|
|
249
281
|
endpoint: EndpointWithActivityFeedback,
|
|
250
282
|
platform: string,
|
|
251
|
-
|
|
283
|
+
outbound: ActivityFeedbackSendPort,
|
|
252
284
|
): PhaseDriver {
|
|
253
285
|
const manager = endpoint.$activityFeedback;
|
|
254
286
|
if (manager && !isGenericActivityFeedbackManager(manager)) {
|
|
255
287
|
return new PlatformPhaseDriver(manager);
|
|
256
288
|
}
|
|
257
|
-
return new GenericPhaseDriver(endpoint, platform,
|
|
289
|
+
return new GenericPhaseDriver(endpoint, platform, outbound);
|
|
258
290
|
}
|
|
259
291
|
|
|
260
292
|
export class ActivityFeedbackExecutor {
|
|
@@ -265,23 +297,37 @@ export class ActivityFeedbackExecutor {
|
|
|
265
297
|
phase: ActivityFeedbackPhase,
|
|
266
298
|
phaseConfig: ResolvedActivityFeedbackPhaseConfig,
|
|
267
299
|
): Promise<void> {
|
|
268
|
-
const resolved = this.access.resolve(ctx.platform, ctx.
|
|
300
|
+
const resolved = this.access.resolve(ctx.platform, ctx.endpointKey);
|
|
269
301
|
if (!resolved) return;
|
|
270
|
-
const driver = createPhaseDriver(resolved.endpoint, ctx.platform, resolved.
|
|
302
|
+
const driver = createPhaseDriver(resolved.endpoint, ctx.platform, resolved.outbound);
|
|
271
303
|
await driver.start(ctx, phase, phaseConfig);
|
|
272
304
|
}
|
|
273
305
|
|
|
274
306
|
async stop(ctx: ActivityFeedbackEventContext, phase: ActivityFeedbackPhase): Promise<void> {
|
|
275
|
-
const resolved = this.access.resolve(ctx.platform, ctx.
|
|
307
|
+
const resolved = this.access.resolve(ctx.platform, ctx.endpointKey);
|
|
276
308
|
if (!resolved?.endpoint.$activityFeedback) return;
|
|
277
|
-
const driver = createPhaseDriver(resolved.endpoint, ctx.platform, resolved.
|
|
309
|
+
const driver = createPhaseDriver(resolved.endpoint, ctx.platform, resolved.outbound);
|
|
278
310
|
await driver.stop(ctx, phase);
|
|
279
311
|
}
|
|
280
312
|
|
|
281
|
-
async
|
|
282
|
-
|
|
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);
|
|
283
319
|
if (!resolved) return;
|
|
284
|
-
const driver = createPhaseDriver(resolved.endpoint, ctx.platform, resolved.
|
|
285
|
-
|
|
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);
|
|
286
332
|
}
|
|
287
333
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,13 +4,10 @@
|
|
|
4
4
|
* Pure module exports. Plugin Runtime entry is `plugin.ts`.
|
|
5
5
|
*/
|
|
6
6
|
export {
|
|
7
|
-
bindActivityFeedbackToAIEvents,
|
|
8
7
|
bindActivityFeedbackToAIEventBus,
|
|
9
8
|
createActivityFeedbackAIEventHandlers,
|
|
10
9
|
createActivityFeedbackOrchestrator,
|
|
11
|
-
createActivityFeedbackOrchestratorFromPlugin,
|
|
12
10
|
createActivityFeedbackOrchestratorForRuntime,
|
|
13
|
-
mountActivityFeedbackService,
|
|
14
11
|
} from './ai-event-binder.js';
|
|
15
12
|
export { ActivityFeedbackOrchestrator } from './orchestrator.js';
|
|
16
13
|
export { ActivityFeedbackPolicy } from './policy.js';
|
|
@@ -23,5 +20,4 @@ export type { ActivityFeedbackEndpointAccess } from './executor.js';
|
|
|
23
20
|
export {
|
|
24
21
|
createNoopEndpointAccess,
|
|
25
22
|
createOutboundEndpointAccess,
|
|
26
|
-
createRootEndpointAccess,
|
|
27
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.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { definePlugin, outboundHostToken } from '@zhin.js/plugin-runtime';
|
|
2
|
-
import { getLogger } from '@zhin.js/logger';
|
|
3
|
-
import {
|
|
4
|
-
loadActivityFeedbackServiceConfig,
|
|
5
|
-
type ActivityFeedbackServiceConfig,
|
|
6
|
-
} from './src/config.js';
|
|
7
|
-
import {
|
|
8
|
-
bindActivityFeedbackToAIEventBus,
|
|
9
|
-
createActivityFeedbackOrchestratorForRuntime,
|
|
10
|
-
} from './src/ai-event-binder.js';
|
|
11
|
-
import {
|
|
12
|
-
createNoopEndpointAccess,
|
|
13
|
-
createOutboundEndpointAccess,
|
|
14
|
-
} from './src/executor.js';
|
|
15
|
-
|
|
16
|
-
const logger = getLogger('activity-feedback');
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Activity Feedback service — Plugin Runtime entry.
|
|
20
|
-
*
|
|
21
|
-
* Subscribes AI lifecycle events via `activityFeedbackAiBus`.
|
|
22
|
-
* When Root provides `outboundHostToken`, typing/status text uses
|
|
23
|
-
* ImRuntime.sendEndpointMessage; otherwise phases no-op.
|
|
24
|
-
*/
|
|
25
|
-
export default definePlugin<ActivityFeedbackServiceConfig>({
|
|
26
|
-
name: 'activity-feedback',
|
|
27
|
-
metadata: {
|
|
28
|
-
displayName: 'Activity Feedback',
|
|
29
|
-
},
|
|
30
|
-
setup(context) {
|
|
31
|
-
const serviceConfig = loadActivityFeedbackServiceConfig(context.config.get());
|
|
32
|
-
if (serviceConfig.enabled === false) {
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const access = context.resources.has(outboundHostToken)
|
|
37
|
-
? createOutboundEndpointAccess(context.resources.use(outboundHostToken), logger)
|
|
38
|
-
: createNoopEndpointAccess();
|
|
39
|
-
|
|
40
|
-
const orchestrator = createActivityFeedbackOrchestratorForRuntime(
|
|
41
|
-
serviceConfig,
|
|
42
|
-
logger,
|
|
43
|
-
access,
|
|
44
|
-
);
|
|
45
|
-
const dispose = bindActivityFeedbackToAIEventBus(orchestrator);
|
|
46
|
-
context.lifecycle.add(() => {
|
|
47
|
-
logger.debug('[ActivityFeedback] Disposing Runtime AI event binder');
|
|
48
|
-
dispose();
|
|
49
|
-
});
|
|
50
|
-
},
|
|
51
|
-
});
|