@clowder-ai/connector-feishu 0.0.0-stage → 0.1.0-alpha.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.
Files changed (78) hide show
  1. package/README.en.md +54 -0
  2. package/README.md +25 -2
  3. package/assets/icon.svg +3 -0
  4. package/dist/FeishuAdapter.d.ts +250 -0
  5. package/dist/FeishuAdapter.d.ts.map +1 -0
  6. package/dist/FeishuAdapter.js +845 -0
  7. package/dist/FeishuAdapter.js.map +1 -0
  8. package/dist/FeishuQrBindClient.d.ts +23 -0
  9. package/dist/FeishuQrBindClient.d.ts.map +1 -0
  10. package/dist/FeishuQrBindClient.js +97 -0
  11. package/dist/FeishuQrBindClient.js.map +1 -0
  12. package/dist/FeishuTokenManager.d.ts +15 -0
  13. package/dist/FeishuTokenManager.d.ts.map +1 -0
  14. package/dist/FeishuTokenManager.js +35 -0
  15. package/dist/FeishuTokenManager.js.map +1 -0
  16. package/dist/command-ingress.d.ts +10 -0
  17. package/dist/command-ingress.d.ts.map +1 -0
  18. package/dist/command-ingress.js +15 -0
  19. package/dist/command-ingress.js.map +1 -0
  20. package/dist/execution-status.d.ts +27 -0
  21. package/dist/execution-status.d.ts.map +1 -0
  22. package/dist/execution-status.js +48 -0
  23. package/dist/execution-status.js.map +1 -0
  24. package/dist/feishu-card-formatter.d.ts +18 -0
  25. package/dist/feishu-card-formatter.d.ts.map +1 -0
  26. package/dist/feishu-card-formatter.js +58 -0
  27. package/dist/feishu-card-formatter.js.map +1 -0
  28. package/dist/inbound-download.d.ts +13 -0
  29. package/dist/inbound-download.d.ts.map +1 -0
  30. package/dist/inbound-download.js +70 -0
  31. package/dist/inbound-download.js.map +1 -0
  32. package/dist/inbound-media-source.d.ts +25 -0
  33. package/dist/inbound-media-source.d.ts.map +1 -0
  34. package/dist/inbound-media-source.js +111 -0
  35. package/dist/inbound-media-source.js.map +1 -0
  36. package/dist/index.d.ts +9 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +5 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/lifecycle-action.d.ts +30 -0
  41. package/dist/lifecycle-action.d.ts.map +1 -0
  42. package/dist/lifecycle-action.js +457 -0
  43. package/dist/lifecycle-action.js.map +1 -0
  44. package/dist/materialize-media.d.ts +7 -0
  45. package/dist/materialize-media.d.ts.map +1 -0
  46. package/dist/materialize-media.js +35 -0
  47. package/dist/materialize-media.js.map +1 -0
  48. package/dist/media-notice.d.ts +6 -0
  49. package/dist/media-notice.d.ts.map +1 -0
  50. package/dist/media-notice.js +50 -0
  51. package/dist/media-notice.js.map +1 -0
  52. package/dist/plugin-entrypoint.d.ts +9 -0
  53. package/dist/plugin-entrypoint.d.ts.map +1 -0
  54. package/dist/plugin-entrypoint.js +347 -0
  55. package/dist/plugin-entrypoint.js.map +1 -0
  56. package/dist/reply-sender-map.d.ts +10 -0
  57. package/dist/reply-sender-map.d.ts.map +1 -0
  58. package/dist/reply-sender-map.js +123 -0
  59. package/dist/reply-sender-map.js.map +1 -0
  60. package/dist/runtime.d.ts +190 -0
  61. package/dist/runtime.d.ts.map +1 -0
  62. package/dist/runtime.js +580 -0
  63. package/dist/runtime.js.map +1 -0
  64. package/dist/settings.d.ts +20 -0
  65. package/dist/settings.d.ts.map +1 -0
  66. package/dist/settings.js +35 -0
  67. package/dist/settings.js.map +1 -0
  68. package/dist/thread-mappings.d.ts +46 -0
  69. package/dist/thread-mappings.d.ts.map +1 -0
  70. package/dist/thread-mappings.js +62 -0
  71. package/dist/thread-mappings.js.map +1 -0
  72. package/dist/types.d.ts +54 -0
  73. package/dist/types.d.ts.map +1 -0
  74. package/dist/types.js +7 -0
  75. package/dist/types.js.map +1 -0
  76. package/npm-shrinkwrap.json +979 -0
  77. package/package.json +58 -4
  78. package/plugin.yaml +128 -0
