@zhin.js/service-activity-feedback 3.0.14 → 3.0.16
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 -1
- package/lib/ai-event-binder.d.ts.map +1 -1
- package/lib/ai-event-binder.js +126 -11
- package/lib/ai-event-binder.js.map +1 -1
- package/lib/config.d.ts +1 -1
- package/lib/config.d.ts.map +1 -1
- package/lib/executor.d.ts +4 -4
- package/lib/executor.d.ts.map +1 -1
- package/lib/executor.js +65 -31
- package/lib/executor.js.map +1 -1
- package/lib/orchestrator.d.ts +8 -0
- package/lib/orchestrator.d.ts.map +1 -1
- package/lib/orchestrator.js +60 -2
- package/lib/orchestrator.js.map +1 -1
- package/lib/policy.d.ts.map +1 -1
- package/lib/policy.js +12 -1
- package/lib/policy.js.map +1 -1
- package/package.json +6 -6
- package/plugin.js +16 -8
- package/schema.json +121 -5
- package/src/ai-event-binder.ts +138 -10
- package/src/config.ts +1 -1
- package/src/executor.ts +82 -39
- package/src/orchestrator.ts +80 -2
- package/src/policy.ts +16 -0
package/src/executor.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { enableActivityFeedbackForBot, isGenericActivityFeedbackManager, type ActivityFeedbackManager, type ActivityFeedbackPhase, type EndpointWithActivityFeedback, type PlatformActivityFeedbackManager, type ResolvedActivityFeedbackPhaseConfig, type ActivityFeedbackEventContext } from '@zhin.js/agent';
|
|
1
|
+
import { enableActivityFeedbackForBot, isGenericActivityFeedbackManager, type ActivityFeedbackManager, type ActivityFeedbackPhase, type ActivityFeedbackSendPort, type EndpointWithActivityFeedback, type PlatformActivityFeedbackManager, type ResolvedActivityFeedbackPhaseConfig, type ActivityFeedbackEventContext } from '@zhin.js/agent';
|
|
3
2
|
import type { OutboundHost } from 'zhin.js';
|
|
4
3
|
|
|
5
4
|
/** IM 侧 endpoint 访问 seam(便于测试注入 fake) */
|
|
@@ -7,7 +6,7 @@ export interface ActivityFeedbackEndpointAccess {
|
|
|
7
6
|
resolve(
|
|
8
7
|
platform: string,
|
|
9
8
|
endpointKey: string,
|
|
10
|
-
): { endpoint: EndpointWithActivityFeedback;
|
|
9
|
+
): { endpoint: EndpointWithActivityFeedback; outbound: ActivityFeedbackSendPort } | undefined;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
/** Slice-2: no Adapter inject — executor start/stop no-op when resolve returns undefined. */
|
|
@@ -42,7 +41,7 @@ function stringifySendContent(content: unknown): string {
|
|
|
42
41
|
* Plugin Runtime: resolve endpoints via OutboundHost → ImRuntime.sendEndpointMessage.
|
|
43
42
|
* Typing/reaction text goes through the unified outbound chain (no legacy Adapter.inject).
|
|
44
43
|
*
|
|
45
|
-
* 按 platform:endpointKey 缓存 { endpoint,
|
|
44
|
+
* 按 platform:endpointKey 缓存 { endpoint, outbound }:activity manager 挂在
|
|
46
45
|
* endpoint.$activityFeedback 上,start/stop 必须解析到同一个对象,否则
|
|
47
46
|
* stop 时拿不到 manager,typing 指示器永远无法停止。
|
|
48
47
|
*/
|
|
@@ -50,19 +49,27 @@ export function createOutboundEndpointAccess(
|
|
|
50
49
|
outbound: OutboundHost,
|
|
51
50
|
logger?: { debug: (msg: string, ...args: unknown[]) => void },
|
|
52
51
|
): ActivityFeedbackEndpointAccess {
|
|
53
|
-
const cache = new Map<string, {
|
|
52
|
+
const cache = new Map<string, {
|
|
53
|
+
endpoint: EndpointWithActivityFeedback;
|
|
54
|
+
outbound: ActivityFeedbackSendPort;
|
|
55
|
+
}>();
|
|
54
56
|
return {
|
|
55
57
|
resolve(platform, endpointKey) {
|
|
56
|
-
const key =
|
|
58
|
+
const key = JSON.stringify([platform, endpointKey]);
|
|
57
59
|
const cached = cache.get(key);
|
|
58
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;
|
|
59
64
|
const recall = outbound.recall;
|
|
65
|
+
const edit = outbound.edit;
|
|
60
66
|
const addReaction = outbound.addReaction;
|
|
61
67
|
const removeReaction = outbound.removeReaction;
|
|
68
|
+
const typing = outbound.typing;
|
|
62
69
|
const endpoint = {
|
|
63
70
|
$id: endpointKey,
|
|
64
71
|
control: {
|
|
65
|
-
...(recall ? {
|
|
72
|
+
...(recall && supports('recall') ? {
|
|
66
73
|
recall: async (message: Parameters<typeof recall>[0]['message']) => {
|
|
67
74
|
try {
|
|
68
75
|
await recall({ adapter: platform, endpointKey, message });
|
|
@@ -74,7 +81,23 @@ export function createOutboundEndpointAccess(
|
|
|
74
81
|
}
|
|
75
82
|
},
|
|
76
83
|
} : {}),
|
|
77
|
-
...(
|
|
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') ? {
|
|
78
101
|
addReaction: async (
|
|
79
102
|
message: Parameters<typeof addReaction>[0]['message'],
|
|
80
103
|
emoji: string,
|
|
@@ -98,44 +121,53 @@ export function createOutboundEndpointAccess(
|
|
|
98
121
|
}
|
|
99
122
|
},
|
|
100
123
|
} : {}),
|
|
101
|
-
...(removeReaction ? {
|
|
124
|
+
...(removeReaction && supports('reaction') ? {
|
|
102
125
|
removeReaction: async (
|
|
103
126
|
message: Parameters<typeof removeReaction>[0]['message'],
|
|
104
127
|
reactionId: string,
|
|
105
128
|
) => {
|
|
106
|
-
|
|
107
|
-
removeReaction({ adapter: platform, endpointKey, message, reactionId })
|
|
108
|
-
|
|
129
|
+
try {
|
|
130
|
+
await removeReaction({ adapter: platform, endpointKey, message, reactionId });
|
|
131
|
+
} catch (error) {
|
|
109
132
|
logger?.debug(
|
|
110
133
|
`[ActivityFeedback] outbound removeReaction failed (${key}):`,
|
|
111
134
|
error instanceof Error ? error.message : String(error),
|
|
112
135
|
);
|
|
113
|
-
}
|
|
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
|
+
}
|
|
114
152
|
},
|
|
115
153
|
} : {}),
|
|
116
154
|
},
|
|
117
155
|
} as EndpointWithActivityFeedback;
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
id
|
|
122
|
-
content?: unknown;
|
|
123
|
-
}) => {
|
|
124
|
-
const text = stringifySendContent(options.content);
|
|
125
|
-
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;
|
|
126
160
|
try {
|
|
127
161
|
const messageId = await outbound.send({
|
|
128
162
|
adapter: platform,
|
|
129
163
|
endpointKey,
|
|
130
164
|
conversation: {
|
|
131
|
-
kind:
|
|
132
|
-
id:
|
|
165
|
+
kind: conversation.kind,
|
|
166
|
+
id: conversation.id,
|
|
133
167
|
},
|
|
134
168
|
content: text,
|
|
135
169
|
});
|
|
136
|
-
|
|
137
|
-
// keeps the phase active until stop (recall is a no-op here).
|
|
138
|
-
return messageId || `outbound:${Date.now()}`;
|
|
170
|
+
return messageId || null;
|
|
139
171
|
} catch (error) {
|
|
140
172
|
logger?.debug(
|
|
141
173
|
`[ActivityFeedback] outbound send failed (${key}):`,
|
|
@@ -144,11 +176,8 @@ export function createOutboundEndpointAccess(
|
|
|
144
176
|
return null;
|
|
145
177
|
}
|
|
146
178
|
},
|
|
147
|
-
endpoints: {
|
|
148
|
-
get: (id: string) => (id === endpointKey ? endpoint : undefined),
|
|
149
|
-
},
|
|
150
179
|
};
|
|
151
|
-
const resolved = { endpoint,
|
|
180
|
+
const resolved = { endpoint, outbound: sendPort };
|
|
152
181
|
cache.set(key, resolved);
|
|
153
182
|
return resolved;
|
|
154
183
|
},
|
|
@@ -208,7 +237,7 @@ class GenericPhaseDriver implements PhaseDriver {
|
|
|
208
237
|
constructor(
|
|
209
238
|
private readonly endpoint: EndpointWithActivityFeedback,
|
|
210
239
|
private readonly platform: string,
|
|
211
|
-
private readonly
|
|
240
|
+
private readonly outbound: ActivityFeedbackSendPort,
|
|
212
241
|
) {}
|
|
213
242
|
|
|
214
243
|
private async ensureManager(): Promise<ActivityFeedbackManager> {
|
|
@@ -218,7 +247,7 @@ class GenericPhaseDriver implements PhaseDriver {
|
|
|
218
247
|
this.manager = existing;
|
|
219
248
|
return existing;
|
|
220
249
|
}
|
|
221
|
-
this.manager = enableActivityFeedbackForBot(this.endpoint, this.platform, this.
|
|
250
|
+
this.manager = enableActivityFeedbackForBot(this.endpoint, this.platform, this.outbound);
|
|
222
251
|
return this.manager;
|
|
223
252
|
}
|
|
224
253
|
|
|
@@ -251,13 +280,13 @@ class GenericPhaseDriver implements PhaseDriver {
|
|
|
251
280
|
function createPhaseDriver(
|
|
252
281
|
endpoint: EndpointWithActivityFeedback,
|
|
253
282
|
platform: string,
|
|
254
|
-
|
|
283
|
+
outbound: ActivityFeedbackSendPort,
|
|
255
284
|
): PhaseDriver {
|
|
256
285
|
const manager = endpoint.$activityFeedback;
|
|
257
286
|
if (manager && !isGenericActivityFeedbackManager(manager)) {
|
|
258
287
|
return new PlatformPhaseDriver(manager);
|
|
259
288
|
}
|
|
260
|
-
return new GenericPhaseDriver(endpoint, platform,
|
|
289
|
+
return new GenericPhaseDriver(endpoint, platform, outbound);
|
|
261
290
|
}
|
|
262
291
|
|
|
263
292
|
export class ActivityFeedbackExecutor {
|
|
@@ -270,21 +299,35 @@ export class ActivityFeedbackExecutor {
|
|
|
270
299
|
): Promise<void> {
|
|
271
300
|
const resolved = this.access.resolve(ctx.platform, ctx.endpointKey);
|
|
272
301
|
if (!resolved) return;
|
|
273
|
-
const driver = createPhaseDriver(resolved.endpoint, ctx.platform, resolved.
|
|
302
|
+
const driver = createPhaseDriver(resolved.endpoint, ctx.platform, resolved.outbound);
|
|
274
303
|
await driver.start(ctx, phase, phaseConfig);
|
|
275
304
|
}
|
|
276
305
|
|
|
277
306
|
async stop(ctx: ActivityFeedbackEventContext, phase: ActivityFeedbackPhase): Promise<void> {
|
|
278
307
|
const resolved = this.access.resolve(ctx.platform, ctx.endpointKey);
|
|
279
308
|
if (!resolved?.endpoint.$activityFeedback) return;
|
|
280
|
-
const driver = createPhaseDriver(resolved.endpoint, ctx.platform, resolved.
|
|
309
|
+
const driver = createPhaseDriver(resolved.endpoint, ctx.platform, resolved.outbound);
|
|
281
310
|
await driver.stop(ctx, phase);
|
|
282
311
|
}
|
|
283
312
|
|
|
284
|
-
async
|
|
313
|
+
async updateText(
|
|
314
|
+
ctx: ActivityFeedbackEventContext,
|
|
315
|
+
phase: ActivityFeedbackPhase,
|
|
316
|
+
text: string,
|
|
317
|
+
): Promise<void> {
|
|
285
318
|
const resolved = this.access.resolve(ctx.platform, ctx.endpointKey);
|
|
286
319
|
if (!resolved) return;
|
|
287
|
-
const driver = createPhaseDriver(resolved.endpoint, ctx.platform, resolved.
|
|
288
|
-
|
|
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);
|
|
289
332
|
}
|
|
290
333
|
}
|
package/src/orchestrator.ts
CHANGED
|
@@ -12,6 +12,17 @@ import { ActivityFeedbackPolicy } from './policy.js';
|
|
|
12
12
|
type Logger = { debug: (msg: string, ...args: unknown[]) => void; error: (msg: string, ...args: unknown[]) => void };
|
|
13
13
|
|
|
14
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
|
+
|
|
15
26
|
constructor(
|
|
16
27
|
private readonly policy: ActivityFeedbackPolicy,
|
|
17
28
|
private readonly executor: ActivityFeedbackExecutor,
|
|
@@ -49,6 +60,7 @@ export class ActivityFeedbackOrchestrator {
|
|
|
49
60
|
const config = applySubagentActivityPrefixToConfig(resolution.config, payload);
|
|
50
61
|
this.log.debug(`[ActivityFeedback] start ${phase} (${reason}) session=${ctx.sessionId}`);
|
|
51
62
|
await this.executor.start(ctx, phase, config);
|
|
63
|
+
this.active.set(this.phaseKey(ctx, phase), { ctx, phase });
|
|
52
64
|
} catch (error) {
|
|
53
65
|
this.log.error(`[ActivityFeedback] start ${phase} failed (${reason}):`, error);
|
|
54
66
|
}
|
|
@@ -57,6 +69,9 @@ export class ActivityFeedbackOrchestrator {
|
|
|
57
69
|
async stopPhase(payload: AIEventPayload, phase: ActivityFeedbackPhase, reason: string): Promise<void> {
|
|
58
70
|
const ctx = toActivityFeedbackEventContext(payload);
|
|
59
71
|
if (!ctx) return;
|
|
72
|
+
const key = this.phaseKey(ctx, phase);
|
|
73
|
+
this.clearTransient(key);
|
|
74
|
+
this.active.delete(key);
|
|
60
75
|
|
|
61
76
|
try {
|
|
62
77
|
this.log.debug(`[ActivityFeedback] stop ${phase} (${reason}) session=${ctx.sessionId}`);
|
|
@@ -67,13 +82,76 @@ export class ActivityFeedbackOrchestrator {
|
|
|
67
82
|
}
|
|
68
83
|
|
|
69
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> {
|
|
70
93
|
const ctx = toActivityFeedbackEventContext(payload);
|
|
71
94
|
if (!ctx || !text) return;
|
|
72
95
|
|
|
73
96
|
try {
|
|
74
|
-
await this.executor.
|
|
97
|
+
await this.executor.updateText(ctx, phase, withSubagentActivityPrefix(text, payload));
|
|
75
98
|
} catch (error) {
|
|
76
|
-
this.log.error(
|
|
99
|
+
this.log.error(`[ActivityFeedback] ${phase} update failed:`, error);
|
|
77
100
|
}
|
|
78
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
|
+
}
|
|
79
157
|
}
|
package/src/policy.ts
CHANGED
|
@@ -35,6 +35,7 @@ export class ActivityFeedbackPolicy {
|
|
|
35
35
|
this.service.defaults,
|
|
36
36
|
this.service.platforms?.[platform],
|
|
37
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
|
+
}
|