@zhin.js/service-activity-feedback 1.0.2 → 1.1.3

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/src/executor.ts CHANGED
@@ -1,26 +1,12 @@
1
- import type { Adapter } from 'zhin.js';
2
- import { enableActivityFeedbackForBot, isGenericActivityFeedbackManager, type ActivityFeedbackManager, type ActivityFeedbackPhase, type EndpointWithActivityFeedback, type PlatformActivityFeedbackManager, type ResolvedActivityFeedbackPhaseConfig, type ActivityFeedbackEventContext } from '@zhin.js/agent';
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
- endpointId: string,
10
- ): { endpoint: EndpointWithActivityFeedback; adapter: Adapter } | undefined;
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:endpointId 缓存 { endpoint, adapter }:activity manager 挂在
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, { endpoint: EndpointWithActivityFeedback; adapter: Adapter }>();
52
+ const cache = new Map<string, {
53
+ endpoint: EndpointWithActivityFeedback;
54
+ outbound: ActivityFeedbackSendPort;
55
+ }>();
67
56
  return {
68
- resolve(platform, endpointId) {
69
- const key = `${platform}:${endpointId}`;
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: endpointId,
74
- // Prefer real OutboundHost.recall when available (icqq RECALL_MSG).
75
- $recallMessage: async (messageId: string) => {
76
- if (outbound.recall) {
77
- try {
78
- await outbound.recall({ adapter: platform, endpointId, messageId });
79
- return;
80
- } catch (error) {
81
- logger?.debug(
82
- `[ActivityFeedback] outbound recall failed (${key}):`,
83
- error instanceof Error ? error.message : String(error),
84
- );
85
- return;
86
- }
87
- }
88
- logger?.debug(
89
- `[ActivityFeedback] recall not supported via OutboundHost (${key}, messageId=${messageId})`,
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 adapter = {
118
- sendMessage: async (options: {
119
- type?: string;
120
- id?: string;
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
- endpointId,
129
- channelType: options.type || 'private',
130
- channelId: options.id,
163
+ endpointKey,
164
+ conversation: {
165
+ kind: conversation.kind,
166
+ id: conversation.id,
167
+ },
131
168
  content: text,
132
169
  });
133
- // Prefer real id; fall back to a sentinel so MessageTypingIndicator
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, adapter: adapter as unknown as Adapter };
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 adapter: Adapter,
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.adapter);
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
- adapter: Adapter,
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, adapter);
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.endpointId);
300
+ const resolved = this.access.resolve(ctx.platform, ctx.endpointKey);
269
301
  if (!resolved) return;
270
- const driver = createPhaseDriver(resolved.endpoint, ctx.platform, resolved.adapter);
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.endpointId);
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.adapter);
309
+ const driver = createPhaseDriver(resolved.endpoint, ctx.platform, resolved.outbound);
278
310
  await driver.stop(ctx, phase);
279
311
  }
280
312
 
281
- async updateThinkingText(ctx: ActivityFeedbackEventContext, text: string): Promise<void> {
282
- const resolved = this.access.resolve(ctx.platform, ctx.endpointId);
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.adapter);
285
- await driver.updateThinkingText(ctx, text);
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,24 +4,19 @@
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';
17
14
  export {
18
15
  loadActivityFeedbackServiceConfig,
19
- resolveActivityFeedbackForTarget,
20
16
  type ActivityFeedbackServiceConfig,
21
17
  } from './config.js';
22
18
  export type { ActivityFeedbackEndpointAccess } from './executor.js';
23
19
  export {
24
20
  createNoopEndpointAccess,
25
21
  createOutboundEndpointAccess,
26
- createRootEndpointAccess,
27
22
  } from './executor.js';
@@ -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)) return;
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) return;
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.endpointId, phase, ctx.sceneType);
27
- if (resolution.kind !== 'active') return;
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, resolution.config);
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.updateThinkingText(ctx, text);
97
+ await this.executor.updateText(ctx, phase, withSubagentActivityPrefix(text, payload));
54
98
  } catch (error) {
55
- this.log.error('[ActivityFeedback] thinking update failed:', 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
@@ -11,7 +11,7 @@ import {
11
11
  } from './config.js';
12
12
 
13
13
  export type { ActivityFeedbackServiceConfig } from './config.js';
14
- export { loadActivityFeedbackServiceConfig, resolveActivityFeedbackForTarget } from './config.js';
14
+ export { loadActivityFeedbackServiceConfig } from './config.js';
15
15
 
16
16
  export type PhaseResolution =
17
17
  | { kind: 'disabled' }
@@ -23,7 +23,7 @@ export class ActivityFeedbackPolicy {
23
23
 
24
24
  resolvePhase(
25
25
  platform: string,
26
- endpointId: string,
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}:${endpointId}`],
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
- });