@zengxingyuan/aamp-feishu-task-bridge 0.1.1-dev.8
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 +169 -0
- package/dist/ack.d.ts +12 -0
- package/dist/ack.d.ts.map +1 -0
- package/dist/ack.js +41 -0
- package/dist/ack.js.map +1 -0
- package/dist/config.d.ts +41 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +283 -0
- package/dist/config.js.map +1 -0
- package/dist/dispatch.d.ts +11 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +220 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/events.d.ts +3 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +21 -0
- package/dist/events.js.map +1 -0
- package/dist/feishu.d.ts +41 -0
- package/dist/feishu.d.ts.map +1 -0
- package/dist/feishu.js +479 -0
- package/dist/feishu.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +244 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime.d.ts +94 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +1019 -0
- package/dist/runtime.js.map +1 -0
- package/dist/types.d.ts +151 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +30 -0
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,1019 @@
|
|
|
1
|
+
import { AampClient, } from 'aamp-sdk';
|
|
2
|
+
import { buildAckComment, markAckCommented, shouldCommentAck } from './ack.js';
|
|
3
|
+
import { createDefaultBridgeState, FEISHU_BOE_DOMAIN, FEISHU_PRE_DOMAIN, loadBridgeState, saveBridgeState, } from './config.js';
|
|
4
|
+
import { buildFeishuTaskDispatch } from './dispatch.js';
|
|
5
|
+
import { classifyFeishuTaskEvent } from './events.js';
|
|
6
|
+
import { OapiFeishuTaskClient } from './feishu.js';
|
|
7
|
+
const MAX_STREAM_STEPS_PER_TASK = 16;
|
|
8
|
+
const STREAM_STEP_FLUSH_BATCH_SIZE = 4;
|
|
9
|
+
const STREAM_STEP_FLUSH_INTERVAL_MS = 5000;
|
|
10
|
+
function createDebugLogger(logger, enabled) {
|
|
11
|
+
return {
|
|
12
|
+
log: (message) => {
|
|
13
|
+
if (enabled)
|
|
14
|
+
logger.log(message);
|
|
15
|
+
},
|
|
16
|
+
error: (message) => {
|
|
17
|
+
logger.error(message);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function normalizeDomain(domain) {
|
|
22
|
+
return domain?.trim().replace(/\/+$/, '') || undefined;
|
|
23
|
+
}
|
|
24
|
+
function isFeishuBoeDomain(domain) {
|
|
25
|
+
return normalizeDomain(domain) === FEISHU_BOE_DOMAIN;
|
|
26
|
+
}
|
|
27
|
+
function isFeishuPreDomain(domain) {
|
|
28
|
+
return normalizeDomain(domain) === FEISHU_PRE_DOMAIN;
|
|
29
|
+
}
|
|
30
|
+
function getFeishuHeader(headers, headerName) {
|
|
31
|
+
for (const [key, value] of Object.entries(headers ?? {})) {
|
|
32
|
+
if (key.toLowerCase() === headerName)
|
|
33
|
+
return value.trim() || undefined;
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
function getFeishuEnvHeader(headers) {
|
|
38
|
+
return getFeishuHeader(headers, 'x-tt-env');
|
|
39
|
+
}
|
|
40
|
+
function isFeishuPpeHeaderEnabled(headers) {
|
|
41
|
+
return getFeishuHeader(headers, 'x-use-ppe') === '1';
|
|
42
|
+
}
|
|
43
|
+
function buildFeishuTaskDispatchOptions(config) {
|
|
44
|
+
const feishuEnv = getFeishuEnvHeader(config.feishu.headers);
|
|
45
|
+
const ppeEnabled = isFeishuPpeHeaderEnabled(config.feishu.headers);
|
|
46
|
+
const feishuEnvMode = feishuEnv
|
|
47
|
+
? isFeishuBoeDomain(config.feishu.domain)
|
|
48
|
+
? 'boe'
|
|
49
|
+
: isFeishuPreDomain(config.feishu.domain) && ppeEnabled
|
|
50
|
+
? 'pre'
|
|
51
|
+
: ppeEnabled
|
|
52
|
+
? 'ppe'
|
|
53
|
+
: undefined
|
|
54
|
+
: undefined;
|
|
55
|
+
return {
|
|
56
|
+
...(feishuEnv ? { feishuEnv } : {}),
|
|
57
|
+
...(feishuEnvMode ? { feishuEnvMode } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function buildFeishuAgentRegistrationIdentity(config) {
|
|
61
|
+
const env = getFeishuEnvHeader(config.feishu.headers);
|
|
62
|
+
return {
|
|
63
|
+
appId: config.feishu.appId,
|
|
64
|
+
domain: normalizeDomain(config.feishu.domain) ?? 'default',
|
|
65
|
+
...(env ? { env } : {}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function buildFeishuTaskSubscriptionIdentity(config) {
|
|
69
|
+
const env = getFeishuEnvHeader(config.feishu.headers);
|
|
70
|
+
return {
|
|
71
|
+
appId: config.feishu.appId,
|
|
72
|
+
domain: normalizeDomain(config.feishu.domain) ?? 'default',
|
|
73
|
+
...(env ? { env } : {}),
|
|
74
|
+
userIdType: config.feishu.userIdType ?? 'open_id',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function hasMatchingFeishuAgentRegistration(current, expected) {
|
|
78
|
+
return Boolean(current)
|
|
79
|
+
&& current?.appId === expected.appId
|
|
80
|
+
&& current?.domain === expected.domain
|
|
81
|
+
&& (current?.env ?? undefined) === (expected.env ?? undefined);
|
|
82
|
+
}
|
|
83
|
+
function hasMatchingFeishuTaskSubscription(current, expected) {
|
|
84
|
+
return Boolean(current)
|
|
85
|
+
&& current?.appId === expected.appId
|
|
86
|
+
&& current?.domain === expected.domain
|
|
87
|
+
&& (current?.env ?? undefined) === (expected.env ?? undefined)
|
|
88
|
+
&& current?.userIdType === expected.userIdType;
|
|
89
|
+
}
|
|
90
|
+
function describeFeishuAgentRegistration(registration) {
|
|
91
|
+
return [
|
|
92
|
+
`app=${registration.appId}`,
|
|
93
|
+
`domain=${registration.domain}`,
|
|
94
|
+
`env=${registration.env ?? '(none)'}`,
|
|
95
|
+
].join(' ');
|
|
96
|
+
}
|
|
97
|
+
function describeFeishuTaskSubscription(subscription) {
|
|
98
|
+
return [
|
|
99
|
+
`app=${subscription.appId}`,
|
|
100
|
+
`domain=${subscription.domain}`,
|
|
101
|
+
`env=${subscription.env ?? '(none)'}`,
|
|
102
|
+
`userIdType=${subscription.userIdType}`,
|
|
103
|
+
].join(' ');
|
|
104
|
+
}
|
|
105
|
+
const FEISHU_RESULT_MARKER = 'FEISHU_TASK_RESULT_JSON:';
|
|
106
|
+
function asRecord(value) {
|
|
107
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
108
|
+
return undefined;
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
function normalizeResultText(value) {
|
|
112
|
+
return value
|
|
113
|
+
.replace(/\\r\\n/g, '\n')
|
|
114
|
+
.replace(/\\n/g, '\n')
|
|
115
|
+
.replace(/\\r/g, '\n');
|
|
116
|
+
}
|
|
117
|
+
function getString(value) {
|
|
118
|
+
if (typeof value !== 'string')
|
|
119
|
+
return undefined;
|
|
120
|
+
const normalized = normalizeResultText(value).trim();
|
|
121
|
+
return normalized ? normalized : undefined;
|
|
122
|
+
}
|
|
123
|
+
function getBoolean(value) {
|
|
124
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
125
|
+
}
|
|
126
|
+
function getTaskFlowIntent(payload) {
|
|
127
|
+
const taskFlowIntent = getString(payload.task_flow_intent)?.toLowerCase();
|
|
128
|
+
if (taskFlowIntent === 'complete_task' || taskFlowIntent === 'comment_reply')
|
|
129
|
+
return taskFlowIntent;
|
|
130
|
+
const legacyCommentIntent = getString(payload.comment_intent)?.toLowerCase();
|
|
131
|
+
if (legacyCommentIntent === 'rerun')
|
|
132
|
+
return 'complete_task';
|
|
133
|
+
if (legacyCommentIntent === 'reply_only')
|
|
134
|
+
return 'comment_reply';
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
function parseFeishuResultPayload(output) {
|
|
138
|
+
const markerIndex = output.indexOf(FEISHU_RESULT_MARKER);
|
|
139
|
+
if (markerIndex < 0)
|
|
140
|
+
return undefined;
|
|
141
|
+
const jsonText = output.slice(markerIndex + FEISHU_RESULT_MARKER.length).trim();
|
|
142
|
+
if (!jsonText)
|
|
143
|
+
return undefined;
|
|
144
|
+
const parsed = JSON.parse(jsonText);
|
|
145
|
+
return asRecord(parsed);
|
|
146
|
+
}
|
|
147
|
+
function classifyTaskResult(result) {
|
|
148
|
+
const output = result.output.trim();
|
|
149
|
+
if (result.status === 'rejected') {
|
|
150
|
+
return {
|
|
151
|
+
kind: 'failure',
|
|
152
|
+
message: result.errorMsg?.trim() || output || 'ACP agent rejected the task without an error message.',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
let payload;
|
|
156
|
+
try {
|
|
157
|
+
payload = parseFeishuResultPayload(output);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
return {
|
|
161
|
+
kind: 'failure',
|
|
162
|
+
message: `智能体返回了无法解析的 FEISHU_TASK_RESULT_JSON:${error instanceof Error ? error.message : String(error)}`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (payload) {
|
|
166
|
+
const status = getString(payload.status)?.toLowerCase();
|
|
167
|
+
const summary = getString(payload.summary);
|
|
168
|
+
const deliverableSummary = getString(payload.deliverable_summary) ?? getString(payload.delivery);
|
|
169
|
+
const error = getString(payload.error);
|
|
170
|
+
const question = getString(payload.question);
|
|
171
|
+
const helpNeeded = getBoolean(payload.help_needed);
|
|
172
|
+
const taskFlowIntent = getTaskFlowIntent(payload);
|
|
173
|
+
if (status === 'answered') {
|
|
174
|
+
return { kind: 'answered', ...(summary ? { summary } : {}), ...(taskFlowIntent ? { taskFlowIntent } : {}) };
|
|
175
|
+
}
|
|
176
|
+
if (status === 'success') {
|
|
177
|
+
return {
|
|
178
|
+
kind: 'success',
|
|
179
|
+
summary: summary ?? deliverableSummary ?? '已完成交付物处理。',
|
|
180
|
+
...(deliverableSummary ? { deliverableSummary } : {}),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
if (status === 'need_help' || status === 'help_needed' || helpNeeded === true) {
|
|
184
|
+
return { kind: 'help_needed', message: question ?? summary ?? error ?? '智能体需要更多信息才能继续处理该任务。' };
|
|
185
|
+
}
|
|
186
|
+
if (status === 'failure' || status === 'failed' || error) {
|
|
187
|
+
return {
|
|
188
|
+
kind: 'failure',
|
|
189
|
+
...(summary ? { summary } : {}),
|
|
190
|
+
message: error ?? summary ?? '智能体报告任务执行失败,但未提供具体错误。',
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
kind: 'failure',
|
|
195
|
+
message: `智能体返回了未知 FEISHU_TASK_RESULT_JSON.status:${status ?? '(missing)'}`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
kind: 'failure',
|
|
200
|
+
message: output
|
|
201
|
+
? `智能体返回了非预期结果,未按 FEISHU_TASK_RESULT_JSON 协议收尾:${output}`
|
|
202
|
+
: '智能体返回了空结果,未按 FEISHU_TASK_RESULT_JSON 协议收尾。',
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function getTaskCreateIgnoreReason(eventKind, task) {
|
|
206
|
+
if (eventKind !== 'task_create')
|
|
207
|
+
return undefined;
|
|
208
|
+
if (task.parentGuid)
|
|
209
|
+
return 'subtask_create_context_only';
|
|
210
|
+
if (task.reminders?.length)
|
|
211
|
+
return 'task_create_deferred_to_reminder';
|
|
212
|
+
if (task.rrule)
|
|
213
|
+
return 'recurring_task_create_deferred';
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
function normalizeStepText(content) {
|
|
217
|
+
return content.trim().replace(/\s+/g, ' ').toLowerCase();
|
|
218
|
+
}
|
|
219
|
+
const IGNORED_STREAM_STEP_TEXTS = new Set([
|
|
220
|
+
'ACP task started',
|
|
221
|
+
'Prompt sent to ACP agent',
|
|
222
|
+
'ACP agent is composing the reply',
|
|
223
|
+
'ACP response received',
|
|
224
|
+
].map(normalizeStepText));
|
|
225
|
+
function getPayloadText(payload, keys) {
|
|
226
|
+
for (const key of keys) {
|
|
227
|
+
const value = getString(payload[key]);
|
|
228
|
+
if (value)
|
|
229
|
+
return value;
|
|
230
|
+
}
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
function streamEventToTaskStep(event) {
|
|
234
|
+
if (event.type === 'status') {
|
|
235
|
+
return getPayloadText(event.payload, ['label', 'stage', 'status', 'message', 'text']);
|
|
236
|
+
}
|
|
237
|
+
if (event.type === 'progress') {
|
|
238
|
+
return getPayloadText(event.payload, ['label', 'stage', 'message', 'text']);
|
|
239
|
+
}
|
|
240
|
+
if (event.type === 'error') {
|
|
241
|
+
const message = getPayloadText(event.payload, ['message', 'error', 'reason']);
|
|
242
|
+
return message ? `执行遇到错误:${message}` : '执行遇到错误。';
|
|
243
|
+
}
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
export class FeishuTaskBridgeRuntime {
|
|
247
|
+
config;
|
|
248
|
+
configDir;
|
|
249
|
+
logger;
|
|
250
|
+
aamp;
|
|
251
|
+
feishu;
|
|
252
|
+
forceRegisterAgent;
|
|
253
|
+
streamStepFlushIntervalMs;
|
|
254
|
+
state = createDefaultBridgeState();
|
|
255
|
+
ackCommentInFlight = new Set();
|
|
256
|
+
helpCommentInFlight = new Set();
|
|
257
|
+
resultInFlight = new Set();
|
|
258
|
+
feishuCompleteInFlight = new Set();
|
|
259
|
+
feishuBlockInFlight = new Set();
|
|
260
|
+
activeStreamSubscriptions = new Map();
|
|
261
|
+
streamEventQueues = new Map();
|
|
262
|
+
streamStepBuffers = new Map();
|
|
263
|
+
streamStepFlushQueues = new Map();
|
|
264
|
+
stopping = false;
|
|
265
|
+
constructor(config, options = {}) {
|
|
266
|
+
this.config = config;
|
|
267
|
+
this.configDir = options.configDir;
|
|
268
|
+
this.logger = options.logger ?? console;
|
|
269
|
+
this.forceRegisterAgent = Boolean(options.forceRegisterAgent);
|
|
270
|
+
this.streamStepFlushIntervalMs = options.streamStepFlushIntervalMs ?? STREAM_STEP_FLUSH_INTERVAL_MS;
|
|
271
|
+
this.aamp = options.aampClient ?? new AampClient({
|
|
272
|
+
email: config.mailbox.email,
|
|
273
|
+
mailboxToken: config.mailbox.mailboxToken,
|
|
274
|
+
smtpPassword: config.mailbox.smtpPassword,
|
|
275
|
+
baseUrl: config.mailbox.baseUrl,
|
|
276
|
+
});
|
|
277
|
+
this.feishu = options.feishuClient ?? new OapiFeishuTaskClient(config.feishu, {
|
|
278
|
+
logger: createDebugLogger(this.logger, Boolean(config.behavior.debug)),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
async start() {
|
|
282
|
+
this.state = await loadBridgeState(this.configDir);
|
|
283
|
+
this.state.lastStartedAt = new Date().toISOString();
|
|
284
|
+
this.state.lastError = undefined;
|
|
285
|
+
this.setConnectivity('aamp', 'connecting');
|
|
286
|
+
this.setConnectivity('feishu', 'connecting');
|
|
287
|
+
this.logger.log([
|
|
288
|
+
'[bridge] starting',
|
|
289
|
+
`target=${this.config.targetAgentEmail}`,
|
|
290
|
+
`mailbox=${this.config.mailbox.email}`,
|
|
291
|
+
`events=${this.config.feishu.eventNames.join(',')}`,
|
|
292
|
+
`taskApi=${this.config.feishu.taskApiVersion}`,
|
|
293
|
+
`ackComment=${this.config.behavior.ackComment ? 'on' : 'off'}`,
|
|
294
|
+
`debug=${this.config.behavior.debug ? 'on' : 'off'}`,
|
|
295
|
+
].join(' '));
|
|
296
|
+
await this.ensureFeishuAgentRegistered();
|
|
297
|
+
await this.ensureFeishuTaskEventsSubscribed();
|
|
298
|
+
this.registerAampHandlers();
|
|
299
|
+
await this.aamp.connect();
|
|
300
|
+
this.setConnectivity('aamp', 'connected');
|
|
301
|
+
await this.feishu.start(async (event) => {
|
|
302
|
+
await this.handleFeishuTaskEvent(event);
|
|
303
|
+
});
|
|
304
|
+
this.setConnectivity('feishu', 'connected');
|
|
305
|
+
this.logger.log(`[feishu] listener started events=${this.config.feishu.eventNames.join(',')}`);
|
|
306
|
+
await this.aamp.updateDirectoryProfile?.({
|
|
307
|
+
summary: `Feishu task bridge mailbox for ${this.config.targetAgentEmail}`,
|
|
308
|
+
cardText: [
|
|
309
|
+
'This mailbox belongs to a local Feishu task bridge.',
|
|
310
|
+
`Target AAMP Agent: ${this.config.targetAgentEmail}`,
|
|
311
|
+
'Dispatch source: feishu-task',
|
|
312
|
+
].join('\n'),
|
|
313
|
+
}).catch(() => { });
|
|
314
|
+
await this.persistState();
|
|
315
|
+
}
|
|
316
|
+
async stop() {
|
|
317
|
+
if (this.stopping)
|
|
318
|
+
return;
|
|
319
|
+
this.stopping = true;
|
|
320
|
+
for (const subscription of this.activeStreamSubscriptions.values()) {
|
|
321
|
+
subscription.close();
|
|
322
|
+
}
|
|
323
|
+
this.activeStreamSubscriptions.clear();
|
|
324
|
+
await Promise.allSettled([...this.streamEventQueues.values()]);
|
|
325
|
+
this.streamEventQueues.clear();
|
|
326
|
+
await this.flushAllStreamStepBuffers();
|
|
327
|
+
await Promise.allSettled([...this.streamStepFlushQueues.values()]);
|
|
328
|
+
this.clearAllStreamStepFlushTimers();
|
|
329
|
+
await this.feishu.stop().catch(() => { });
|
|
330
|
+
this.aamp.disconnect();
|
|
331
|
+
this.state.lastStoppedAt = new Date().toISOString();
|
|
332
|
+
this.setConnectivity('aamp', 'disconnected');
|
|
333
|
+
this.setConnectivity('feishu', 'disconnected');
|
|
334
|
+
await this.persistState();
|
|
335
|
+
}
|
|
336
|
+
getStateSnapshot() {
|
|
337
|
+
return structuredClone(this.state);
|
|
338
|
+
}
|
|
339
|
+
async ensureFeishuAgentRegistered() {
|
|
340
|
+
const expected = buildFeishuAgentRegistrationIdentity(this.config);
|
|
341
|
+
if (!this.forceRegisterAgent && hasMatchingFeishuAgentRegistration(this.state.agentRegistration, expected)) {
|
|
342
|
+
this.logger.log(`[feishu agent] registration cached ${describeFeishuAgentRegistration(expected)}`);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
this.logger.log(`[feishu agent] registering ${describeFeishuAgentRegistration(expected)}`);
|
|
346
|
+
await this.feishu.registerAgent();
|
|
347
|
+
this.state.agentRegistration = {
|
|
348
|
+
...expected,
|
|
349
|
+
registeredAt: new Date().toISOString(),
|
|
350
|
+
};
|
|
351
|
+
await this.persistState();
|
|
352
|
+
this.logger.log(`[feishu agent] registered ${describeFeishuAgentRegistration(expected)}`);
|
|
353
|
+
}
|
|
354
|
+
async ensureFeishuTaskEventsSubscribed() {
|
|
355
|
+
const expected = buildFeishuTaskSubscriptionIdentity(this.config);
|
|
356
|
+
if (hasMatchingFeishuTaskSubscription(this.state.taskSubscription, expected)) {
|
|
357
|
+
this.logger.log(`[feishu task subscription] cached ${describeFeishuTaskSubscription(expected)}`);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
this.logger.log(`[feishu task subscription] subscribing ${describeFeishuTaskSubscription(expected)}`);
|
|
361
|
+
await this.feishu.subscribeTaskEvents();
|
|
362
|
+
this.state.taskSubscription = {
|
|
363
|
+
...expected,
|
|
364
|
+
subscribedAt: new Date().toISOString(),
|
|
365
|
+
};
|
|
366
|
+
await this.persistState();
|
|
367
|
+
this.logger.log(`[feishu task subscription] subscribed ${describeFeishuTaskSubscription(expected)}`);
|
|
368
|
+
}
|
|
369
|
+
registerAampHandlers() {
|
|
370
|
+
this.aamp.on('connected', () => {
|
|
371
|
+
this.setConnectivity('aamp', 'connected');
|
|
372
|
+
this.logger.log('[aamp] connected');
|
|
373
|
+
this.debugLog(`[aamp] connected mailbox=${this.config.mailbox.email}`);
|
|
374
|
+
void this.persistState();
|
|
375
|
+
});
|
|
376
|
+
this.aamp.on('disconnected', (reason) => {
|
|
377
|
+
this.setConnectivity('aamp', 'disconnected');
|
|
378
|
+
this.logger.log(`[aamp] disconnected${reason ? ` reason=${reason}` : ''}`);
|
|
379
|
+
void this.persistState();
|
|
380
|
+
});
|
|
381
|
+
this.aamp.on('error', (error) => {
|
|
382
|
+
this.state.lastError = error.message;
|
|
383
|
+
this.logger.error(`[aamp] ${error.message}`);
|
|
384
|
+
void this.persistState();
|
|
385
|
+
});
|
|
386
|
+
this.aamp.on('task.ack', (ack) => {
|
|
387
|
+
this.state.lastAampAckAt = new Date().toISOString();
|
|
388
|
+
this.state.lastAampAckTaskId = ack.taskId;
|
|
389
|
+
this.logger.log(`[aamp ack] received task=${ack.taskId}`);
|
|
390
|
+
this.debugLog(`[aamp ack ${ack.taskId}] received from=${ack.from}`);
|
|
391
|
+
void this.handleTaskAck(ack).catch((error) => {
|
|
392
|
+
this.state.lastError = error.message;
|
|
393
|
+
this.logger.error(`[aamp ack ${ack.taskId}] ${error.message}`);
|
|
394
|
+
void this.persistState();
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
this.aamp.on('task.stream.opened', (stream) => {
|
|
398
|
+
this.logger.log(`[aamp stream] opened task=${stream.taskId} stream=${stream.streamId}`);
|
|
399
|
+
this.debugLog(`[aamp stream ${stream.taskId}] opened stream=${stream.streamId} from=${stream.from}`);
|
|
400
|
+
void this.handleTaskStreamOpened(stream).catch((error) => {
|
|
401
|
+
this.state.lastError = error.message;
|
|
402
|
+
this.logger.error(`[aamp stream ${stream.taskId}] ${error.message}`);
|
|
403
|
+
void this.persistState();
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
this.aamp.on('task.help_needed', (help) => {
|
|
407
|
+
this.state.lastAampHelpAt = new Date().toISOString();
|
|
408
|
+
this.state.lastAampHelpTaskId = help.taskId;
|
|
409
|
+
this.logger.log(`[aamp help] received task=${help.taskId}`);
|
|
410
|
+
this.debugLog(`[aamp help ${help.taskId}] received from=${help.from}`);
|
|
411
|
+
void this.handleTaskHelp(help).catch((error) => {
|
|
412
|
+
this.state.lastError = error.message;
|
|
413
|
+
this.logger.error(`[aamp help ${help.taskId}] ${error.message}`);
|
|
414
|
+
void this.persistState();
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
this.aamp.on('task.result', (result) => {
|
|
418
|
+
this.state.lastAampResultAt = new Date().toISOString();
|
|
419
|
+
this.state.lastAampResultTaskId = result.taskId;
|
|
420
|
+
this.logger.log(`[aamp result] received task=${result.taskId} status=${result.status}`);
|
|
421
|
+
this.debugLog(`[aamp result ${result.taskId}] received from=${result.from} status=${result.status}`);
|
|
422
|
+
void this.handleTaskResult(result).catch((error) => {
|
|
423
|
+
this.state.lastError = error.message;
|
|
424
|
+
this.logger.error(`[aamp result ${result.taskId}] ${error.message}`);
|
|
425
|
+
void this.persistState();
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
async ignoreFeishuTaskEvent(event, reason) {
|
|
430
|
+
this.logger.log(`[feishu event] ignored task=${event.taskGuid} types=${event.eventTypes.join(',') || '(unknown)'} reason=${reason}`);
|
|
431
|
+
this.debugLog(`[feishu event ${event.eventId}] ignored reason=${reason}`);
|
|
432
|
+
this.state.lastIgnoredFeishuEventAt = new Date().toISOString();
|
|
433
|
+
this.state.lastIgnoredFeishuEventId = event.eventId;
|
|
434
|
+
this.state.lastIgnoredFeishuEventTaskGuid = event.taskGuid;
|
|
435
|
+
this.state.lastIgnoredFeishuEventTypes = event.eventTypes;
|
|
436
|
+
this.state.lastIgnoredFeishuEventReason = reason;
|
|
437
|
+
await this.persistState();
|
|
438
|
+
}
|
|
439
|
+
async handleFeishuTaskEvent(event) {
|
|
440
|
+
this.state.lastFeishuEventAt = new Date().toISOString();
|
|
441
|
+
this.state.lastFeishuEventId = event.eventId;
|
|
442
|
+
this.state.lastFeishuEventTaskGuid = event.taskGuid;
|
|
443
|
+
this.logger.log(`[feishu event] received task=${event.taskGuid}`);
|
|
444
|
+
this.debugLog(`[feishu event ${event.eventId}] received task=${event.taskGuid} types=${event.eventTypes.join(',') || '(unknown)'}`);
|
|
445
|
+
if (!this.rememberEvent(event)) {
|
|
446
|
+
this.debugLog(`[feishu event ${event.eventId}] duplicate ignored`);
|
|
447
|
+
await this.persistState();
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const eventKind = classifyFeishuTaskEvent(event.eventTypes);
|
|
451
|
+
if (!eventKind) {
|
|
452
|
+
await this.ignoreFeishuTaskEvent(event, 'event_type_not_allowlisted');
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
let taskState;
|
|
456
|
+
try {
|
|
457
|
+
this.debugLog(`[feishu task ${event.taskGuid}] loading details`);
|
|
458
|
+
const task = await this.feishu.getTask(event.taskGuid);
|
|
459
|
+
this.debugLog(`[feishu task ${task.guid}] loaded summary="${task.summary}" children=${task.subtasks?.length ?? 0}`);
|
|
460
|
+
const ignoreReason = getTaskCreateIgnoreReason(eventKind, task);
|
|
461
|
+
if (ignoreReason) {
|
|
462
|
+
await this.ignoreFeishuTaskEvent(event, ignoreReason);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const dispatch = buildFeishuTaskDispatch(event, task, eventKind, {
|
|
466
|
+
...buildFeishuTaskDispatchOptions(this.config),
|
|
467
|
+
});
|
|
468
|
+
const now = new Date().toISOString();
|
|
469
|
+
taskState = {
|
|
470
|
+
taskGuid: task.guid,
|
|
471
|
+
aampTaskId: dispatch.taskId,
|
|
472
|
+
feishuEventId: event.eventId,
|
|
473
|
+
feishuEventKind: eventKind,
|
|
474
|
+
...(task.taskId ? { feishuTaskId: task.taskId } : {}),
|
|
475
|
+
...(task.subtasks?.length ? { childTaskGuids: task.subtasks.map((subtask) => subtask.guid) } : {}),
|
|
476
|
+
status: 'dispatching',
|
|
477
|
+
createdAt: now,
|
|
478
|
+
updatedAt: now,
|
|
479
|
+
};
|
|
480
|
+
this.state.tasks[dispatch.taskId] = taskState;
|
|
481
|
+
await this.persistState();
|
|
482
|
+
this.debugLog([
|
|
483
|
+
`[aamp dispatch ${dispatch.taskId}] sending`,
|
|
484
|
+
`to=${this.config.targetAgentEmail}`,
|
|
485
|
+
`session=${dispatch.sessionKey}`,
|
|
486
|
+
`source=${dispatch.dispatchContext.source ?? '(none)'}`,
|
|
487
|
+
`event_kind=${dispatch.dispatchContext.feishu_event_kind ?? '(none)'}`,
|
|
488
|
+
`event_types=${dispatch.dispatchContext.feishu_task_event_types ?? '(none)'}`,
|
|
489
|
+
].join(' '));
|
|
490
|
+
const result = await this.aamp.sendTask({
|
|
491
|
+
to: this.config.targetAgentEmail,
|
|
492
|
+
taskId: dispatch.taskId,
|
|
493
|
+
sessionKey: dispatch.sessionKey,
|
|
494
|
+
title: dispatch.title,
|
|
495
|
+
bodyText: dispatch.bodyText,
|
|
496
|
+
rawBodyText: dispatch.bodyText,
|
|
497
|
+
dispatchContext: dispatch.dispatchContext,
|
|
498
|
+
promptRules: dispatch.promptRules,
|
|
499
|
+
});
|
|
500
|
+
this.state.tasks[dispatch.taskId] = {
|
|
501
|
+
...taskState,
|
|
502
|
+
aampMessageId: result.messageId,
|
|
503
|
+
status: 'dispatched',
|
|
504
|
+
updatedAt: new Date().toISOString(),
|
|
505
|
+
};
|
|
506
|
+
this.state.lastAampDispatchAt = new Date().toISOString();
|
|
507
|
+
this.state.lastAampDispatchTaskId = dispatch.taskId;
|
|
508
|
+
await this.persistState();
|
|
509
|
+
this.logger.log(`[aamp dispatch] sent task=${dispatch.taskId}`);
|
|
510
|
+
this.debugLog(`[aamp dispatch ${dispatch.taskId}] sent message=${result.messageId}`);
|
|
511
|
+
}
|
|
512
|
+
catch (error) {
|
|
513
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
514
|
+
this.state.lastError = message;
|
|
515
|
+
if (taskState) {
|
|
516
|
+
this.state.tasks[taskState.aampTaskId] = {
|
|
517
|
+
...taskState,
|
|
518
|
+
status: 'failed',
|
|
519
|
+
lastError: message,
|
|
520
|
+
updatedAt: new Date().toISOString(),
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
await this.persistState();
|
|
524
|
+
throw error;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
async handleTaskStreamOpened(stream) {
|
|
528
|
+
const taskState = this.state.tasks[stream.taskId];
|
|
529
|
+
if (!taskState)
|
|
530
|
+
return;
|
|
531
|
+
this.state.tasks[stream.taskId] = {
|
|
532
|
+
...taskState,
|
|
533
|
+
streamId: stream.streamId,
|
|
534
|
+
updatedAt: new Date().toISOString(),
|
|
535
|
+
};
|
|
536
|
+
await this.persistState();
|
|
537
|
+
await this.subscribeToTaskStream(stream.taskId, stream.streamId);
|
|
538
|
+
}
|
|
539
|
+
async subscribeToTaskStream(aampTaskId, streamId) {
|
|
540
|
+
if (!this.aamp.subscribeStream) {
|
|
541
|
+
this.debugLog(`[aamp stream ${aampTaskId}] subscribeStream unavailable`);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (this.activeStreamSubscriptions.has(aampTaskId))
|
|
545
|
+
return;
|
|
546
|
+
const latestTaskState = this.state.tasks[aampTaskId];
|
|
547
|
+
const subscription = await this.aamp.subscribeStream(streamId, {
|
|
548
|
+
onEvent: (event) => {
|
|
549
|
+
this.enqueueStreamEvent(aampTaskId, event);
|
|
550
|
+
},
|
|
551
|
+
onError: (error) => {
|
|
552
|
+
this.state.lastError = error.message;
|
|
553
|
+
this.logger.error(`[aamp stream ${aampTaskId}] ${error.message}`);
|
|
554
|
+
void this.persistState();
|
|
555
|
+
},
|
|
556
|
+
}, latestTaskState?.lastStreamEventId ? { lastEventId: latestTaskState.lastStreamEventId } : {});
|
|
557
|
+
this.activeStreamSubscriptions.set(aampTaskId, subscription);
|
|
558
|
+
}
|
|
559
|
+
enqueueStreamEvent(aampTaskId, event) {
|
|
560
|
+
const previous = this.streamEventQueues.get(aampTaskId) ?? Promise.resolve();
|
|
561
|
+
const next = previous
|
|
562
|
+
.catch(() => { })
|
|
563
|
+
.then(() => this.handleStreamEvent(aampTaskId, event))
|
|
564
|
+
.catch((error) => {
|
|
565
|
+
this.state.lastError = error.message;
|
|
566
|
+
this.logger.error(`[aamp stream ${aampTaskId}] ${error.message}`);
|
|
567
|
+
void this.persistState();
|
|
568
|
+
});
|
|
569
|
+
this.streamEventQueues.set(aampTaskId, next);
|
|
570
|
+
void next.finally(() => {
|
|
571
|
+
if (this.streamEventQueues.get(aampTaskId) === next) {
|
|
572
|
+
this.streamEventQueues.delete(aampTaskId);
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
async handleStreamEvent(aampTaskId, event) {
|
|
577
|
+
const taskState = this.state.tasks[aampTaskId];
|
|
578
|
+
if (!taskState)
|
|
579
|
+
return;
|
|
580
|
+
const baseState = {
|
|
581
|
+
...taskState,
|
|
582
|
+
...(event.id ? { lastStreamEventId: event.id } : {}),
|
|
583
|
+
updatedAt: new Date().toISOString(),
|
|
584
|
+
};
|
|
585
|
+
this.state.tasks[aampTaskId] = baseState;
|
|
586
|
+
const stepContent = streamEventToTaskStep(event);
|
|
587
|
+
if (!stepContent) {
|
|
588
|
+
await this.persistState();
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const normalized = normalizeStepText(stepContent);
|
|
592
|
+
if (IGNORED_STREAM_STEP_TEXTS.has(normalized)) {
|
|
593
|
+
await this.persistState();
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
const streamStepTexts = new Set(baseState.streamStepTexts ?? []);
|
|
597
|
+
const buffer = this.getStreamStepBuffer(aampTaskId);
|
|
598
|
+
const pendingStepCount = buffer.steps.length;
|
|
599
|
+
if (streamStepTexts.has(normalized)
|
|
600
|
+
|| buffer.steps.some((step) => step.normalized === normalized)
|
|
601
|
+
|| (baseState.streamStepCount ?? 0) + pendingStepCount >= MAX_STREAM_STEPS_PER_TASK) {
|
|
602
|
+
await this.persistState();
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
buffer.steps.push({ content: stepContent, normalized });
|
|
606
|
+
if (buffer.steps.length >= STREAM_STEP_FLUSH_BATCH_SIZE) {
|
|
607
|
+
await this.enqueueStreamStepFlush(aampTaskId);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
this.scheduleStreamStepFlush(aampTaskId);
|
|
611
|
+
await this.persistState();
|
|
612
|
+
}
|
|
613
|
+
getStreamStepBuffer(aampTaskId) {
|
|
614
|
+
const existing = this.streamStepBuffers.get(aampTaskId);
|
|
615
|
+
if (existing)
|
|
616
|
+
return existing;
|
|
617
|
+
const buffer = { steps: [] };
|
|
618
|
+
this.streamStepBuffers.set(aampTaskId, buffer);
|
|
619
|
+
return buffer;
|
|
620
|
+
}
|
|
621
|
+
scheduleStreamStepFlush(aampTaskId) {
|
|
622
|
+
if (this.stopping)
|
|
623
|
+
return;
|
|
624
|
+
const buffer = this.streamStepBuffers.get(aampTaskId);
|
|
625
|
+
if (!buffer || buffer.steps.length === 0 || buffer.timer)
|
|
626
|
+
return;
|
|
627
|
+
buffer.timer = setTimeout(() => {
|
|
628
|
+
const latestBuffer = this.streamStepBuffers.get(aampTaskId);
|
|
629
|
+
if (latestBuffer)
|
|
630
|
+
delete latestBuffer.timer;
|
|
631
|
+
void this.enqueueStreamStepFlush(aampTaskId);
|
|
632
|
+
}, this.streamStepFlushIntervalMs);
|
|
633
|
+
buffer.timer.unref?.();
|
|
634
|
+
}
|
|
635
|
+
clearStreamStepFlushTimer(aampTaskId) {
|
|
636
|
+
const buffer = this.streamStepBuffers.get(aampTaskId);
|
|
637
|
+
if (!buffer?.timer)
|
|
638
|
+
return;
|
|
639
|
+
clearTimeout(buffer.timer);
|
|
640
|
+
delete buffer.timer;
|
|
641
|
+
}
|
|
642
|
+
clearAllStreamStepFlushTimers() {
|
|
643
|
+
for (const aampTaskId of this.streamStepBuffers.keys()) {
|
|
644
|
+
this.clearStreamStepFlushTimer(aampTaskId);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
async enqueueStreamStepFlush(aampTaskId) {
|
|
648
|
+
this.clearStreamStepFlushTimer(aampTaskId);
|
|
649
|
+
const previous = this.streamStepFlushQueues.get(aampTaskId) ?? Promise.resolve();
|
|
650
|
+
const next = previous
|
|
651
|
+
.catch(() => { })
|
|
652
|
+
.then(() => this.flushStreamStepBuffer(aampTaskId))
|
|
653
|
+
.catch((error) => {
|
|
654
|
+
this.state.lastError = error.message;
|
|
655
|
+
this.logger.error(`[aamp stream ${aampTaskId}] ${error.message}`);
|
|
656
|
+
this.scheduleStreamStepFlush(aampTaskId);
|
|
657
|
+
void this.persistState();
|
|
658
|
+
});
|
|
659
|
+
this.streamStepFlushQueues.set(aampTaskId, next);
|
|
660
|
+
void next.finally(() => {
|
|
661
|
+
if (this.streamStepFlushQueues.get(aampTaskId) === next) {
|
|
662
|
+
this.streamStepFlushQueues.delete(aampTaskId);
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
await next;
|
|
666
|
+
}
|
|
667
|
+
async flushAllStreamStepBuffers() {
|
|
668
|
+
await Promise.all([...this.streamStepBuffers.keys()].map((aampTaskId) => this.enqueueStreamStepFlush(aampTaskId)));
|
|
669
|
+
}
|
|
670
|
+
async flushStreamStepBuffer(aampTaskId) {
|
|
671
|
+
const buffer = this.streamStepBuffers.get(aampTaskId);
|
|
672
|
+
if (!buffer || buffer.steps.length === 0) {
|
|
673
|
+
this.streamStepBuffers.delete(aampTaskId);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
const taskState = this.state.tasks[aampTaskId];
|
|
677
|
+
if (!taskState) {
|
|
678
|
+
this.streamStepBuffers.delete(aampTaskId);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
const stepsToFlush = buffer.steps.slice();
|
|
682
|
+
await this.feishu.appendTaskSteps(taskState.taskGuid, stepsToFlush.map((step) => step.content));
|
|
683
|
+
const latestBuffer = this.streamStepBuffers.get(aampTaskId);
|
|
684
|
+
if (latestBuffer) {
|
|
685
|
+
latestBuffer.steps = latestBuffer.steps.slice(stepsToFlush.length);
|
|
686
|
+
if (latestBuffer.steps.length === 0) {
|
|
687
|
+
this.streamStepBuffers.delete(aampTaskId);
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
this.scheduleStreamStepFlush(aampTaskId);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
const latestTaskState = this.state.tasks[aampTaskId] ?? taskState;
|
|
694
|
+
const latestStreamStepTexts = new Set(latestTaskState.streamStepTexts ?? []);
|
|
695
|
+
for (const step of stepsToFlush) {
|
|
696
|
+
latestStreamStepTexts.add(step.normalized);
|
|
697
|
+
}
|
|
698
|
+
this.state.tasks[aampTaskId] = {
|
|
699
|
+
...latestTaskState,
|
|
700
|
+
streamStepCount: (latestTaskState.streamStepCount ?? 0) + stepsToFlush.length,
|
|
701
|
+
streamStepTexts: [...latestStreamStepTexts],
|
|
702
|
+
updatedAt: new Date().toISOString(),
|
|
703
|
+
};
|
|
704
|
+
await this.persistState();
|
|
705
|
+
this.debugLog(`[aamp stream ${aampTaskId}] appended ${stepsToFlush.length} Feishu step(s)`);
|
|
706
|
+
}
|
|
707
|
+
async handleTaskAck(ack) {
|
|
708
|
+
if (this.ackCommentInFlight.has(ack.taskId))
|
|
709
|
+
return;
|
|
710
|
+
const taskState = this.state.tasks[ack.taskId];
|
|
711
|
+
if (!taskState)
|
|
712
|
+
return;
|
|
713
|
+
if (!this.config.behavior.ackComment) {
|
|
714
|
+
this.logger.log(`[aamp ack ${ack.taskId}] ack comment disabled`);
|
|
715
|
+
this.state.tasks[ack.taskId] = {
|
|
716
|
+
...taskState,
|
|
717
|
+
status: 'acknowledged',
|
|
718
|
+
updatedAt: new Date().toISOString(),
|
|
719
|
+
};
|
|
720
|
+
await this.persistState();
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
if (!shouldCommentAck(taskState, ack.taskId)) {
|
|
724
|
+
this.logger.log(`[aamp ack ${ack.taskId}] comment already recorded`);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
this.ackCommentInFlight.add(ack.taskId);
|
|
728
|
+
try {
|
|
729
|
+
this.debugLog(`[aamp ack ${ack.taskId}] commenting on Feishu task ${taskState.taskGuid}`);
|
|
730
|
+
await this.feishu.commentTask(taskState.taskGuid, buildAckComment({
|
|
731
|
+
aampTaskId: ack.taskId,
|
|
732
|
+
bridgeName: this.config.slug,
|
|
733
|
+
eventKind: taskState.feishuEventKind,
|
|
734
|
+
debug: this.config.behavior.debug,
|
|
735
|
+
}));
|
|
736
|
+
this.state.tasks[ack.taskId] = markAckCommented(taskState, ack.taskId);
|
|
737
|
+
await this.persistState();
|
|
738
|
+
this.logger.log(`[aamp ack] commented task=${ack.taskId}`);
|
|
739
|
+
this.debugLog(`[aamp ack ${ack.taskId}] commented on Feishu task ${taskState.taskGuid}`);
|
|
740
|
+
}
|
|
741
|
+
finally {
|
|
742
|
+
this.ackCommentInFlight.delete(ack.taskId);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
async handleTaskHelp(help) {
|
|
746
|
+
if (this.helpCommentInFlight.has(help.taskId))
|
|
747
|
+
return;
|
|
748
|
+
const taskState = this.state.tasks[help.taskId];
|
|
749
|
+
if (!taskState)
|
|
750
|
+
return;
|
|
751
|
+
this.helpCommentInFlight.add(help.taskId);
|
|
752
|
+
try {
|
|
753
|
+
await this.enqueueStreamStepFlush(help.taskId);
|
|
754
|
+
const latestTaskState = this.state.tasks[help.taskId] ?? taskState;
|
|
755
|
+
if ((latestTaskState.helpCommentedTaskIds ?? []).includes(help.taskId)) {
|
|
756
|
+
this.logger.log(`[aamp help ${help.taskId}] comment already recorded`);
|
|
757
|
+
await this.markFeishuTaskBlockedOnce(help.taskId, latestTaskState);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
const comment = (help.question ?? '').trim()
|
|
761
|
+
|| (help.blockedReason ?? '').trim()
|
|
762
|
+
|| '智能体需要更多信息才能继续处理该任务。';
|
|
763
|
+
this.debugLog(`[aamp help ${help.taskId}] commenting on Feishu task ${latestTaskState.taskGuid}`);
|
|
764
|
+
await this.feishu.commentTask(latestTaskState.taskGuid, comment);
|
|
765
|
+
const helpCommentedTaskIds = new Set(latestTaskState.helpCommentedTaskIds ?? []);
|
|
766
|
+
helpCommentedTaskIds.add(help.taskId);
|
|
767
|
+
const updatedState = {
|
|
768
|
+
...latestTaskState,
|
|
769
|
+
status: 'help_needed',
|
|
770
|
+
helpCommentedTaskIds: [...helpCommentedTaskIds],
|
|
771
|
+
updatedAt: new Date().toISOString(),
|
|
772
|
+
};
|
|
773
|
+
this.state.tasks[help.taskId] = updatedState;
|
|
774
|
+
await this.markFeishuTaskBlockedOnce(help.taskId, updatedState);
|
|
775
|
+
this.state.tasks[help.taskId] = {
|
|
776
|
+
...(this.state.tasks[help.taskId] ?? updatedState),
|
|
777
|
+
status: 'help_needed',
|
|
778
|
+
updatedAt: new Date().toISOString(),
|
|
779
|
+
};
|
|
780
|
+
await this.persistState();
|
|
781
|
+
this.logger.log(`[aamp help] commented task=${help.taskId}`);
|
|
782
|
+
this.debugLog(`[aamp help ${help.taskId}] commented on Feishu task ${latestTaskState.taskGuid}`);
|
|
783
|
+
}
|
|
784
|
+
finally {
|
|
785
|
+
this.helpCommentInFlight.delete(help.taskId);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
async handleTaskResult(result) {
|
|
789
|
+
if (this.resultInFlight.has(result.taskId))
|
|
790
|
+
return;
|
|
791
|
+
const taskState = this.state.tasks[result.taskId];
|
|
792
|
+
if (!taskState)
|
|
793
|
+
return;
|
|
794
|
+
this.resultInFlight.add(result.taskId);
|
|
795
|
+
try {
|
|
796
|
+
await this.enqueueStreamStepFlush(result.taskId);
|
|
797
|
+
const flushedTaskState = this.state.tasks[result.taskId] ?? taskState;
|
|
798
|
+
if ((flushedTaskState.resultHandledTaskIds ?? []).includes(result.taskId)) {
|
|
799
|
+
this.logger.log(`[aamp result ${result.taskId}] result already handled`);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const disposition = classifyTaskResult(result);
|
|
803
|
+
if (disposition.kind === 'answered') {
|
|
804
|
+
await this.commentAnsweredResultOnce(result.taskId, flushedTaskState, disposition);
|
|
805
|
+
const shouldCompleteFeishuTask = flushedTaskState.feishuEventKind !== 'task_comment' || disposition.taskFlowIntent === 'complete_task';
|
|
806
|
+
if (shouldCompleteFeishuTask) {
|
|
807
|
+
await this.completeFeishuTasksOnce(result.taskId, this.state.tasks[result.taskId] ?? flushedTaskState);
|
|
808
|
+
}
|
|
809
|
+
const latestTaskState = this.state.tasks[result.taskId] ?? flushedTaskState;
|
|
810
|
+
const resultHandledTaskIds = new Set(latestTaskState.resultHandledTaskIds ?? []);
|
|
811
|
+
resultHandledTaskIds.add(result.taskId);
|
|
812
|
+
this.state.tasks[result.taskId] = {
|
|
813
|
+
...latestTaskState,
|
|
814
|
+
status: 'completed',
|
|
815
|
+
resultHandledTaskIds: [...resultHandledTaskIds],
|
|
816
|
+
lastError: undefined,
|
|
817
|
+
updatedAt: new Date().toISOString(),
|
|
818
|
+
};
|
|
819
|
+
await this.persistState();
|
|
820
|
+
this.logger.log(`[aamp result] answered task=${result.taskId}`);
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
if (disposition.kind === 'help_needed') {
|
|
824
|
+
await this.commentHelpNeededOnce(result.taskId, flushedTaskState, disposition.message);
|
|
825
|
+
await this.markFeishuTaskBlockedOnce(result.taskId, this.state.tasks[result.taskId] ?? flushedTaskState);
|
|
826
|
+
const latestTaskState = this.state.tasks[result.taskId] ?? flushedTaskState;
|
|
827
|
+
const resultHandledTaskIds = new Set(latestTaskState.resultHandledTaskIds ?? []);
|
|
828
|
+
resultHandledTaskIds.add(result.taskId);
|
|
829
|
+
this.state.tasks[result.taskId] = {
|
|
830
|
+
...latestTaskState,
|
|
831
|
+
status: 'help_needed',
|
|
832
|
+
resultHandledTaskIds: [...resultHandledTaskIds],
|
|
833
|
+
updatedAt: new Date().toISOString(),
|
|
834
|
+
};
|
|
835
|
+
await this.persistState();
|
|
836
|
+
this.logger.log(`[aamp result] help-needed task=${result.taskId}`);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
await this.commentTaskResultOnce(result.taskId, flushedTaskState, disposition);
|
|
840
|
+
await this.completeFeishuTasksOnce(result.taskId, this.state.tasks[result.taskId] ?? flushedTaskState);
|
|
841
|
+
const latestTaskState = this.state.tasks[result.taskId] ?? flushedTaskState;
|
|
842
|
+
const resultHandledTaskIds = new Set(latestTaskState.resultHandledTaskIds ?? []);
|
|
843
|
+
resultHandledTaskIds.add(result.taskId);
|
|
844
|
+
this.state.tasks[result.taskId] = {
|
|
845
|
+
...latestTaskState,
|
|
846
|
+
status: disposition.kind === 'success' ? 'completed' : 'failed',
|
|
847
|
+
resultHandledTaskIds: [...resultHandledTaskIds],
|
|
848
|
+
...(disposition.kind === 'failure' ? { lastError: disposition.message } : { lastError: undefined }),
|
|
849
|
+
updatedAt: new Date().toISOString(),
|
|
850
|
+
};
|
|
851
|
+
await this.persistState();
|
|
852
|
+
this.logger.log(`[aamp result] closed task=${result.taskId} status=${disposition.kind}`);
|
|
853
|
+
}
|
|
854
|
+
finally {
|
|
855
|
+
this.resultInFlight.delete(result.taskId);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
async commentHelpNeededOnce(aampTaskId, taskState, message) {
|
|
859
|
+
const latestTaskState = this.state.tasks[aampTaskId] ?? taskState;
|
|
860
|
+
if ((latestTaskState.resultCommentedTaskIds ?? []).includes(aampTaskId)) {
|
|
861
|
+
this.logger.log(`[aamp result ${aampTaskId}] result comment already recorded`);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
const comment = [
|
|
865
|
+
'智能体需要更多信息才能继续处理该任务。',
|
|
866
|
+
'',
|
|
867
|
+
message,
|
|
868
|
+
].join('\n');
|
|
869
|
+
this.debugLog(`[aamp result ${aampTaskId}] commenting help-needed on Feishu task ${latestTaskState.taskGuid}`);
|
|
870
|
+
await this.feishu.commentTask(latestTaskState.taskGuid, comment);
|
|
871
|
+
const resultCommentedTaskIds = new Set(latestTaskState.resultCommentedTaskIds ?? []);
|
|
872
|
+
resultCommentedTaskIds.add(aampTaskId);
|
|
873
|
+
this.state.tasks[aampTaskId] = {
|
|
874
|
+
...latestTaskState,
|
|
875
|
+
resultCommentedTaskIds: [...resultCommentedTaskIds],
|
|
876
|
+
updatedAt: new Date().toISOString(),
|
|
877
|
+
};
|
|
878
|
+
await this.persistState();
|
|
879
|
+
this.debugLog(`[aamp result ${aampTaskId}] commented help-needed on Feishu task ${latestTaskState.taskGuid}`);
|
|
880
|
+
}
|
|
881
|
+
async commentAnsweredResultOnce(aampTaskId, taskState, disposition) {
|
|
882
|
+
const latestTaskState = this.state.tasks[aampTaskId] ?? taskState;
|
|
883
|
+
if ((latestTaskState.resultCommentedTaskIds ?? []).includes(aampTaskId)) {
|
|
884
|
+
this.logger.log(`[aamp result ${aampTaskId}] result comment already recorded`);
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
const comment = disposition.summary?.trim() || '已处理完成。';
|
|
888
|
+
this.debugLog(`[aamp result ${aampTaskId}] commenting answered result on Feishu task ${latestTaskState.taskGuid}`);
|
|
889
|
+
await this.feishu.commentTask(latestTaskState.taskGuid, comment);
|
|
890
|
+
const resultCommentedTaskIds = new Set(latestTaskState.resultCommentedTaskIds ?? []);
|
|
891
|
+
resultCommentedTaskIds.add(aampTaskId);
|
|
892
|
+
this.state.tasks[aampTaskId] = {
|
|
893
|
+
...latestTaskState,
|
|
894
|
+
resultCommentedTaskIds: [...resultCommentedTaskIds],
|
|
895
|
+
updatedAt: new Date().toISOString(),
|
|
896
|
+
};
|
|
897
|
+
await this.persistState();
|
|
898
|
+
this.debugLog(`[aamp result ${aampTaskId}] commented answered result on Feishu task ${latestTaskState.taskGuid}`);
|
|
899
|
+
}
|
|
900
|
+
async commentTaskResultOnce(aampTaskId, taskState, disposition) {
|
|
901
|
+
const latestTaskState = this.state.tasks[aampTaskId] ?? taskState;
|
|
902
|
+
if ((latestTaskState.resultCommentedTaskIds ?? []).includes(aampTaskId)) {
|
|
903
|
+
this.logger.log(`[aamp result ${aampTaskId}] result comment already recorded`);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
const comment = disposition.kind === 'success'
|
|
907
|
+
? [
|
|
908
|
+
disposition.summary,
|
|
909
|
+
...(disposition.deliverableSummary ? ['', `交付物:${disposition.deliverableSummary}`] : []),
|
|
910
|
+
].join('\n')
|
|
911
|
+
: [
|
|
912
|
+
...(disposition.summary ? [disposition.summary, ''] : []),
|
|
913
|
+
`失败原因:${disposition.message}`,
|
|
914
|
+
].join('\n');
|
|
915
|
+
this.debugLog(`[aamp result ${aampTaskId}] commenting result on Feishu task ${latestTaskState.taskGuid}`);
|
|
916
|
+
await this.feishu.commentTask(latestTaskState.taskGuid, comment);
|
|
917
|
+
const resultCommentedTaskIds = new Set(latestTaskState.resultCommentedTaskIds ?? []);
|
|
918
|
+
resultCommentedTaskIds.add(aampTaskId);
|
|
919
|
+
this.state.tasks[aampTaskId] = {
|
|
920
|
+
...latestTaskState,
|
|
921
|
+
resultCommentedTaskIds: [...resultCommentedTaskIds],
|
|
922
|
+
updatedAt: new Date().toISOString(),
|
|
923
|
+
};
|
|
924
|
+
await this.persistState();
|
|
925
|
+
this.debugLog(`[aamp result ${aampTaskId}] commented result on Feishu task ${latestTaskState.taskGuid}`);
|
|
926
|
+
}
|
|
927
|
+
async completeFeishuTasksOnce(aampTaskId, taskState) {
|
|
928
|
+
const taskGuids = [...new Set([...(taskState.childTaskGuids ?? []), taskState.taskGuid])];
|
|
929
|
+
for (const taskGuid of taskGuids) {
|
|
930
|
+
await this.completeFeishuTaskOnce(aampTaskId, taskState, taskGuid);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
async completeFeishuTaskOnce(aampTaskId, taskState, taskGuid) {
|
|
934
|
+
const inFlightKey = `${aampTaskId}:${taskGuid}`;
|
|
935
|
+
if (this.feishuCompleteInFlight.has(inFlightKey))
|
|
936
|
+
return;
|
|
937
|
+
const latestTaskState = this.state.tasks[aampTaskId] ?? taskState;
|
|
938
|
+
if ((latestTaskState.feishuCompletedTaskIds ?? []).includes(taskGuid)) {
|
|
939
|
+
this.logger.log(`[feishu task ${taskGuid}] completion already recorded for ${aampTaskId}`);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
this.feishuCompleteInFlight.add(inFlightKey);
|
|
943
|
+
try {
|
|
944
|
+
this.debugLog(`[feishu task ${taskGuid}] completing for ${aampTaskId}`);
|
|
945
|
+
await this.feishu.completeTask(taskGuid);
|
|
946
|
+
const feishuCompletedTaskIds = new Set(latestTaskState.feishuCompletedTaskIds ?? []);
|
|
947
|
+
feishuCompletedTaskIds.add(taskGuid);
|
|
948
|
+
this.state.tasks[aampTaskId] = {
|
|
949
|
+
...latestTaskState,
|
|
950
|
+
feishuCompletedTaskIds: [...feishuCompletedTaskIds],
|
|
951
|
+
updatedAt: new Date().toISOString(),
|
|
952
|
+
};
|
|
953
|
+
await this.persistState();
|
|
954
|
+
this.logger.log(`[feishu task] completed task=${taskGuid}`);
|
|
955
|
+
this.debugLog(`[feishu task ${taskGuid}] completed for ${aampTaskId}`);
|
|
956
|
+
}
|
|
957
|
+
finally {
|
|
958
|
+
this.feishuCompleteInFlight.delete(inFlightKey);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
async markFeishuTaskBlockedOnce(aampTaskId, taskState) {
|
|
962
|
+
const latestTaskState = this.state.tasks[aampTaskId] ?? taskState;
|
|
963
|
+
const taskGuid = latestTaskState.taskGuid;
|
|
964
|
+
const inFlightKey = `${aampTaskId}:${taskGuid}`;
|
|
965
|
+
if (this.feishuBlockInFlight.has(inFlightKey))
|
|
966
|
+
return;
|
|
967
|
+
if ((latestTaskState.feishuBlockedTaskIds ?? []).includes(taskGuid)) {
|
|
968
|
+
this.logger.log(`[feishu task ${taskGuid}] blocked state already recorded for ${aampTaskId}`);
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
this.feishuBlockInFlight.add(inFlightKey);
|
|
972
|
+
try {
|
|
973
|
+
this.debugLog(`[feishu task ${taskGuid}] marking blocked for ${aampTaskId}`);
|
|
974
|
+
await this.feishu.markTaskWaitingForHuman(taskGuid);
|
|
975
|
+
const feishuBlockedTaskIds = new Set(latestTaskState.feishuBlockedTaskIds ?? []);
|
|
976
|
+
feishuBlockedTaskIds.add(taskGuid);
|
|
977
|
+
this.state.tasks[aampTaskId] = {
|
|
978
|
+
...latestTaskState,
|
|
979
|
+
feishuBlockedTaskIds: [...feishuBlockedTaskIds],
|
|
980
|
+
updatedAt: new Date().toISOString(),
|
|
981
|
+
};
|
|
982
|
+
await this.persistState();
|
|
983
|
+
this.logger.log(`[feishu task] blocked task=${taskGuid}`);
|
|
984
|
+
this.debugLog(`[feishu task ${taskGuid}] marked blocked for ${aampTaskId}`);
|
|
985
|
+
}
|
|
986
|
+
finally {
|
|
987
|
+
this.feishuBlockInFlight.delete(inFlightKey);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
rememberEvent(event) {
|
|
991
|
+
if (this.state.dedupEventIds[event.eventId])
|
|
992
|
+
return false;
|
|
993
|
+
this.state.dedupEventIds[event.eventId] = new Date().toISOString();
|
|
994
|
+
this.pruneDedupEvents();
|
|
995
|
+
return true;
|
|
996
|
+
}
|
|
997
|
+
pruneDedupEvents() {
|
|
998
|
+
const entries = Object.entries(this.state.dedupEventIds);
|
|
999
|
+
if (entries.length <= 1000)
|
|
1000
|
+
return;
|
|
1001
|
+
entries
|
|
1002
|
+
.sort((a, b) => a[1].localeCompare(b[1]))
|
|
1003
|
+
.slice(0, entries.length - 1000)
|
|
1004
|
+
.forEach(([eventId]) => {
|
|
1005
|
+
delete this.state.dedupEventIds[eventId];
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
setConnectivity(kind, value) {
|
|
1009
|
+
this.state.connectivity[kind] = value;
|
|
1010
|
+
}
|
|
1011
|
+
debugLog(message) {
|
|
1012
|
+
if (this.config.behavior.debug)
|
|
1013
|
+
this.logger.log(message);
|
|
1014
|
+
}
|
|
1015
|
+
async persistState() {
|
|
1016
|
+
await saveBridgeState(this.state, this.configDir);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
//# sourceMappingURL=runtime.js.map
|