@@ -0,0 +1,111 @@
1
+ import { createHash } from 'node:crypto';
2
+ const STATE_PREFIX = 'media-source/';
3
+ const stateKey = (reference) => `${STATE_PREFIX}${reference}`;
4
+ const DEFINITIVE_SEND_REJECTION_CODES = new Set([
5
+ 'VALIDATION', 'PERMISSION',
6
+ ]);
7
+ function isLocator(value) {
8
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
9
+ return false;
10
+ const candidate = value;
11
+ return typeof candidate.sourceEventId === 'string'
12
+ && ['image', 'file', 'audio', 'video'].includes(String(candidate.type))
13
+ && typeof candidate.platformKey === 'string';
14
+ }
15
+ function sameLocator(left, right) {
16
+ return left.sourceEventId === right.sourceEventId
17
+ && left.type === right.type
18
+ && left.platformKey === right.platformKey
19
+ && left.fileName === right.fileName
20
+ && left.duration === right.duration;
21
+ }
22
+ export function privateMediaReference(connectorId, sourceEventId, elementId) {
23
+ const digest = createHash('sha256').update(`${connectorId}\\0${sourceEventId}\\0${elementId}`).digest('hex');
24
+ return `pmr_${connectorId}_${digest}`;
25
+ }
26
+ export async function retainInboundMedia(context, connectorId, sourceId, sourceEventId, attachments) {
27
+ const retained = [];
28
+ try {
29
+ const elements = [];
30
+ for (const [index, attachment] of attachments.entries()) {
31
+ const elementId = `media-${index + 1}`;
32
+ const reference = privateMediaReference(connectorId, sourceEventId, elementId);
33
+ const locator = { sourceEventId, ...attachment };
34
+ const inserted = await context.storage.compareAndSet(stateKey(reference), null, locator);
35
+ if (inserted.applied) {
36
+ if (typeof inserted.revision !== 'number') {
37
+ throw new Error('Inbound media state did not return a revision fence');
38
+ }
39
+ retained.push({ reference, revision: inserted.revision });
40
+ }
41
+ else {
42
+ const existing = await context.storage.get(stateKey(reference));
43
+ if (existing === undefined || !isLocator(existing.value) || !sameLocator(existing.value, locator)) {
44
+ throw new Error('Inbound media reference conflicts with retained locator');
45
+ }
46
+ }
47
+ elements.push({
48
+ elementId,
49
+ kind: 'media_ref',
50
+ payload: {
51
+ type: attachment.type, reference, sourceId,
52
+ ...(attachment.fileName === undefined ? {} : { fileName: attachment.fileName }),
53
+ ...(attachment.duration === undefined ? {} : { duration: attachment.duration }),
54
+ },
55
+ });
56
+ }
57
+ return { elements, ownership: { entries: retained } };
58
+ }
59
+ catch (error) {
60
+ const cleanup = await Promise.allSettled(retained.map(({ reference, revision }) => (context.storage.delete(stateKey(reference), revision))));
61
+ context.log('warn', 'Inbound media locator retention failed', {
62
+ connectorId,
63
+ sourceEventId,
64
+ attachmentCount: attachments.length,
65
+ cleanupFailed: cleanup.some(result => result.status === 'rejected'),
66
+ errorName: error instanceof Error ? error.name : 'unknown',
67
+ });
68
+ return {
69
+ elements: attachments.map((attachment, index) => ({
70
+ elementId: `media-${index + 1}`,
71
+ kind: 'media_unavailable',
72
+ payload: {
73
+ type: attachment.type,
74
+ reason: 'unavailable',
75
+ ...(attachment.fileName === undefined ? {} : { fileName: attachment.fileName }),
76
+ },
77
+ })),
78
+ ownership: { entries: [] },
79
+ };
80
+ }
81
+ }
82
+ export async function releaseInboundMedia(context, ownership, sendError) {
83
+ if (sendError !== undefined) {
84
+ const code = sendError !== null && typeof sendError === 'object'
85
+ ? sendError.code
86
+ : undefined;
87
+ if (typeof code !== 'string' || !DEFINITIVE_SEND_REJECTION_CODES.has(code))
88
+ return;
89
+ }
90
+ await Promise.all(ownership.entries.map(({ reference, revision }) => (context.storage.delete(stateKey(reference), revision))));
91
+ }
92
+ export function createInboundMediaSource(context, download) {
93
+ return { read(reference, options = {}) {
94
+ return { async *[Symbol.asyncIterator]() {
95
+ options.signal?.throwIfAborted();
96
+ const entry = await context.storage.get(stateKey(reference));
97
+ if (!entry || !isLocator(entry.value))
98
+ throw new Error('plugin stream source is unavailable');
99
+ const bytes = await download(entry.value);
100
+ options.signal?.throwIfAborted();
101
+ // Provider adapters retain their existing bounded download checks. No base64 materialization.
102
+ for (let offset = 0; offset < bytes.length; offset += 524288) {
103
+ options.signal?.throwIfAborted();
104
+ // A retained iterator cannot outlive its current feature client.
105
+ await context.storage.get(stateKey(reference));
106
+ yield bytes.subarray(offset, Math.min(offset + 524288, bytes.length));
107
+ }
108
+ } };
109
+ } };
110
+ }
111
+ //# sourceMappingURL=inbound-media-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbound-media-source.js","sourceRoot":"","sources":["../src/inbound-media-source.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ export { FeishuAdapter, inferFeishuFileType } from './FeishuAdapter.js';
2
+ export type { FeishuAdapterOptions, FeishuAttachment, FeishuCardAction, FeishuInboundMessage, FeishuMediaPayload, FeishuMentionAlias, } from './FeishuAdapter.js';
3
+ export { FeishuTokenManager } from './FeishuTokenManager.js';
4
+ export type { FeishuTokenManagerOptions } from './FeishuTokenManager.js';
5
+ export { DefaultFeishuQrBindClient } from './FeishuQrBindClient.js';
6
+ export type { FeishuQrBindClient, FeishuQrCreateResult, FeishuQrPollResult } from './FeishuQrBindClient.js';
7
+ export { formatFeishuCard } from './feishu-card-formatter.js';
8
+ export type { ConnectorLogger, MessageEnvelope, RichBlock } from './types.js';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACxE,YAAY,EACV,oBAAoB,EACpB,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,YAAY,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AACzE,OAAO,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AACpE,YAAY,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC5G,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAC9D,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { FeishuAdapter, inferFeishuFileType } from './FeishuAdapter.js';
2
+ export { FeishuTokenManager } from './FeishuTokenManager.js';
3
+ export { DefaultFeishuQrBindClient } from './FeishuQrBindClient.js';
4
+ export { formatFeishuCard } from './feishu-card-formatter.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AASxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,OAAO,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AAEpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC"}
@@ -0,0 +1,30 @@
1
+ import { type LifecycleAction } from './execution-status.js';
2
+ import { type FeatureContext } from '@clowder-ai/plugin-sdk';
3
+ type LifecycleEvent = Parameters<LifecycleAction>[0];
4
+ type SettledEvent = Extract<LifecycleEvent, {
5
+ readonly state: 'settled';
6
+ }>;
7
+ export interface ConnectorLifecycleCallbacks {
8
+ sendPlaceholder(externalConversationId: string, text: string): Promise<string>;
9
+ editPlaceholder(externalConversationId: string, platformMessageId: string, text: string, phase: 'catching_up' | 'blocked', executionId: string): Promise<boolean>;
10
+ sendRecovery(externalConversationId: string, text: string): Promise<void>;
11
+ /** Resolve the sender display name recorded for a Host message id; only connectors with a reply-sender mapping implement this. */
12
+ resolveReplySenderName?(replyTo: string): Promise<string | undefined>;
13
+ onPlaceholder?(externalConversationId: string, platformMessageId: string, executionId: string): void | Promise<void>;
14
+ settle(input: {
15
+ readonly externalConversationId: string;
16
+ readonly platformMessageId?: string;
17
+ readonly actorDisplayName: string;
18
+ readonly recoveryText?: string;
19
+ readonly event: SettledEvent;
20
+ }): Promise<void>;
21
+ }
22
+ export declare function createConnectorLifecycleAction(context: FeatureContext, callbacks: ConnectorLifecycleCallbacks): ((input: unknown) => Promise<{
23
+ readonly deliveryId: string;
24
+ } | {
25
+ readonly deliveryId: string;
26
+ }>) & {
27
+ sweepNow: () => Promise<void>;
28
+ };
29
+ export {};
30
+ //# sourceMappingURL=lifecycle-action.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle-action.d.ts","sourceRoot":"","sources":["../src/lifecycle-action.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuE,KAAK,eAAe,EAAC,MAAM,uBAAuB,CAAC;AAEjI,OAAO,EACL,KAAK,cAAc,EACpB,MAAM,wBAAwB,CAAC;AAEhC,KAAK,cAAc,GAAG,UAAU,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC;AACrD,KAAK,YAAY,GAAG,OAAO,CAAC,cAAc,EAAE;IAAE,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;CAAE,CAAC,CAAC;AAE3E,MAAM,WAAW,2BAA2B;IAC1C,eAAe,CAAC,sBAAsB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/E,eAAe,CACb,sBAAsB,EAAE,MAAM,EAC9B,iBAAiB,EAAE,MAAM,EACzB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,aAAa,GAAG,SAAS,EAChC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,OAAO,CAAC,CAAC;IACpB,YAAY,CAAC,sBAAsB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1E,kIAAkI;IAClI,sBAAsB,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACtE,aAAa,CAAC,CACZ,sBAAsB,EAAE,MAAM,EAC9B,iBAAiB,EAAE,MAAM,EACzB,WAAW,EAAE,MAAM,GAClB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,MAAM,CAAC,KAAK,EAAE;QACZ,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;QACxC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;QACpC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;QAClC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAC/B,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;KAC9B,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB;AAuFD,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,2BAA2B;;;;;oBAMd,OAAO,CAAC,IAAI,CAAC;EAgctC"}
@@ -0,0 +1,457 @@
1
+ import { decideLifecycleTransition, defineLifecycleAction, lifecycleRejectReason } from './execution-status.js';
2
+ import { threadMappings } from './thread-mappings.js';
3
+ const STARTED_TEXT = '🤔 思考中...';
4
+ const CATCHING_UP_TEXT = '🔄 收到新消息,正在重新整理回复…';
5
+ const LIFECYCLE_KEY_PREFIX = 'lifecycle/';
6
+ // A settled tombstone only answers Host redeliveries with replay/reject; a
7
+ // day is far beyond any sane redelivery horizon, so keeping it longer only
8
+ // accumulates dead keys.
9
+ const LIFECYCLE_TOMBSTONE_TTL_MS = 24 * 60 * 60 * 1000;
10
+ // Records that never settled are Host-crash orphans: their placeholder text
11
+ // is already terminal. The TTL must exceed the longest legitimate in-flight
12
+ // turn (cat work can span many hours), so 24h bounds orphans without
13
+ // deleting lifecycles that are still actively running.
14
+ const LIFECYCLE_STRANDED_TTL_MS = 24 * 60 * 60 * 1000;
15
+ const SWEEP_EVERY_WRITES = 25;
16
+ function object(value) {
17
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
18
+ }
19
+ function isPendingEffect(value) {
20
+ return object(value)
21
+ && value.v === 1
22
+ && (value.state === 'blocked' || value.state === 'settled')
23
+ && typeof value.recoveryText === 'string';
24
+ }
25
+ function lifecycleError(code, message) {
26
+ return Object.assign(new Error(message), { code });
27
+ }
28
+ function storedLifecycle(value) {
29
+ if (!object(value)
30
+ || (value.version !== 1 && value.version !== 2)
31
+ || !Array.isArray(value.history)
32
+ || typeof value.actorDisplayName !== 'string'
33
+ || (value.platformMessageId !== undefined && typeof value.platformMessageId !== 'string')
34
+ || (value.platformMessageByKey !== undefined
35
+ && (!object(value.platformMessageByKey) || Object.values(value.platformMessageByKey).some(id => typeof id !== 'string')))
36
+ || (value.updatedAt !== undefined && typeof value.updatedAt !== 'number')
37
+ || (value.tombstone !== undefined && typeof value.tombstone !== 'boolean')
38
+ || (value.deliveryIds !== undefined && (!Array.isArray(value.deliveryIds) || value.deliveryIds.some(id => typeof id !== 'string')))
39
+ || (value.settledAt !== undefined && typeof value.settledAt !== 'number')
40
+ || (value.pendingEffect !== undefined && !isPendingEffect(value.pendingEffect))) {
41
+ throw lifecycleError('PLUGIN_INTERNAL', 'stored lifecycle state is invalid');
42
+ }
43
+ return structuredClone(value);
44
+ }
45
+ function recoveryText(event) {
46
+ return `⚠️ 未能完成最新消息重读(${event.reason})。请打开 Clowder AI 重试${event.recoveryUrl ? `:${event.recoveryUrl}` : '。'}`;
47
+ }
48
+ export function createConnectorLifecycleAction(context, callbacks) {
49
+ const tails = new Map();
50
+ let writesSinceSweep = 0;
51
+ let sweepInFlight = false;
52
+ const sweep = async () => {
53
+ let listed;
54
+ try {
55
+ listed = await context.storage.list();
56
+ }
57
+ catch (error) {
58
+ context.log('warn', 'Connector lifecycle sweep failed', {
59
+ errorName: error instanceof Error ? error.name : 'unknown',
60
+ });
61
+ return;
62
+ }
63
+ const now = Date.now();
64
+ for (const [key, item] of Object.entries(listed)) {
65
+ if (!key.startsWith(LIFECYCLE_KEY_PREFIX))
66
+ continue;
67
+ let record;
68
+ try {
69
+ record = storedLifecycle(item?.value);
70
+ }
71
+ catch {
72
+ continue;
73
+ }
74
+ if (record.tombstone === true) {
75
+ const settledAt = record.settledAt ?? record.updatedAt;
76
+ if (settledAt !== undefined && settledAt + LIFECYCLE_TOMBSTONE_TTL_MS < now) {
77
+ await context.storage.delete(key, item.revision).catch(() => undefined);
78
+ }
79
+ continue;
80
+ }
81
+ // v1 records carry no updatedAt, so their age is unknown; the v2 write
82
+ // path re-binds them with a timestamp on the next transition.
83
+ if (record.updatedAt !== undefined && record.updatedAt + LIFECYCLE_STRANDED_TTL_MS < now) {
84
+ await context.storage.delete(key, item.revision).catch(() => undefined);
85
+ }
86
+ }
87
+ };
88
+ // Single-flight background sweep, mirroring reply-sender-map: the hot path
89
+ // only counts writes. console.warn (not context.log) is deliberate here —
90
+ // log() throws FeatureContextRevokedError after feature shutdown, which
91
+ // would turn this last-resort guard into an unhandled rejection.
92
+ const sweepInBackground = () => {
93
+ if (sweepInFlight)
94
+ return;
95
+ sweepInFlight = true;
96
+ void sweep()
97
+ .catch((error) => {
98
+ console.warn('Connector lifecycle sweep failed', {
99
+ errorName: error instanceof Error ? error.name : 'unknown',
100
+ });
101
+ })
102
+ .finally(() => {
103
+ sweepInFlight = false;
104
+ });
105
+ };
106
+ const countWrite = () => {
107
+ writesSinceSweep += 1;
108
+ if (writesSinceSweep < SWEEP_EVERY_WRITES)
109
+ return;
110
+ writesSinceSweep = 0;
111
+ sweepInBackground();
112
+ };
113
+ // Platform effects must not fail the action; the write-ahead record already
114
+ // accepted the event, so a throwing callback is logged and skipped.
115
+ const runEffectSafely = async (executionId, state, label, effect) => {
116
+ try {
117
+ await effect();
118
+ return true;
119
+ }
120
+ catch (error) {
121
+ context.log('warn', `Connector lifecycle ${label} failed`, {
122
+ executionId,
123
+ state,
124
+ errorName: error instanceof Error ? error.name : 'unknown',
125
+ });
126
+ return false;
127
+ }
128
+ };
129
+ // Binding listing feeds every side-effect path; a failure here must
130
+ // surface as a coded lifecycle error, not a bare storage/transport
131
+ // exception escaping the action boundary.
132
+ const listThreadBindings = async (threadId) => {
133
+ let listed;
134
+ try {
135
+ listed = await threadMappings(context).listBindings();
136
+ }
137
+ catch (error) {
138
+ throw lifecycleError('PLUGIN_INTERNAL', `lifecycle thread ${threadId} binding listing failed: ${error instanceof Error ? error.message : 'unknown'}`);
139
+ }
140
+ return listed.filter(candidate => candidate.threadId === threadId);
141
+ };
142
+ // A redelivery (or a later transition arriving first) can find a record
143
+ // whose accepted side effect never ran because the process died between the
144
+ // pre-write and the platform call. Re-run exactly that effect through the
145
+ // same callbacks and per-binding fallbacks as the original path.
146
+ //
147
+ // The marker is cleared after the ATTEMPT, not after confirmed success:
148
+ // runEffectSafely swallows per-binding failures (they are logged), and an
149
+ // unbounded "retry until applied" loop would duplicate-send on every
150
+ // redelivery. One bounded re-execution per redelivery beats both a lost
151
+ // recovery hint and an effect storm.
152
+ //
153
+ // Returns whether the effect was actually attempted on at least one
154
+ // binding. A thread with no matching binding is not an attempt: the caller
155
+ // must keep the marker so a redelivery after the binding returns can still
156
+ // recover the hint.
157
+ const reexecutePendingEffect = async (stored) => {
158
+ const pending = stored.pendingEffect;
159
+ if (pending === undefined)
160
+ return false;
161
+ const sourceEvent = [...stored.history].reverse().find((candidate) => (candidate.state === pending.state));
162
+ if (sourceEvent === undefined)
163
+ return false;
164
+ const bindings = await listThreadBindings(sourceEvent.threadId);
165
+ // Legacy records carry a single platformMessageId; with exactly one
166
+ // matching binding it can only have belonged to that binding.
167
+ let messageIds = { ...(stored.platformMessageByKey ?? {}) };
168
+ if (Object.keys(messageIds).length === 0 && bindings.length === 1 && stored.platformMessageId !== undefined) {
169
+ messageIds = { [bindings[0].key]: stored.platformMessageId };
170
+ }
171
+ for (const binding of bindings) {
172
+ const platformMessageId = messageIds[binding.key];
173
+ if (pending.state === 'blocked') {
174
+ const edited = platformMessageId !== undefined && await runEffectSafely(sourceEvent.executionId, 'blocked', 'blocked edit', async () => {
175
+ const applied = await callbacks.editPlaceholder(binding.key, platformMessageId, pending.recoveryText, 'blocked', sourceEvent.executionId);
176
+ if (!applied)
177
+ throw new Error('placeholder is no longer editable');
178
+ });
179
+ if (!edited) {
180
+ await runEffectSafely(sourceEvent.executionId, 'blocked', 'blocked recovery send', () => callbacks.sendRecovery(binding.key, pending.recoveryText));
181
+ }
182
+ }
183
+ else {
184
+ await runEffectSafely(sourceEvent.executionId, 'settled', 'settlement', () => callbacks.settle({
185
+ externalConversationId: binding.key,
186
+ ...(platformMessageId === undefined ? {} : { platformMessageId }),
187
+ actorDisplayName: stored.actorDisplayName,
188
+ recoveryText: pending.recoveryText,
189
+ event: sourceEvent,
190
+ }));
191
+ }
192
+ }
193
+ return bindings.length > 0;
194
+ };
195
+ const clearPendingEffect = async (stateKey, stored) => {
196
+ try {
197
+ await context.storage.set(stateKey, { ...stored, pendingEffect: undefined });
198
+ countWrite();
199
+ }
200
+ catch (error) {
201
+ // The marker survives, so a further redelivery re-executes the effect
202
+ // once more; losing the clear is duplicate-tolerant by design.
203
+ context.log('warn', 'Connector lifecycle pending-effect clear failed', {
204
+ errorName: error instanceof Error ? error.name : 'unknown',
205
+ });
206
+ }
207
+ };
208
+ const handle = async (event) => {
209
+ const stateKey = `lifecycle/${event.executionId}`;
210
+ const safely = (label, effect) => runEffectSafely(event.executionId, event.state, label, effect);
211
+ const entry = await context.storage.get(stateKey);
212
+ const stored = entry === undefined ? undefined : storedLifecycle(entry.value);
213
+ if (stored?.tombstone === true) {
214
+ // The tombstone keeps the full event history, so redelivery judgment is
215
+ // the same as the pre-settle path: an exact redelivery of anything the
216
+ // Host already acked answers replay, the same deliveryId with different
217
+ // content is a delivery conflict, and anything else is out of order.
218
+ const tombstoneDecision = decideLifecycleTransition(stored.history, event);
219
+ if (tombstoneDecision.kind === 'replay') {
220
+ // Crash residue: the accepted side effect may never have run. Re-run
221
+ // it once for this redelivery. The marker is cleared only when the
222
+ // effect was actually attempted: with no binding there is nothing to
223
+ // retry, and a later redelivery must still be able to recover the
224
+ // hint (see reexecutePendingEffect for why not after success).
225
+ if (stored.pendingEffect !== undefined && stored.pendingEffect.state === event.state) {
226
+ if (await reexecutePendingEffect(stored)) {
227
+ await clearPendingEffect(stateKey, stored);
228
+ }
229
+ }
230
+ return { deliveryId: event.deliveryId };
231
+ }
232
+ if (tombstoneDecision.kind === 'reject' && tombstoneDecision.reason === 'DELIVERY_CONFLICT') {
233
+ throw lifecycleError(lifecycleRejectReason(tombstoneDecision), `lifecycle ${tombstoneDecision.reason.toLowerCase()}`);
234
+ }
235
+ // Tombstones written before the full history was retained only know the
236
+ // accepted deliveryId set; for them that set is the only replay identity.
237
+ const deliveryIds = stored.deliveryIds ?? [];
238
+ if (deliveryIds.includes(event.deliveryId)) {
239
+ return { deliveryId: event.deliveryId };
240
+ }
241
+ throw lifecycleError('EXECUTION_OUT_OF_ORDER', 'lifecycle is already settled');
242
+ }
243
+ const history = stored?.history ?? [];
244
+ const decision = decideLifecycleTransition(history, event);
245
+ if (decision.kind === 'replay') {
246
+ // Crash residue, pre-settle flavor: the blocked recovery effect was
247
+ // accepted but may never have run. Re-run it once. The marker is
248
+ // cleared only when the effect was actually attempted: with no binding
249
+ // a later redelivery must still be able to recover the hint.
250
+ if (stored?.pendingEffect !== undefined && stored.pendingEffect.state === event.state) {
251
+ if (await reexecutePendingEffect(stored)) {
252
+ await clearPendingEffect(stateKey, stored);
253
+ }
254
+ }
255
+ return { deliveryId: event.deliveryId };
256
+ }
257
+ if (decision.kind === 'reject') {
258
+ throw lifecycleError(lifecycleRejectReason(decision), `lifecycle ${decision.reason.toLowerCase()}`);
259
+ }
260
+ // A new transition arrives while an older one still has an unconfirmed
261
+ // side effect (the crash window, or a lost post-write): this write would
262
+ // overwrite the marker, so flush the pending effect first and clear the
263
+ // marker. The marker is cleared only when the flush was actually
264
+ // attempted on at least one binding: with no binding the flush is a
265
+ // no-op and clearing here would lose the recovery hint for good once
266
+ // the zero-binding throw below rejects this delivery. Without the clear,
267
+ // a failed new pre-write would leave the old marker in place and every
268
+ // Host retry of this delivery would re-run the already-flushed effect
269
+ // again. An occasional duplicate beats a lost recovery hint.
270
+ if (stored?.pendingEffect !== undefined) {
271
+ if (await reexecutePendingEffect(stored)) {
272
+ await clearPendingEffect(stateKey, stored);
273
+ }
274
+ }
275
+ // One thread can carry several provider bindings (the Host allows
276
+ // rebinding different external chats onto the same thread), so every
277
+ // matching binding receives the outbound effects.
278
+ const bindings = await listThreadBindings(event.threadId);
279
+ if (bindings.length === 0) {
280
+ throw lifecycleError('PLUGIN_INTERNAL', `lifecycle thread ${event.threadId} has no provider binding`);
281
+ }
282
+ const actorDisplayName = event.state === 'started'
283
+ ? event.presentation.actor.displayName
284
+ : stored?.actorDisplayName ?? '';
285
+ // Per-binding placeholder message ids. Legacy records carry a single
286
+ // platformMessageId; with exactly one matching binding it can only have
287
+ // belonged to that binding.
288
+ let messageIds = { ...(stored?.platformMessageByKey ?? {}) };
289
+ if (Object.keys(messageIds).length === 0 && bindings.length === 1 && stored?.platformMessageId !== undefined) {
290
+ messageIds = { [bindings[0].key]: stored.platformMessageId };
291
+ }
292
+ // Recovery text is decided up front: the blocked transition's hint, or
293
+ // the retained blocked event's hint for a settled transition.
294
+ const blocked = history.slice(history.map(candidate => candidate.state).lastIndexOf('resumed') + 1).reverse().find((candidate) => (candidate.state === 'blocked'));
295
+ // Transitions carrying a user-facing recovery side effect are marked
296
+ // pending-effect in the pre-write; the post-write clears the marker once
297
+ // the effect completed. Started placeholders stay at-most-once on
298
+ // purpose — the final reply arrives separately — and catching_up edits
299
+ // self-heal through the later blocked/settle effects.
300
+ const pendingEffect = event.state === 'blocked'
301
+ ? { v: 1, state: 'blocked', recoveryText: recoveryText(event) }
302
+ : event.state === 'settled' && blocked !== undefined
303
+ ? { v: 1, state: 'settled', recoveryText: recoveryText(blocked) }
304
+ : undefined;
305
+ // Settled records compress into a tombstone: platform effects stop, but
306
+ // the full event history is retained so redelivery judgment can still
307
+ // distinguish an exact replay from a same-deliveryId content conflict.
308
+ const nextRecord = (ids, pending) => ({
309
+ version: 2,
310
+ history: [...history, event],
311
+ // Keep the legacy single-id field only when it is unambiguous.
312
+ ...(Object.keys(ids).length === 1 ? { platformMessageId: Object.values(ids)[0] } : {}),
313
+ ...(Object.keys(ids).length === 0 ? {} : { platformMessageByKey: ids }),
314
+ actorDisplayName,
315
+ updatedAt: Date.now(),
316
+ ...(pending !== undefined ? { pendingEffect: pending } : {}),
317
+ ...(event.state === 'settled'
318
+ ? { tombstone: true, deliveryIds: [...history.map(candidate => candidate.deliveryId), event.deliveryId], settledAt: Date.now() }
319
+ : {}),
320
+ });
321
+ // Write-ahead: persist the accepted event before any platform side
322
+ // effect, so a crash between effect and store cannot leave later events
323
+ // orphaned as OUT_OF_ORDER. The pre-write carries the pending-effect
324
+ // marker, so a redelivery that finds it re-runs the unconfirmed effect.
325
+ // If even the identical retry fails we throw before any side effect: no
326
+ // half-persisted record, no duplicate sends.
327
+ const preWrite = nextRecord(messageIds, pendingEffect);
328
+ try {
329
+ await context.storage.set(stateKey, preWrite);
330
+ countWrite();
331
+ }
332
+ catch {
333
+ try {
334
+ await context.storage.set(stateKey, preWrite);
335
+ countWrite();
336
+ }
337
+ catch (error) {
338
+ throw lifecycleError('PLUGIN_INTERNAL', `lifecycle ${event.executionId} state pre-write failed: ${error instanceof Error ? error.message : 'unknown'}`);
339
+ }
340
+ }
341
+ // Resolve the sender name once, outside the per-binding loop: it only
342
+ // depends on event.replyTo, never on the binding, so N bindings must not
343
+ // trigger N lookups. Resolve before the safely() wrapper: a throwing
344
+ // callback must not take down the whole placeholder send. resolve() in
345
+ // reply-sender-map already swallows storage errors; this catch covers
346
+ // every other failure reason.
347
+ let senderName;
348
+ if (event.state === 'started' && event.replyTo !== undefined && callbacks.resolveReplySenderName !== undefined) {
349
+ try {
350
+ senderName = await callbacks.resolveReplySenderName(event.replyTo);
351
+ }
352
+ catch (error) {
353
+ context.log('warn', 'Connector lifecycle sender name resolution failed', {
354
+ executionId: event.executionId,
355
+ errorName: error instanceof Error ? error.name : 'unknown',
356
+ });
357
+ senderName = undefined;
358
+ }
359
+ }
360
+ const senderSuffix = senderName ? `→${senderName}` : '';
361
+ for (const binding of bindings) {
362
+ let platformMessageId = messageIds[binding.key];
363
+ switch (event.state) {
364
+ case 'started':
365
+ await safely('placeholder send', async () => {
366
+ const displayName = actorDisplayName || '猫猫';
367
+ const placeholderLine = event.placeholderLine ?? STARTED_TEXT;
368
+ const candidate = await callbacks.sendPlaceholder(binding.key, `【${displayName}🐱${senderSuffix}】${placeholderLine}`);
369
+ platformMessageId = candidate.length > 0 ? candidate : undefined;
370
+ if (platformMessageId !== undefined) {
371
+ await callbacks.onPlaceholder?.(binding.key, platformMessageId, event.executionId);
372
+ }
373
+ });
374
+ break;
375
+ case 'resumed':
376
+ await safely('resumed feedback', async () => {
377
+ const text = `【${actorDisplayName || '猫猫'}🐱】${STARTED_TEXT}`;
378
+ if (platformMessageId !== undefined && await callbacks.editPlaceholder(binding.key, platformMessageId, text, 'catching_up', event.executionId))
379
+ return;
380
+ const candidate = await callbacks.sendPlaceholder(binding.key, text);
381
+ platformMessageId = candidate || undefined;
382
+ if (platformMessageId)
383
+ await callbacks.onPlaceholder?.(binding.key, platformMessageId, event.executionId);
384
+ });
385
+ break;
386
+ case 'catching_up':
387
+ if (platformMessageId !== undefined) {
388
+ await safely('catching-up edit', () => callbacks.editPlaceholder(binding.key, platformMessageId, CATCHING_UP_TEXT, 'catching_up', event.executionId));
389
+ }
390
+ break;
391
+ case 'blocked':
392
+ {
393
+ const text = recoveryText(event);
394
+ const edited = platformMessageId !== undefined && await safely('blocked edit', async () => {
395
+ const applied = await callbacks.editPlaceholder(binding.key, platformMessageId, text, 'blocked', event.executionId);
396
+ if (!applied)
397
+ throw new Error('placeholder is no longer editable');
398
+ });
399
+ if (!edited) {
400
+ await safely('blocked recovery send', () => callbacks.sendRecovery(binding.key, text));
401
+ }
402
+ }
403
+ break;
404
+ case 'settled': {
405
+ await safely('settlement', () => callbacks.settle({
406
+ externalConversationId: binding.key,
407
+ ...(platformMessageId === undefined ? {} : { platformMessageId }),
408
+ actorDisplayName,
409
+ ...(blocked === undefined ? {} : { recoveryText: recoveryText(blocked) }),
410
+ event,
411
+ }));
412
+ break;
413
+ }
414
+ }
415
+ if (platformMessageId === undefined)
416
+ delete messageIds[binding.key];
417
+ else
418
+ messageIds[binding.key] = platformMessageId;
419
+ }
420
+ try {
421
+ await context.storage.set(stateKey, nextRecord(messageIds));
422
+ countWrite();
423
+ }
424
+ catch (error) {
425
+ // Post-write failure is warn-only: the event already sits in history
426
+ // via the write-ahead, so a Host replay of this delivery is answered
427
+ // as replay and never re-runs the platform side effects. The recovery
428
+ // effect is the exception: its marker survives until a redelivery
429
+ // re-executes it, so a lost post-write there is duplicate-tolerant
430
+ // rather than losing the hint. The only other loss is the fresh
431
+ // platformMessageId, covered by the existing
432
+ // undefined-platformMessageId fallback for later edits.
433
+ context.log('warn', 'Connector lifecycle state post-write failed', {
434
+ executionId: event.executionId,
435
+ state: event.state,
436
+ errorName: error instanceof Error ? error.name : 'unknown',
437
+ });
438
+ }
439
+ return { deliveryId: event.deliveryId };
440
+ };
441
+ const action = defineLifecycleAction(async (event) => {
442
+ const previous = tails.get(event.executionId) ?? Promise.resolve();
443
+ const current = previous.catch(() => undefined).then(() => handle(event));
444
+ const tail = current.then(() => undefined, () => undefined);
445
+ tails.set(event.executionId, tail);
446
+ try {
447
+ return await current;
448
+ }
449
+ finally {
450
+ if (tails.get(event.executionId) === tail)
451
+ tails.delete(event.executionId);
452
+ }
453
+ });
454
+ // Test/debug hook: force the background sweep synchronously.
455
+ return Object.assign(action, { sweepNow: sweep });
456
+ }
457
+ //# sourceMappingURL=lifecycle-action.js.map