@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/plugin.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { createGenerationAdmissionGate, createToken, definePlugin, outboundHostToken, } from 'zhin.js';
|
|
3
|
+
import { getLogger } from '@zhin.js/logger';
|
|
4
|
+
import { loadActivityFeedbackServiceConfig, } from "./lib/config.js";
|
|
5
|
+
import { bindActivityFeedbackToAIEventBus, createActivityFeedbackOrchestratorForRuntime, } from "./lib/ai-event-binder.js";
|
|
6
|
+
import { createOutboundEndpointAccess, } from "./lib/executor.js";
|
|
7
|
+
const logger = getLogger('activity-feedback');
|
|
8
|
+
const activityFeedbackAdmissionToken = createToken('zhin.activity-feedback.generation-admission');
|
|
9
|
+
/**
|
|
10
|
+
* Activity Feedback service — Plugin Runtime entry.
|
|
11
|
+
*
|
|
12
|
+
* Subscribes AI lifecycle events via `activityFeedbackAiBus`.
|
|
13
|
+
* When Root provides `outboundHostToken`, typing/status text uses
|
|
14
|
+
* ImRuntime.sendEndpointMessage; otherwise phases no-op.
|
|
15
|
+
*/
|
|
16
|
+
export default definePlugin({
|
|
17
|
+
name: 'activity-feedback',
|
|
18
|
+
metadata: {
|
|
19
|
+
displayName: 'Activity Feedback',
|
|
20
|
+
},
|
|
21
|
+
setup(context) {
|
|
22
|
+
const serviceConfig = loadActivityFeedbackServiceConfig(context.config.get());
|
|
23
|
+
if (serviceConfig.enabled === false) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const outbound = context.resources.has(outboundHostToken)
|
|
27
|
+
? context.resources.use(outboundHostToken)
|
|
28
|
+
: undefined;
|
|
29
|
+
if (!outbound || typeof outbound.runWithView !== 'function') {
|
|
30
|
+
logger.debug('[ActivityFeedback] disabled: OutboundHost with generation-bound runWithView is required');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const access = createOutboundEndpointAccess(outbound, logger);
|
|
34
|
+
const orchestrator = createActivityFeedbackOrchestratorForRuntime(serviceConfig, logger, access);
|
|
35
|
+
const admission = createGenerationAdmissionGate();
|
|
36
|
+
context.resources.provide(activityFeedbackAdmissionToken, admission);
|
|
37
|
+
const dispose = bindActivityFeedbackToAIEventBus(orchestrator, admission, outbound.runWithView.bind(outbound));
|
|
38
|
+
context.lifecycle.add(async () => {
|
|
39
|
+
logger.debug('[ActivityFeedback] Disposing Runtime AI event binder');
|
|
40
|
+
await dispose();
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
});
|
package/schema.json
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"additionalProperties": false,
|
|
5
|
+
"properties": {
|
|
6
|
+
"enabled": {
|
|
7
|
+
"type": "boolean",
|
|
8
|
+
"default": true,
|
|
9
|
+
"description": "Enable activity feedback for this plugin instance.",
|
|
10
|
+
"x-descriptionZh": "是否启用当前活动反馈插件实例。"
|
|
11
|
+
},
|
|
12
|
+
"defaults": {
|
|
13
|
+
"$ref": "#/$defs/activityFeedback",
|
|
14
|
+
"description": "Default policy for all platforms and Endpoints.",
|
|
15
|
+
"x-descriptionZh": "所有平台与 Endpoint 的默认策略。"
|
|
16
|
+
},
|
|
17
|
+
"platforms": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"x-keyPlaceholder": "<platform>",
|
|
20
|
+
"additionalProperties": { "$ref": "#/$defs/activityFeedback" },
|
|
21
|
+
"description": "Platform overrides keyed by platform name; values use the same shape as defaults.",
|
|
22
|
+
"x-descriptionZh": "按平台名覆盖;值的结构与 defaults 相同。"
|
|
23
|
+
},
|
|
24
|
+
"endpoints": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"x-keyPlaceholder": "<platform:endpointKey>",
|
|
27
|
+
"additionalProperties": { "$ref": "#/$defs/activityFeedback" },
|
|
28
|
+
"description": "Endpoint overrides keyed by platform:endpointKey; values use the same shape as defaults.",
|
|
29
|
+
"x-descriptionZh": "按 platform:endpointKey 覆盖;值的结构与 defaults 相同。"
|
|
30
|
+
},
|
|
31
|
+
"schedule": {
|
|
32
|
+
"$ref": "#/$defs/scheduleFeedback",
|
|
33
|
+
"description": "Schedule-only start, finish, and error feedback.",
|
|
34
|
+
"x-descriptionZh": "仅用于 Schedule 的开始、完成与失败反馈。"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"$defs": {
|
|
38
|
+
"activityFeedback": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"additionalProperties": false,
|
|
41
|
+
"properties": {
|
|
42
|
+
"enabled": {
|
|
43
|
+
"type": "boolean",
|
|
44
|
+
"default": true,
|
|
45
|
+
"description": "Enable this policy layer.",
|
|
46
|
+
"x-descriptionZh": "是否启用当前策略层。"
|
|
47
|
+
},
|
|
48
|
+
"phases": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"additionalProperties": false,
|
|
51
|
+
"description": "Feedback policy keyed by lifecycle phase.",
|
|
52
|
+
"x-descriptionZh": "按生命周期 phase 配置反馈策略。",
|
|
53
|
+
"properties": {
|
|
54
|
+
"queued": { "$ref": "#/$defs/scenePhases" },
|
|
55
|
+
"active": { "$ref": "#/$defs/scenePhases" },
|
|
56
|
+
"thinking": { "$ref": "#/$defs/scenePhases" },
|
|
57
|
+
"schedule_start": { "$ref": "#/$defs/scenePhases" },
|
|
58
|
+
"schedule_finish": { "$ref": "#/$defs/scenePhases" },
|
|
59
|
+
"schedule_error": { "$ref": "#/$defs/scenePhases" }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"scenePhases": {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"additionalProperties": false,
|
|
67
|
+
"properties": {
|
|
68
|
+
"private": { "$ref": "#/$defs/phaseConfig" },
|
|
69
|
+
"group": { "$ref": "#/$defs/phaseConfig" },
|
|
70
|
+
"channel": { "$ref": "#/$defs/phaseConfig" }
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"phaseConfig": {
|
|
74
|
+
"type": "object",
|
|
75
|
+
"additionalProperties": false,
|
|
76
|
+
"properties": {
|
|
77
|
+
"type": {
|
|
78
|
+
"type": "string",
|
|
79
|
+
"enum": ["reaction", "message", "typing", "none"],
|
|
80
|
+
"description": "How the phase is presented.",
|
|
81
|
+
"x-descriptionZh": "该 phase 的呈现方式。"
|
|
82
|
+
},
|
|
83
|
+
"emoji": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"description": "Reaction value for type=reaction.",
|
|
86
|
+
"x-descriptionZh": "type=reaction 时使用的回应值。"
|
|
87
|
+
},
|
|
88
|
+
"message": {
|
|
89
|
+
"type": "string",
|
|
90
|
+
"description": "Status text for type=message.",
|
|
91
|
+
"x-descriptionZh": "type=message 时使用的状态文本。"
|
|
92
|
+
},
|
|
93
|
+
"autoRemove": {
|
|
94
|
+
"type": "boolean",
|
|
95
|
+
"default": true,
|
|
96
|
+
"description": "Remove the feedback after the phase stops.",
|
|
97
|
+
"x-descriptionZh": "phase 停止后是否自动移除反馈。"
|
|
98
|
+
},
|
|
99
|
+
"removeDelay": {
|
|
100
|
+
"type": "number",
|
|
101
|
+
"description": "Delay before removal in milliseconds; negative values are normalized to zero at runtime.",
|
|
102
|
+
"x-descriptionZh": "自动移除前等待的毫秒数;负值会在运行时归一化为 0。"
|
|
103
|
+
},
|
|
104
|
+
"platformConfig": {
|
|
105
|
+
"type": "object",
|
|
106
|
+
"additionalProperties": true,
|
|
107
|
+
"description": "Platform-specific options.",
|
|
108
|
+
"x-descriptionZh": "平台特定选项。"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"scheduleFeedback": {
|
|
113
|
+
"type": "object",
|
|
114
|
+
"additionalProperties": false,
|
|
115
|
+
"properties": {
|
|
116
|
+
"phases": {
|
|
117
|
+
"type": "object",
|
|
118
|
+
"additionalProperties": false,
|
|
119
|
+
"properties": {
|
|
120
|
+
"start": { "$ref": "#/$defs/scenePhases" },
|
|
121
|
+
"finish": { "$ref": "#/$defs/scenePhases" },
|
|
122
|
+
"error": { "$ref": "#/$defs/scenePhases" }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
package/src/ai-event-binder.ts
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
subscribeAIEventsOnTarget,
|
|
3
|
+
activityFeedbackAiBus,
|
|
4
|
+
isActivityFeedbackEnabled,
|
|
5
|
+
type AIEventHandlers,
|
|
6
|
+
type AIEventPayload,
|
|
7
|
+
type AIEventTarget,
|
|
8
|
+
} from '@zhin.js/agent';
|
|
3
9
|
import { loadActivityFeedbackServiceConfig, type ActivityFeedbackServiceConfig } from './config.js';
|
|
10
|
+
import type { GenerationAdmissionGate } from 'zhin.js';
|
|
4
11
|
import {
|
|
5
12
|
ActivityFeedbackExecutor,
|
|
6
|
-
|
|
13
|
+
createNoopEndpointAccess,
|
|
7
14
|
type ActivityFeedbackEndpointAccess,
|
|
8
15
|
} from './executor.js';
|
|
9
16
|
import { ActivityFeedbackOrchestrator } from './orchestrator.js';
|
|
10
17
|
import { ActivityFeedbackPolicy } from './policy.js';
|
|
11
18
|
|
|
12
|
-
export function
|
|
13
|
-
root: Plugin['root'],
|
|
19
|
+
export function createActivityFeedbackAIEventHandlers(
|
|
14
20
|
orchestrator: ActivityFeedbackOrchestrator,
|
|
15
|
-
):
|
|
16
|
-
return
|
|
21
|
+
): AIEventHandlers {
|
|
22
|
+
return {
|
|
17
23
|
onQueuedStart: (payload) => {
|
|
18
24
|
if (!isActivityFeedbackEnabled(payload, 'queued')) return;
|
|
19
25
|
return orchestrator.startPhase(payload, 'queued', 'activity.queued.start');
|
|
@@ -26,7 +32,13 @@ export function bindActivityFeedbackToAIEvents(
|
|
|
26
32
|
onProcessingStart: async (payload) => {
|
|
27
33
|
if (!isActivityFeedbackEnabled(payload, 'active')) return;
|
|
28
34
|
await orchestrator.stopPhase(payload, 'queued', 'processing.start');
|
|
35
|
+
if (typeof payload.iterations === 'number' && payload.iterations > 1) {
|
|
36
|
+
await orchestrator.stopPhase(payload, 'thinking', 'processing.iteration');
|
|
37
|
+
}
|
|
29
38
|
await orchestrator.startPhase(payload, 'active', 'processing.start');
|
|
39
|
+
if (typeof payload.iterations === 'number' && payload.iterations > 1 && payload.content?.trim()) {
|
|
40
|
+
await orchestrator.updatePhaseText(payload, 'active', payload.content.trim());
|
|
41
|
+
}
|
|
30
42
|
},
|
|
31
43
|
|
|
32
44
|
onTypingStart: async () => {},
|
|
@@ -44,9 +56,10 @@ export function bindActivityFeedbackToAIEvents(
|
|
|
44
56
|
await orchestrator.stopPhase(payload, 'active', 'processing.error');
|
|
45
57
|
},
|
|
46
58
|
|
|
47
|
-
onTypingStop: (payload) => {
|
|
59
|
+
onTypingStop: async (payload) => {
|
|
48
60
|
if (!isActivityFeedbackEnabled(payload, 'active')) return;
|
|
49
|
-
|
|
61
|
+
await orchestrator.stopPhase(payload, 'thinking', 'typing.stop');
|
|
62
|
+
await orchestrator.stopPhase(payload, 'active', 'typing.stop');
|
|
50
63
|
},
|
|
51
64
|
|
|
52
65
|
onThinking: async (payload) => {
|
|
@@ -57,46 +70,177 @@ export function bindActivityFeedbackToAIEvents(
|
|
|
57
70
|
await orchestrator.updateThinkingText(payload, payload.thinking);
|
|
58
71
|
},
|
|
59
72
|
|
|
73
|
+
onToolCall: async (payload) => {
|
|
74
|
+
if (!isActivityFeedbackEnabled(payload, 'active') || !payload.toolName?.trim()) return;
|
|
75
|
+
const text = `调用工具:${payload.toolName.trim()}…`;
|
|
76
|
+
await orchestrator.updatePhaseText(payload, 'thinking', text);
|
|
77
|
+
await orchestrator.updatePhaseText(payload, 'active', text);
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
onToolResult: async (payload) => {
|
|
81
|
+
if (!isActivityFeedbackEnabled(payload, 'active') || !payload.toolName?.trim()) return;
|
|
82
|
+
const outcome = payload.status === 'error' || payload.error ? '未完成' : '已完成';
|
|
83
|
+
const text = `工具 ${payload.toolName.trim()} ${outcome},继续处理…`;
|
|
84
|
+
await orchestrator.updatePhaseText(payload, 'thinking', text);
|
|
85
|
+
await orchestrator.updatePhaseText(payload, 'active', text);
|
|
86
|
+
},
|
|
87
|
+
|
|
60
88
|
onSubagentStart: async (payload) => {
|
|
61
89
|
if (!isActivityFeedbackEnabled(payload, 'thinking')) return;
|
|
90
|
+
// The subagent has an isolated session key; remove its own active placeholder
|
|
91
|
+
// before switching to the richer thinking state.
|
|
62
92
|
await orchestrator.stopPhase(payload, 'active', 'subagent.start');
|
|
63
93
|
await orchestrator.startPhase(payload, 'thinking', 'subagent.start');
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
94
|
+
const tag = payload.agentId?.trim() || 'subagent';
|
|
95
|
+
const label = payload.label?.trim()
|
|
96
|
+
? `子任务执行中: ${payload.label.trim()}...`
|
|
97
|
+
: '思考中...';
|
|
98
|
+
await orchestrator.updateThinkingText(payload, `[${tag}] ${label}`);
|
|
68
99
|
},
|
|
69
100
|
|
|
70
101
|
onSubagentFinish: async (payload) => {
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
102
|
+
if (isActivityFeedbackEnabled(payload, 'thinking')) {
|
|
103
|
+
await orchestrator.stopPhase(payload, 'thinking', 'subagent.finish');
|
|
104
|
+
}
|
|
105
|
+
if (isActivityFeedbackEnabled(payload, 'active')) {
|
|
106
|
+
await orchestrator.stopPhase(payload, 'active', 'subagent.finish');
|
|
107
|
+
}
|
|
74
108
|
},
|
|
75
109
|
|
|
76
110
|
onScheduleStart: (payload) => orchestrator.startPhase(payload, 'schedule_start', 'schedule.start'),
|
|
77
|
-
onScheduleFinish: (payload) =>
|
|
111
|
+
onScheduleFinish: async (payload) => {
|
|
112
|
+
await orchestrator.stopPhase(payload, 'schedule_start', 'schedule.finish');
|
|
113
|
+
await orchestrator.showTransientPhase(payload, 'schedule_finish', 'schedule.finish');
|
|
114
|
+
},
|
|
78
115
|
onScheduleError: async (payload) => {
|
|
79
116
|
await orchestrator.stopPhase(payload, 'schedule_start', 'schedule.error');
|
|
80
|
-
await orchestrator.
|
|
117
|
+
await orchestrator.showTransientPhase(payload, 'schedule_error', 'schedule.error');
|
|
81
118
|
},
|
|
82
|
-
}
|
|
119
|
+
};
|
|
83
120
|
}
|
|
84
121
|
|
|
85
|
-
|
|
86
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Plugin Runtime path: subscribe on module-level `activityFeedbackAiBus`
|
|
124
|
+
* (fed by ZhinAgentEventEmitter.emit). No usePlugin / Adapter inject.
|
|
125
|
+
*/
|
|
126
|
+
export function bindActivityFeedbackToAIEventBus(
|
|
87
127
|
orchestrator: ActivityFeedbackOrchestrator,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
128
|
+
admission: GenerationAdmissionGate,
|
|
129
|
+
runWithView: <T>(operation: () => Promise<T>) => Promise<T>,
|
|
130
|
+
): () => Promise<void> {
|
|
131
|
+
let stopWatchingRetirement: (() => void) | undefined;
|
|
132
|
+
let cleanupNeedsGenerationView = false;
|
|
133
|
+
const watchRetirement = () => {
|
|
134
|
+
if (stopWatchingRetirement) return;
|
|
135
|
+
cleanupNeedsGenerationView = true;
|
|
136
|
+
stopWatchingRetirement = admission.onDeactivate(() => {
|
|
137
|
+
void close();
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
const serialized = createGenerationSerializedTarget(
|
|
141
|
+
activityFeedbackAiBus,
|
|
142
|
+
admission,
|
|
143
|
+
runWithView,
|
|
144
|
+
watchRetirement,
|
|
145
|
+
);
|
|
146
|
+
const unsubscribe = subscribeAIEventsOnTarget(
|
|
147
|
+
serialized.target,
|
|
148
|
+
createActivityFeedbackAIEventHandlers(orchestrator),
|
|
149
|
+
);
|
|
150
|
+
let shutdown: Promise<void> | undefined;
|
|
151
|
+
function close(): Promise<void> {
|
|
152
|
+
if (shutdown) return shutdown;
|
|
153
|
+
const cleanup = async () => {
|
|
154
|
+
unsubscribe();
|
|
155
|
+
await serialized.close();
|
|
156
|
+
await orchestrator.dispose();
|
|
157
|
+
};
|
|
158
|
+
// Retirement is published before SnapshotStore changes its current pointer.
|
|
159
|
+
// Enter the IM view synchronously here so timers, native-typing keepalives,
|
|
160
|
+
// and final reaction/message cleanup all remain on this generation's Endpoint.
|
|
161
|
+
// A candidate rolled back before admitting any event has no generation-local
|
|
162
|
+
// feedback state, and its view is unavailable while Root is still idle.
|
|
163
|
+
shutdown = cleanupNeedsGenerationView ? runWithView(cleanup) : cleanup();
|
|
164
|
+
return shutdown;
|
|
165
|
+
}
|
|
166
|
+
return async () => {
|
|
167
|
+
stopWatchingRetirement?.();
|
|
168
|
+
await close();
|
|
169
|
+
};
|
|
94
170
|
}
|
|
95
171
|
|
|
172
|
+
/** Generation-local ordering: one queue per IM session, no module-level runtime state. */
|
|
173
|
+
function createGenerationSerializedTarget(
|
|
174
|
+
source: AIEventTarget,
|
|
175
|
+
admission: GenerationAdmissionGate,
|
|
176
|
+
runWithView: <T>(operation: () => Promise<T>) => Promise<T>,
|
|
177
|
+
onAdmitted: () => void,
|
|
178
|
+
): {
|
|
179
|
+
target: AIEventTarget;
|
|
180
|
+
close(): Promise<void>;
|
|
181
|
+
} {
|
|
182
|
+
const tails = new Map<string, Promise<void>>();
|
|
183
|
+
const wrappers = new Map<(payload: AIEventPayload) => void | Promise<void>,
|
|
184
|
+
(payload: AIEventPayload) => Promise<void>>();
|
|
185
|
+
let closed = false;
|
|
186
|
+
const target: AIEventTarget = {
|
|
187
|
+
on(event, listener) {
|
|
188
|
+
const wrapped = async (payload: AIEventPayload) => {
|
|
189
|
+
const release = admission.acquire();
|
|
190
|
+
if (!release) return;
|
|
191
|
+
onAdmitted();
|
|
192
|
+
try {
|
|
193
|
+
await runWithView(async () => {
|
|
194
|
+
const key = payload.sessionId || '__global__';
|
|
195
|
+
const previous = tails.get(key);
|
|
196
|
+
const run = async () => {
|
|
197
|
+
if (!closed) await listener(payload);
|
|
198
|
+
};
|
|
199
|
+
const current = previous
|
|
200
|
+
? previous.catch(() => undefined).then(run)
|
|
201
|
+
: run();
|
|
202
|
+
tails.set(key, current);
|
|
203
|
+
try {
|
|
204
|
+
await current;
|
|
205
|
+
} finally {
|
|
206
|
+
if (tails.get(key) === current) tails.delete(key);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
} finally {
|
|
210
|
+
release();
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
wrappers.set(listener, wrapped);
|
|
214
|
+
return source.on(event, wrapped);
|
|
215
|
+
},
|
|
216
|
+
off(event, listener) {
|
|
217
|
+
const wrapped = wrappers.get(listener);
|
|
218
|
+
if (!wrapped) return source.off(event, listener);
|
|
219
|
+
wrappers.delete(listener);
|
|
220
|
+
return source.off(event, wrapped);
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
return {
|
|
224
|
+
target,
|
|
225
|
+
async close() {
|
|
226
|
+
closed = true;
|
|
227
|
+
const pending = [...tails.values()];
|
|
228
|
+
await Promise.allSettled(pending);
|
|
229
|
+
tails.clear();
|
|
230
|
+
wrappers.clear();
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export type ActivityFeedbackLogger = {
|
|
236
|
+
debug: (msg: string, ...args: unknown[]) => void;
|
|
237
|
+
error: (msg: string, ...args: unknown[]) => void;
|
|
238
|
+
};
|
|
239
|
+
|
|
96
240
|
export interface CreateActivityFeedbackOrchestratorOptions {
|
|
97
241
|
serviceConfig: ActivityFeedbackServiceConfig;
|
|
98
242
|
access: ActivityFeedbackEndpointAccess;
|
|
99
|
-
logger:
|
|
243
|
+
logger: ActivityFeedbackLogger;
|
|
100
244
|
}
|
|
101
245
|
|
|
102
246
|
export function createActivityFeedbackOrchestrator(
|
|
@@ -107,13 +251,15 @@ export function createActivityFeedbackOrchestrator(
|
|
|
107
251
|
return new ActivityFeedbackOrchestrator(policy, executor, options.logger);
|
|
108
252
|
}
|
|
109
253
|
|
|
110
|
-
|
|
111
|
-
|
|
254
|
+
/** Runtime: prefer OutboundHost-backed access; else noop until Host wires outbound. */
|
|
255
|
+
export function createActivityFeedbackOrchestratorForRuntime(
|
|
112
256
|
serviceConfig: ActivityFeedbackServiceConfig,
|
|
257
|
+
logger: ActivityFeedbackLogger,
|
|
258
|
+
access: ActivityFeedbackEndpointAccess = createNoopEndpointAccess(),
|
|
113
259
|
): ActivityFeedbackOrchestrator {
|
|
114
260
|
return createActivityFeedbackOrchestrator({
|
|
115
261
|
serviceConfig,
|
|
116
|
-
access
|
|
117
|
-
logger
|
|
262
|
+
access,
|
|
263
|
+
logger,
|
|
118
264
|
});
|
|
119
265
|
}
|
package/src/config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ActivityFeedbackConfig } from '@zhin.js/agent';
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/** Plugin Runtime 实例配置(`plugins.<instanceKey>`,与 Adapter endpoint 配置解耦)。 */
|
|
4
4
|
export interface ActivityFeedbackServiceConfig {
|
|
5
5
|
enabled?: boolean;
|
|
6
6
|
defaults?: ActivityFeedbackConfig;
|
|
@@ -66,7 +66,7 @@ export function loadActivityFeedbackServiceConfig(
|
|
|
66
66
|
export function resolveActivityFeedbackForTarget(
|
|
67
67
|
service: ActivityFeedbackServiceConfig,
|
|
68
68
|
platform: string,
|
|
69
|
-
|
|
69
|
+
endpointKey: string,
|
|
70
70
|
): ActivityFeedbackConfig | undefined {
|
|
71
71
|
if (service.enabled === false) {
|
|
72
72
|
return { enabled: false };
|
|
@@ -74,6 +74,6 @@ export function resolveActivityFeedbackForTarget(
|
|
|
74
74
|
return mergeActivityFeedbackLayers(
|
|
75
75
|
service.defaults,
|
|
76
76
|
service.platforms?.[platform],
|
|
77
|
-
service.endpoints?.[`${platform}:${
|
|
77
|
+
service.endpoints?.[`${platform}:${endpointKey}`],
|
|
78
78
|
);
|
|
79
79
|
}
|