@krischoichoi/channel-lark 0.6.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 +114 -0
- package/lib/adapter.d.ts +128 -0
- package/lib/adapter.d.ts.map +1 -0
- package/lib/adapter.js +347 -0
- package/lib/adapter.js.map +1 -0
- package/lib/auth/device-authorization.d.ts +12 -0
- package/lib/auth/device-authorization.d.ts.map +1 -0
- package/lib/auth/device-authorization.js +169 -0
- package/lib/auth/device-authorization.js.map +1 -0
- package/lib/card.d.ts +124 -0
- package/lib/card.d.ts.map +1 -0
- package/lib/card.js +347 -0
- package/lib/card.js.map +1 -0
- package/lib/config.d.ts +103 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +60 -0
- package/lib/config.js.map +1 -0
- package/lib/definition.d.ts +74 -0
- package/lib/definition.d.ts.map +1 -0
- package/lib/definition.js +190 -0
- package/lib/definition.js.map +1 -0
- package/lib/inbound.d.ts +41 -0
- package/lib/inbound.d.ts.map +1 -0
- package/lib/inbound.js +102 -0
- package/lib/inbound.js.map +1 -0
- package/lib/index.d.ts +58 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +104 -0
- package/lib/index.js.map +1 -0
- package/lib/lark-sdk-upstream.d.ts +128 -0
- package/lib/lark-sdk-upstream.d.ts.map +1 -0
- package/lib/lark-sdk-upstream.js +339 -0
- package/lib/lark-sdk-upstream.js.map +1 -0
- package/lib/manifest.d.ts +23 -0
- package/lib/manifest.d.ts.map +1 -0
- package/lib/manifest.js +36 -0
- package/lib/manifest.js.map +1 -0
- package/lib/mapper.d.ts +47 -0
- package/lib/mapper.d.ts.map +1 -0
- package/lib/mapper.js +223 -0
- package/lib/mapper.js.map +1 -0
- package/lib/media-hydrator.d.ts +55 -0
- package/lib/media-hydrator.d.ts.map +1 -0
- package/lib/media-hydrator.js +172 -0
- package/lib/media-hydrator.js.map +1 -0
- package/lib/openapi-outbound.d.ts +216 -0
- package/lib/openapi-outbound.d.ts.map +1 -0
- package/lib/openapi-outbound.js +384 -0
- package/lib/openapi-outbound.js.map +1 -0
- package/lib/outbound.d.ts +21 -0
- package/lib/outbound.d.ts.map +1 -0
- package/lib/outbound.js +93 -0
- package/lib/outbound.js.map +1 -0
- package/lib/upstream/media-port.d.ts +154 -0
- package/lib/upstream/media-port.d.ts.map +1 -0
- package/lib/upstream/media-port.js +168 -0
- package/lib/upstream/media-port.js.map +1 -0
- package/lib/upstream.d.ts +119 -0
- package/lib/upstream.d.ts.map +1 -0
- package/lib/upstream.js +2 -0
- package/lib/upstream.js.map +1 -0
- package/package.json +46 -0
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @krischoichoi/channel-lark — Lark / Feishu channel adapter for DeepSeek Harness.
|
|
3
|
+
*
|
|
4
|
+
* Maps the Lark platform to the stable Channel Contract. The upstream is the
|
|
5
|
+
* OFFICIAL `@larksuiteoapi/node-sdk` only:
|
|
6
|
+
* - inbound — WSClient + EventDispatcher (`im.message.receive_v1` +
|
|
7
|
+
* `card.action.trigger`);
|
|
8
|
+
* - outbound — the official OpenAPI client (`im.v1.message.create` / `patch`,
|
|
9
|
+
* media uploads, CardKit 2.0 card entities and native streaming).
|
|
10
|
+
* The AppId is a plain config string (`upstream.appId`); the AppSecret is
|
|
11
|
+
* resolved via `ctx.credentials` (`DSH_CHANNEL_LARK_MAIN_APP_SECRET`) — only
|
|
12
|
+
* the reference name ever lives in config. The secret value is never logged.
|
|
13
|
+
* There is no self-hosted gateway mode, transport injection, plaintext secret
|
|
14
|
+
* field, or runtime migration path.
|
|
15
|
+
*
|
|
16
|
+
* Lifecycle: when the Channel Control Plane (`ctx.channelControl`) is present,
|
|
17
|
+
* apply() registers a `ChannelDefinition` ('lark'); the control plane decides
|
|
18
|
+
* when to instantiate/mount the adapter (headless auto-start). When it is
|
|
19
|
+
* absent (standalone / older harness), apply() falls back to resolving SDK
|
|
20
|
+
* credentials and mounting directly — never throwing when a channel is merely
|
|
21
|
+
* unconfigured.
|
|
22
|
+
*
|
|
23
|
+
* Streaming is `edit` (CardKit 2.0 native streaming card): create card entity
|
|
24
|
+
* → send card reference → stream element content → close streaming at end.
|
|
25
|
+
* Threads are preserved (`conversation.threadId`) so Harness sessions isolate
|
|
26
|
+
* per thread. Auth is connection-state driven — the driver owns platform
|
|
27
|
+
* credentials (never logged).
|
|
28
|
+
*/
|
|
29
|
+
import { type Context } from '@deepseek-ai/cordis';
|
|
30
|
+
import type { LarkConfig, LarkConfigLive } from './config.js';
|
|
31
|
+
import { Config, LARK_APP_SECRET_REF } from './config.js';
|
|
32
|
+
import { type LarkAdapterDeps } from './adapter.js';
|
|
33
|
+
export declare const name = "channel-lark";
|
|
34
|
+
export declare const inject: string[];
|
|
35
|
+
export { Config, LARK_APP_SECRET_REF };
|
|
36
|
+
export { createLarkDefinition, type LarkCredentialSeam, type CreateLarkDefinitionOptions } from './definition.js';
|
|
37
|
+
export { beginLarkDeviceAuthorization, pollLarkDeviceAuthorization, type LarkDeviceAuthorizationOptions, } from './auth/device-authorization.js';
|
|
38
|
+
export { LarkAdapter, resolveDomain, type LarkAdapterDeps } from './adapter.js';
|
|
39
|
+
export { LarkCardReply, truncateSummary, splitForRollover, type LarkCardStatus, type LarkCardUpdate } from './card.js';
|
|
40
|
+
export { InboundProcessor } from './inbound.js';
|
|
41
|
+
export { OutboundSender } from './outbound.js';
|
|
42
|
+
export { type LarkOutbound, type LarkUpstream, type LarkFileRef, type LarkMediaRef, type LarkStreamingCardRef, } from './upstream.js';
|
|
43
|
+
export { LarkOpenApiOutbound, receiveIdType, cardContent, interactiveCardContent, streamingCardJson, STREAM_MARKDOWN_ELEMENT_ID, type LarkOpenApiClient, type LarkOpenApiOutboundOptions, type LarkCreateMessagePayload, type LarkCreateMessageResult, type LarkReceiveIdType, type LarkPatchMessagePayload, type LarkCreateImagePayload, type LarkCreateImageResult, fileTypeFromName, type LarkFileType, type LarkCreateFilePayload, type LarkCreateFileResult, type LarkApiResponse, type LarkCardkitCardCreatePayload, type LarkCardkitCardCreateResult, type LarkCardElementContentPayload, type LarkCardSettingsPayload, } from './openapi-outbound.js';
|
|
44
|
+
export { LarkSdkUpstream, mapSdkMessageEvent, mapSdkCardAction, MESSAGE_EVENT_KEY, CARD_ACTION_EVENT_KEY, type LarkSdkClient, type LarkSdkDispatcher, type LarkSdkUpstreamOptions, type LarkMessageEventData, } from './lark-sdk-upstream.js';
|
|
45
|
+
export { LarkOpenApiMediaPort, type LarkMediaClient, type LarkMediaPort, type LarkMediaPortOptions, type LarkResourceType, type LarkMessageResourceGetPayload, type LarkMessageResourceResult, type LarkImageGetPayload, type LarkImageCreatePayload, type LarkImageCreateResult, type LarkFileCreatePayload, type LarkFileCreateResult, } from './upstream/media-port.js';
|
|
46
|
+
export { MediaHydrator, ImageHydrator, classifyIngressFailure, type MediaHydratorOptions, type ImageHydratorOptions, } from './media-hydrator.js';
|
|
47
|
+
export { mapInbound, mapInteraction, toTextPayload, dedupKey, simpleHash, type LarkTextPayload, } from './mapper.js';
|
|
48
|
+
export { manifest, type LarkManifest } from './manifest.js';
|
|
49
|
+
export type { LarkConfigLive } from './config.js';
|
|
50
|
+
/**
|
|
51
|
+
* Entry plugin apply. Accepts both the activation-time live shape (volatile
|
|
52
|
+
* fields resolve to `Volatile<T>` handles under Harness 0.2.0) and a plain
|
|
53
|
+
* snapshot (tests / programmatic mounting); plain fields pass through
|
|
54
|
+
* `resolveVolatileConfig` untouched. The shape is validated by Schemastery at
|
|
55
|
+
* the Loader boundary via the attached `Config` schema.
|
|
56
|
+
*/
|
|
57
|
+
export declare function apply(ctx: Context, rawConfig: LarkConfig | LarkConfigLive, deps?: LarkAdapterDeps): void;
|
|
58
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAKnD,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAe,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAGjE,eAAO,MAAM,IAAI,iBAAiB,CAAC;AACnC,eAAO,MAAM,MAAM,EAAE,MAAM,EAAgC,CAAC;AAE5D,OAAO,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;AACvC,OAAO,EAAE,oBAAoB,EAAE,KAAK,kBAAkB,EAAE,KAAK,2BAA2B,EAAE,MAAM,iBAAiB,CAAC;AAClH,OAAO,EACL,4BAA4B,EAC5B,2BAA2B,EAC3B,KAAK,8BAA8B,GACpC,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAChF,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,gBAAgB,EAAE,KAAK,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AACvH,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,oBAAoB,GAC1B,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,mBAAmB,EACnB,aAAa,EACb,WAAW,EACX,sBAAsB,EACtB,iBAAiB,EACjB,0BAA0B,EAC1B,KAAK,iBAAiB,EACtB,KAAK,0BAA0B,EAC/B,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,EAC5B,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,gBAAgB,EAChB,KAAK,YAAY,EACjB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,4BAA4B,EACjC,KAAK,2BAA2B,EAChC,KAAK,6BAA6B,EAClC,KAAK,uBAAuB,GAC7B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,qBAAqB,EACrB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,GAC1B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,6BAA6B,EAClC,KAAK,yBAAyB,EAC9B,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,GAC1B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,aAAa,EACb,aAAa,EACb,sBAAsB,EACtB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,GAC1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,UAAU,EACV,cAAc,EACd,aAAa,EACb,QAAQ,EACR,UAAU,EACV,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,QAAQ,EAAE,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAC5D,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD;;;;;;GAMG;AACH,wBAAgB,KAAK,CACnB,GAAG,EAAE,OAAO,EACZ,SAAS,EAAE,UAAU,GAAG,cAAc,EACtC,IAAI,GAAE,eAAoB,GACzB,IAAI,CA2DN"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @krischoichoi/channel-lark — Lark / Feishu channel adapter for DeepSeek Harness.
|
|
3
|
+
*
|
|
4
|
+
* Maps the Lark platform to the stable Channel Contract. The upstream is the
|
|
5
|
+
* OFFICIAL `@larksuiteoapi/node-sdk` only:
|
|
6
|
+
* - inbound — WSClient + EventDispatcher (`im.message.receive_v1` +
|
|
7
|
+
* `card.action.trigger`);
|
|
8
|
+
* - outbound — the official OpenAPI client (`im.v1.message.create` / `patch`,
|
|
9
|
+
* media uploads, CardKit 2.0 card entities and native streaming).
|
|
10
|
+
* The AppId is a plain config string (`upstream.appId`); the AppSecret is
|
|
11
|
+
* resolved via `ctx.credentials` (`DSH_CHANNEL_LARK_MAIN_APP_SECRET`) — only
|
|
12
|
+
* the reference name ever lives in config. The secret value is never logged.
|
|
13
|
+
* There is no self-hosted gateway mode, transport injection, plaintext secret
|
|
14
|
+
* field, or runtime migration path.
|
|
15
|
+
*
|
|
16
|
+
* Lifecycle: when the Channel Control Plane (`ctx.channelControl`) is present,
|
|
17
|
+
* apply() registers a `ChannelDefinition` ('lark'); the control plane decides
|
|
18
|
+
* when to instantiate/mount the adapter (headless auto-start). When it is
|
|
19
|
+
* absent (standalone / older harness), apply() falls back to resolving SDK
|
|
20
|
+
* credentials and mounting directly — never throwing when a channel is merely
|
|
21
|
+
* unconfigured.
|
|
22
|
+
*
|
|
23
|
+
* Streaming is `edit` (CardKit 2.0 native streaming card): create card entity
|
|
24
|
+
* → send card reference → stream element content → close streaming at end.
|
|
25
|
+
* Threads are preserved (`conversation.threadId`) so Harness sessions isolate
|
|
26
|
+
* per thread. Auth is connection-state driven — the driver owns platform
|
|
27
|
+
* credentials (never logged).
|
|
28
|
+
*/
|
|
29
|
+
import {} from '@deepseek-ai/cordis';
|
|
30
|
+
import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
31
|
+
import { mountChannelAdapter, resolveVolatileConfig, resolveVolatileValue } from '@krischoichoi/channel-core';
|
|
32
|
+
import { Config, LARK_APP_SECRET_REF } from './config.js';
|
|
33
|
+
import { LarkAdapter } from './adapter.js';
|
|
34
|
+
import { createLarkDefinition } from './definition.js';
|
|
35
|
+
export const name = 'channel-lark';
|
|
36
|
+
export const inject = ['channels', 'credentials'];
|
|
37
|
+
export { Config, LARK_APP_SECRET_REF };
|
|
38
|
+
export { createLarkDefinition } from './definition.js';
|
|
39
|
+
export { beginLarkDeviceAuthorization, pollLarkDeviceAuthorization, } from './auth/device-authorization.js';
|
|
40
|
+
export { LarkAdapter, resolveDomain } from './adapter.js';
|
|
41
|
+
export { LarkCardReply, truncateSummary, splitForRollover } from './card.js';
|
|
42
|
+
export { InboundProcessor } from './inbound.js';
|
|
43
|
+
export { OutboundSender } from './outbound.js';
|
|
44
|
+
export {} from './upstream.js';
|
|
45
|
+
export { LarkOpenApiOutbound, receiveIdType, cardContent, interactiveCardContent, streamingCardJson, STREAM_MARKDOWN_ELEMENT_ID, fileTypeFromName, } from './openapi-outbound.js';
|
|
46
|
+
export { LarkSdkUpstream, mapSdkMessageEvent, mapSdkCardAction, MESSAGE_EVENT_KEY, CARD_ACTION_EVENT_KEY, } from './lark-sdk-upstream.js';
|
|
47
|
+
export { LarkOpenApiMediaPort, } from './upstream/media-port.js';
|
|
48
|
+
export { MediaHydrator, ImageHydrator, classifyIngressFailure, } from './media-hydrator.js';
|
|
49
|
+
export { mapInbound, mapInteraction, toTextPayload, dedupKey, simpleHash, } from './mapper.js';
|
|
50
|
+
export { manifest } from './manifest.js';
|
|
51
|
+
/**
|
|
52
|
+
* Entry plugin apply. Accepts both the activation-time live shape (volatile
|
|
53
|
+
* fields resolve to `Volatile<T>` handles under Harness 0.2.0) and a plain
|
|
54
|
+
* snapshot (tests / programmatic mounting); plain fields pass through
|
|
55
|
+
* `resolveVolatileConfig` untouched. The shape is validated by Schemastery at
|
|
56
|
+
* the Loader boundary via the attached `Config` schema.
|
|
57
|
+
*/
|
|
58
|
+
export function apply(ctx, rawConfig, deps = {}) {
|
|
59
|
+
const config = resolveVolatileConfig(rawConfig);
|
|
60
|
+
// Adapt the CredentialProvider to the structural seam expected by the
|
|
61
|
+
// definition (credentialRef branding is applied here, once).
|
|
62
|
+
const seam = {
|
|
63
|
+
resolve: (ref) => ctx.credentials.resolve(credentialRef(ref)),
|
|
64
|
+
describe: (ref) => ctx.credentials.describe(credentialRef(ref)),
|
|
65
|
+
set: (ref, value) => ctx.credentials.set(credentialRef(ref), value),
|
|
66
|
+
};
|
|
67
|
+
const control = ctx.get('channelControl');
|
|
68
|
+
if (control) {
|
|
69
|
+
// Control plane present: register the definition EVEN when disabled — the
|
|
70
|
+
// plane owns adapter instantiation + headless auto-start, and a disabled
|
|
71
|
+
// definition must stay visible so the Web control plane can re-enable it
|
|
72
|
+
// later.
|
|
73
|
+
const settings = ctx.get('settings');
|
|
74
|
+
control.definitions.register(createLarkDefinition({
|
|
75
|
+
config,
|
|
76
|
+
deps,
|
|
77
|
+
credentials: seam,
|
|
78
|
+
persistSetup: (patch) => settings?.update('channels-lark', patch) ?? Promise.resolve(),
|
|
79
|
+
persistEnabled: (enabled) => settings?.update('channels-lark', { enabled }) ?? Promise.resolve(),
|
|
80
|
+
}));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// Legacy fallback (standalone, no control plane): mount directly. An
|
|
84
|
+
// unconfigured adapter must NOT throw — log a warning and stay idle. In this
|
|
85
|
+
// mode there is no directory/control surface, so the config `enabled` gate
|
|
86
|
+
// still applies.
|
|
87
|
+
if (!resolveVolatileValue(config.enabled))
|
|
88
|
+
return;
|
|
89
|
+
ctx.effect(async () => {
|
|
90
|
+
const appId = config.upstream.appId;
|
|
91
|
+
const appSecretRef = config.upstream.appSecretRef ?? LARK_APP_SECRET_REF;
|
|
92
|
+
const appSecret = (await ctx.credentials.resolve(credentialRef(appSecretRef)))?.value;
|
|
93
|
+
if (!appId || !appSecret) {
|
|
94
|
+
ctx.logger('channel-lark').warn(`[channel-lark] not configured (missing appId or appSecret ref "${appSecretRef}"); adapter not mounted`);
|
|
95
|
+
return () => { };
|
|
96
|
+
}
|
|
97
|
+
const adapter = new LarkAdapter(config, { ...deps, appId, appSecret });
|
|
98
|
+
mountChannelAdapter(ctx, adapter, (signal) => ctx.channels.createAdapterContext({ channelId: 'lark', signal }));
|
|
99
|
+
// The mount owns the adapter lifecycle; this outer effect only scopes the
|
|
100
|
+
// async credential resolution, so its disposer is a no-op.
|
|
101
|
+
return () => { };
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { LarkFileRef, LarkMediaRef, LarkOutbound, LarkUpstream } from './upstream.js';
|
|
3
|
+
/** Event type key for inbound message delivery (v1 event). */
|
|
4
|
+
export declare const MESSAGE_EVENT_KEY = "im.message.receive_v1";
|
|
5
|
+
/** Official event name for Feishu interactive-card button presses. */
|
|
6
|
+
export declare const CARD_ACTION_EVENT_KEY = "card.action.trigger";
|
|
7
|
+
/**
|
|
8
|
+
* Minimal structural dispatcher surface consumed by the driver. The real SDK
|
|
9
|
+
* `EventDispatcher` satisfies it (register / invoke); the WS client invokes
|
|
10
|
+
* `invoke(data, { needCheck: false })` for every inbound event frame.
|
|
11
|
+
*/
|
|
12
|
+
export interface LarkSdkDispatcher {
|
|
13
|
+
/** Register handlers keyed by event type (e.g. 'im.message.receive_v1'). */
|
|
14
|
+
register(handles: Record<string, (...args: any[]) => unknown>): unknown;
|
|
15
|
+
/** Dispatch one raw event envelope to the registered handler. */
|
|
16
|
+
invoke(data: unknown, params?: {
|
|
17
|
+
needCheck?: boolean;
|
|
18
|
+
}): Promise<unknown>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Minimal structural WS client surface consumed by the driver. The real SDK
|
|
22
|
+
* `WSClient` satisfies it (start / close); tests inject a fake without a
|
|
23
|
+
* WebSocket.
|
|
24
|
+
*/
|
|
25
|
+
export interface LarkSdkClient {
|
|
26
|
+
/** Open the WS long-connection and start dispatching events. */
|
|
27
|
+
start(params: {
|
|
28
|
+
eventDispatcher: LarkSdkDispatcher;
|
|
29
|
+
}): Promise<void>;
|
|
30
|
+
/** Close the WS long-connection (sync in the real SDK). */
|
|
31
|
+
close(params?: {
|
|
32
|
+
force?: boolean;
|
|
33
|
+
}): void;
|
|
34
|
+
}
|
|
35
|
+
export interface LarkSdkUpstreamOptions {
|
|
36
|
+
/** The WS long-connection client (real WSClient or injected fake). */
|
|
37
|
+
client: LarkSdkClient;
|
|
38
|
+
/**
|
|
39
|
+
* Official OpenAPI outbound delegate (`LarkOpenApiOutbound` in production;
|
|
40
|
+
* any `LarkOutbound` fake in tests).
|
|
41
|
+
*/
|
|
42
|
+
outbound: LarkOutbound;
|
|
43
|
+
/** Official OpenAPI chat lookup; absence deliberately fails card actions closed. */
|
|
44
|
+
resolveChatType?: (conversationId: string) => Promise<'p2p' | 'group' | undefined>;
|
|
45
|
+
/** Invoked after the WS connection is established (connection state). */
|
|
46
|
+
onConnected?: () => void;
|
|
47
|
+
}
|
|
48
|
+
declare const larkMessageEventDataSchema: z.ZodObject<{
|
|
49
|
+
event_id: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
|
|
50
|
+
event_type: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
|
|
51
|
+
token: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
|
|
52
|
+
create_time: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
|
|
53
|
+
sender: z.ZodOptional<z.ZodObject<{
|
|
54
|
+
sender_id: z.ZodOptional<z.ZodObject<{
|
|
55
|
+
union_id: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
|
|
56
|
+
user_id: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
|
|
57
|
+
open_id: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
|
|
58
|
+
}, z.core.$loose>>;
|
|
59
|
+
sender_type: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
|
|
60
|
+
tenant_key: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
|
|
61
|
+
}, z.core.$loose>>;
|
|
62
|
+
message: z.ZodOptional<z.ZodObject<{
|
|
63
|
+
message_id: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
|
|
64
|
+
root_id: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
|
|
65
|
+
parent_id: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
|
|
66
|
+
thread_id: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
|
|
67
|
+
create_time: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
|
|
68
|
+
chat_id: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
|
|
69
|
+
chat_type: z.ZodEnum<{
|
|
70
|
+
p2p: "p2p";
|
|
71
|
+
group: "group";
|
|
72
|
+
}>;
|
|
73
|
+
message_type: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
|
|
74
|
+
content: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
|
|
75
|
+
}, z.core.$loose>>;
|
|
76
|
+
}, z.core.$loose>;
|
|
77
|
+
export type LarkMessageEventData = z.infer<typeof larkMessageEventDataSchema>;
|
|
78
|
+
/**
|
|
79
|
+
* Map one parsed v1 message event into the canonical raw envelope consumed by
|
|
80
|
+
* the inbound mapper (`{ type, msgId, eventId, senderId, conversationId,
|
|
81
|
+
* chatType, threadId, content, ... }`). Returns `undefined` when the event
|
|
82
|
+
* carries no message body.
|
|
83
|
+
*/
|
|
84
|
+
export declare function mapSdkMessageEvent(input: unknown): Record<string, unknown> | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* Convert the official SDK-normalized card action into the canonical
|
|
87
|
+
* interaction envelope. The platform callback itself is never hand-parsed —
|
|
88
|
+
* it is first trust-boundary validated with zod and then normalized by the
|
|
89
|
+
* official SDK's `normalizeCardAction()`.
|
|
90
|
+
*/
|
|
91
|
+
export declare function mapSdkCardAction(input: unknown, resolveChatType: ((conversationId: string) => Promise<'p2p' | 'group' | undefined>) | undefined): Promise<Record<string, unknown> | undefined>;
|
|
92
|
+
/** SDK-mode implementation of `LarkUpstream` (inbound via the SDK). */
|
|
93
|
+
export declare class LarkSdkUpstream implements LarkUpstream {
|
|
94
|
+
private readonly options;
|
|
95
|
+
/**
|
|
96
|
+
* Real SDK dispatcher: the registered handler is invoked for every
|
|
97
|
+
* `im.message.receive_v1` frame the WS client delivers. Reused across
|
|
98
|
+
* reconnects (the handler is registered once per driver instance).
|
|
99
|
+
*/
|
|
100
|
+
private readonly dispatcher;
|
|
101
|
+
private registered;
|
|
102
|
+
private onMessage?;
|
|
103
|
+
constructor(options: LarkSdkUpstreamOptions);
|
|
104
|
+
/**
|
|
105
|
+
* Connect the WS long-connection, route inbound message events into the
|
|
106
|
+
* canonical raw shape, and keep the connection open until `signal` aborts.
|
|
107
|
+
*/
|
|
108
|
+
receive(signal: AbortSignal, onMessage: (raw: unknown) => void): Promise<void>;
|
|
109
|
+
private registerOnce;
|
|
110
|
+
sendText(to: string, text: string): Promise<unknown>;
|
|
111
|
+
sendMedia(to: string, media: LarkMediaRef): Promise<unknown>;
|
|
112
|
+
sendFile(to: string, file: LarkFileRef): Promise<unknown>;
|
|
113
|
+
sendInteractive(to: string, text: string, actions: import('@krischoichoi/channel-core').OutboundActionRow[]): Promise<unknown>;
|
|
114
|
+
updateInteractive(cardId: string, text: string, actions: import('@krischoichoi/channel-core').OutboundActionRow[]): Promise<unknown>;
|
|
115
|
+
createCardEntity(cardJson: string): Promise<{
|
|
116
|
+
cardId: string;
|
|
117
|
+
}>;
|
|
118
|
+
sendCardEntity(conversationId: string, cardId: string): Promise<{
|
|
119
|
+
messageId: string;
|
|
120
|
+
}>;
|
|
121
|
+
updateCardElementContent(cardId: string, elementId: string, content: string, sequence: number, uuid: string): Promise<unknown>;
|
|
122
|
+
finishStreamingCard(cardId: string, sequence: number, summary: string): Promise<unknown>;
|
|
123
|
+
startTyping(messageId: string): Promise<void>;
|
|
124
|
+
stopTyping(messageId: string): Promise<void>;
|
|
125
|
+
getChatType(conversationId: string): Promise<'p2p' | 'group' | undefined>;
|
|
126
|
+
}
|
|
127
|
+
export {};
|
|
128
|
+
//# sourceMappingURL=lark-sdk-upstream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lark-sdk-upstream.d.ts","sourceRoot":"","sources":["../src/lark-sdk-upstream.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lark SDK upstream driver (official `@larksuiteoapi/node-sdk`).
|
|
3
|
+
*
|
|
4
|
+
* This driver is the ONLY inbound implementation: robot messages arrive over
|
|
5
|
+
* the official SDK's WebSocket long-connection (`WSClient` +
|
|
6
|
+
* `EventDispatcher`, event `im.message.receive_v1`) and interactive-card
|
|
7
|
+
* button presses arrive over `card.action.trigger`. Both are mapped into the
|
|
8
|
+
* canonical raw envelope the adapter's mapper consumes (`{ type, msgId,
|
|
9
|
+
* eventId, senderId, conversationId, chatType, threadId, ... }`) — the mapper
|
|
10
|
+
* is SDK-event-driven, there is no gateway raw shape anywhere.
|
|
11
|
+
*
|
|
12
|
+
* Thread replies are preserved: the SDK's `message.thread_id` / `root_id` /
|
|
13
|
+
* `parent_id` map into the canonical `threadId` the mapper uses to build the
|
|
14
|
+
* thread-scoped SessionBinding.
|
|
15
|
+
*
|
|
16
|
+
* OUTBOUND is delegated to the injected `LarkOutbound` — the official OpenAPI
|
|
17
|
+
* driver (`LarkOpenApiOutbound`) in production, or an injected fake in tests.
|
|
18
|
+
* This bounded split keeps the inbound and outbound legs independently
|
|
19
|
+
* swappable and fully offline-testable.
|
|
20
|
+
*
|
|
21
|
+
* Credentials never appear in this module (the client is built elsewhere from
|
|
22
|
+
* config) and are never logged. Live verification against a real Lark app
|
|
23
|
+
* (AppId/AppSecret) is a manual step — the offline tests inject a fake WS
|
|
24
|
+
* client and drive a real `EventDispatcher` with v1 event envelopes.
|
|
25
|
+
*/
|
|
26
|
+
import { EventDispatcher, LoggerLevel, normalizeCardAction } from '@larksuiteoapi/node-sdk';
|
|
27
|
+
import { ChannelError } from '@krischoichoi/channel-core';
|
|
28
|
+
import { z } from 'zod';
|
|
29
|
+
/** Event type key for inbound message delivery (v1 event). */
|
|
30
|
+
export const MESSAGE_EVENT_KEY = 'im.message.receive_v1';
|
|
31
|
+
/** Official event name for Feishu interactive-card button presses. */
|
|
32
|
+
export const CARD_ACTION_EVENT_KEY = 'card.action.trigger';
|
|
33
|
+
/**
|
|
34
|
+
* Parsed v1 message event payload — the flat shape the `EventDispatcher`
|
|
35
|
+
* delivers to the `im.message.receive_v1` handler (header + event fields
|
|
36
|
+
* merged, per the SDK's RequestHandle.parse).
|
|
37
|
+
*/
|
|
38
|
+
const optionalStringSchema = z.string().nullish().transform((value) => value ?? undefined);
|
|
39
|
+
const larkMessageEventDataSchema = z.object({
|
|
40
|
+
event_id: optionalStringSchema,
|
|
41
|
+
event_type: optionalStringSchema,
|
|
42
|
+
token: optionalStringSchema,
|
|
43
|
+
create_time: optionalStringSchema,
|
|
44
|
+
sender: z.object({
|
|
45
|
+
sender_id: z.object({
|
|
46
|
+
union_id: optionalStringSchema,
|
|
47
|
+
user_id: optionalStringSchema,
|
|
48
|
+
open_id: optionalStringSchema,
|
|
49
|
+
}).passthrough().optional(),
|
|
50
|
+
sender_type: optionalStringSchema,
|
|
51
|
+
tenant_key: optionalStringSchema,
|
|
52
|
+
}).passthrough().optional(),
|
|
53
|
+
message: z.object({
|
|
54
|
+
message_id: optionalStringSchema,
|
|
55
|
+
root_id: optionalStringSchema,
|
|
56
|
+
parent_id: optionalStringSchema,
|
|
57
|
+
thread_id: optionalStringSchema,
|
|
58
|
+
create_time: optionalStringSchema,
|
|
59
|
+
chat_id: optionalStringSchema,
|
|
60
|
+
chat_type: z.enum(['p2p', 'group']),
|
|
61
|
+
message_type: optionalStringSchema,
|
|
62
|
+
/** JSON-encoded message body per the v1 schema, e.g. '{"text":"hi"}'. */
|
|
63
|
+
content: optionalStringSchema,
|
|
64
|
+
}).passthrough().optional(),
|
|
65
|
+
}).passthrough();
|
|
66
|
+
const actionValueSchema = z.object({ actionId: z.string().trim().min(1) });
|
|
67
|
+
// Trust-boundary validation only. Protocol normalization stays in the
|
|
68
|
+
// official SDK's normalizeCardAction() implementation below.
|
|
69
|
+
const officialCardActionInputSchema = z.object({
|
|
70
|
+
event_id: z.string().optional(),
|
|
71
|
+
context: z.object({
|
|
72
|
+
open_message_id: z.string().optional(),
|
|
73
|
+
open_chat_id: z.string().optional(),
|
|
74
|
+
}).optional(),
|
|
75
|
+
open_message_id: z.string().optional(),
|
|
76
|
+
open_chat_id: z.string().optional(),
|
|
77
|
+
operator: z.object({
|
|
78
|
+
open_id: z.string().optional(),
|
|
79
|
+
user_id: z.string().optional(),
|
|
80
|
+
name: z.string().optional(),
|
|
81
|
+
}).optional(),
|
|
82
|
+
action: z.object({
|
|
83
|
+
value: z.unknown().optional(),
|
|
84
|
+
tag: z.string().optional(),
|
|
85
|
+
name: z.string().optional(),
|
|
86
|
+
option: z.string().optional(),
|
|
87
|
+
}).optional(),
|
|
88
|
+
}).passthrough();
|
|
89
|
+
/**
|
|
90
|
+
* Map one parsed v1 message event into the canonical raw envelope consumed by
|
|
91
|
+
* the inbound mapper (`{ type, msgId, eventId, senderId, conversationId,
|
|
92
|
+
* chatType, threadId, content, ... }`). Returns `undefined` when the event
|
|
93
|
+
* carries no message body.
|
|
94
|
+
*/
|
|
95
|
+
export function mapSdkMessageEvent(input) {
|
|
96
|
+
const parsed = larkMessageEventDataSchema.safeParse(input);
|
|
97
|
+
if (!parsed.success) {
|
|
98
|
+
throw new ChannelError('CHANNEL_ERROR', `lark message event payload is invalid: ${zodIssueSummary(parsed.error)}`);
|
|
99
|
+
}
|
|
100
|
+
const data = parsed.data;
|
|
101
|
+
const message = data.message;
|
|
102
|
+
if (!message || typeof message !== 'object')
|
|
103
|
+
return undefined;
|
|
104
|
+
const raw = {
|
|
105
|
+
type: message.message_type,
|
|
106
|
+
msgId: message.message_id,
|
|
107
|
+
eventId: data.event_id,
|
|
108
|
+
senderId: senderIdOf(data.sender),
|
|
109
|
+
conversationId: message.chat_id,
|
|
110
|
+
// Platform chat kind ('p2p' | 'group') is authoritative for ACL identity;
|
|
111
|
+
// Feishu uses oc_ chat ids for both direct and group conversations.
|
|
112
|
+
chatType: message.chat_type,
|
|
113
|
+
};
|
|
114
|
+
// Thread preservation: the mapper keys Harness sessions by
|
|
115
|
+
// conversation.threadId, so replies inside a thread must carry the thread
|
|
116
|
+
// root. thread_id (when present) is canonical; root_id identifies the
|
|
117
|
+
// thread root; parent_id is the direct parent — a fallback for plain
|
|
118
|
+
// replies without a thread.
|
|
119
|
+
const threadId = message.thread_id ?? message.root_id ?? message.parent_id;
|
|
120
|
+
if (threadId)
|
|
121
|
+
raw.threadId = threadId;
|
|
122
|
+
const content = parseContent(message.content);
|
|
123
|
+
switch (message.message_type) {
|
|
124
|
+
case 'text':
|
|
125
|
+
raw.content = content.text;
|
|
126
|
+
break;
|
|
127
|
+
case 'image':
|
|
128
|
+
// SDK image bodies carry an image_key (not a URL); the mapper surfaces
|
|
129
|
+
// it as an opaque resourceRef resolved later by the media port.
|
|
130
|
+
raw.picUrl = content.image_key;
|
|
131
|
+
break;
|
|
132
|
+
case 'audio':
|
|
133
|
+
raw.mediaUrl = content.file_key;
|
|
134
|
+
raw.durationMs = content.duration;
|
|
135
|
+
break;
|
|
136
|
+
case 'media':
|
|
137
|
+
// Feishu video messages use message_type 'media'; the mapper knows 'video'.
|
|
138
|
+
raw.type = 'video';
|
|
139
|
+
raw.mediaUrl = content.file_key;
|
|
140
|
+
raw.durationMs = content.duration;
|
|
141
|
+
break;
|
|
142
|
+
case 'file':
|
|
143
|
+
raw.mediaUrl = content.file_key;
|
|
144
|
+
raw.title = content.file_name;
|
|
145
|
+
break;
|
|
146
|
+
case 'post':
|
|
147
|
+
raw.content = postText(content);
|
|
148
|
+
break;
|
|
149
|
+
default:
|
|
150
|
+
// Keep the SDK message_type; the mapper reports unknown types as
|
|
151
|
+
// unsupported (honest rather than guessing).
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
return raw;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Convert the official SDK-normalized card action into the canonical
|
|
158
|
+
* interaction envelope. The platform callback itself is never hand-parsed —
|
|
159
|
+
* it is first trust-boundary validated with zod and then normalized by the
|
|
160
|
+
* official SDK's `normalizeCardAction()`.
|
|
161
|
+
*/
|
|
162
|
+
export async function mapSdkCardAction(input, resolveChatType) {
|
|
163
|
+
const parsed = officialCardActionInputSchema.safeParse(input);
|
|
164
|
+
if (!parsed.success)
|
|
165
|
+
return undefined;
|
|
166
|
+
const event = normalizeCardAction(parsed.data);
|
|
167
|
+
if (!event)
|
|
168
|
+
return undefined;
|
|
169
|
+
const value = actionValueSchema.safeParse(event.action.value);
|
|
170
|
+
if (!value.success)
|
|
171
|
+
return undefined;
|
|
172
|
+
const chatType = await resolveChatType?.(event.chatId);
|
|
173
|
+
if (!chatType)
|
|
174
|
+
return undefined;
|
|
175
|
+
return {
|
|
176
|
+
type: 'interaction',
|
|
177
|
+
msgId: event.messageId,
|
|
178
|
+
// EventDispatcher preserves the official header event_id in its flat
|
|
179
|
+
// payload. Use it when available so a later deliberate press of the same
|
|
180
|
+
// button is not collapsed with a transport retry of an earlier press.
|
|
181
|
+
eventId: parsed.data.event_id ?? `${event.messageId}:${event.operator.openId}:${value.data.actionId}`,
|
|
182
|
+
senderId: event.operator.openId,
|
|
183
|
+
conversationId: event.chatId,
|
|
184
|
+
chatType,
|
|
185
|
+
interactionId: event.messageId,
|
|
186
|
+
action: value.data.actionId,
|
|
187
|
+
value: event.action.value,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/** SDK-mode implementation of `LarkUpstream` (inbound via the SDK). */
|
|
191
|
+
export class LarkSdkUpstream {
|
|
192
|
+
options;
|
|
193
|
+
/**
|
|
194
|
+
* Real SDK dispatcher: the registered handler is invoked for every
|
|
195
|
+
* `im.message.receive_v1` frame the WS client delivers. Reused across
|
|
196
|
+
* reconnects (the handler is registered once per driver instance).
|
|
197
|
+
*/
|
|
198
|
+
dispatcher;
|
|
199
|
+
registered = false;
|
|
200
|
+
onMessage;
|
|
201
|
+
constructor(options) {
|
|
202
|
+
this.options = options;
|
|
203
|
+
// No verification token needed: the WS long-connection dispatches with
|
|
204
|
+
// needCheck: false (tokens/encrypt are webhook-only concerns).
|
|
205
|
+
this.dispatcher = new EventDispatcher({ loggerLevel: LoggerLevel.fatal });
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Connect the WS long-connection, route inbound message events into the
|
|
209
|
+
* canonical raw shape, and keep the connection open until `signal` aborts.
|
|
210
|
+
*/
|
|
211
|
+
async receive(signal, onMessage) {
|
|
212
|
+
this.onMessage = onMessage;
|
|
213
|
+
this.registerOnce();
|
|
214
|
+
try {
|
|
215
|
+
await this.options.client.start({ eventDispatcher: this.dispatcher });
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
// Abort-driven teardown exits gracefully; other failures propagate to
|
|
219
|
+
// the adapter, which owns reconnect/backoff.
|
|
220
|
+
if (signal.aborted)
|
|
221
|
+
return;
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
if (signal.aborted) {
|
|
225
|
+
this.options.client.close();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
this.options.onConnected?.();
|
|
229
|
+
await waitForAbort(signal);
|
|
230
|
+
this.options.client.close();
|
|
231
|
+
}
|
|
232
|
+
registerOnce() {
|
|
233
|
+
if (this.registered)
|
|
234
|
+
return;
|
|
235
|
+
this.dispatcher.register({
|
|
236
|
+
[MESSAGE_EVENT_KEY]: (data) => {
|
|
237
|
+
if (!this.onMessage)
|
|
238
|
+
return undefined;
|
|
239
|
+
const raw = mapSdkMessageEvent(data);
|
|
240
|
+
if (raw !== undefined)
|
|
241
|
+
this.onMessage(raw);
|
|
242
|
+
return undefined;
|
|
243
|
+
},
|
|
244
|
+
[CARD_ACTION_EVENT_KEY]: async (data) => {
|
|
245
|
+
if (!this.onMessage)
|
|
246
|
+
return undefined;
|
|
247
|
+
const raw = await mapSdkCardAction(data, this.options.resolveChatType);
|
|
248
|
+
if (raw !== undefined)
|
|
249
|
+
this.onMessage(raw);
|
|
250
|
+
return undefined;
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
this.registered = true;
|
|
254
|
+
}
|
|
255
|
+
sendText(to, text) {
|
|
256
|
+
return this.options.outbound.sendText(to, text);
|
|
257
|
+
}
|
|
258
|
+
sendMedia(to, media) {
|
|
259
|
+
return this.options.outbound.sendMedia(to, media);
|
|
260
|
+
}
|
|
261
|
+
sendFile(to, file) {
|
|
262
|
+
return this.options.outbound.sendFile(to, file);
|
|
263
|
+
}
|
|
264
|
+
sendInteractive(to, text, actions) {
|
|
265
|
+
return this.options.outbound.sendInteractive(to, text, actions);
|
|
266
|
+
}
|
|
267
|
+
updateInteractive(cardId, text, actions) {
|
|
268
|
+
return this.options.outbound.updateInteractive(cardId, text, actions);
|
|
269
|
+
}
|
|
270
|
+
createCardEntity(cardJson) {
|
|
271
|
+
return this.options.outbound.createCardEntity(cardJson);
|
|
272
|
+
}
|
|
273
|
+
sendCardEntity(conversationId, cardId) {
|
|
274
|
+
return this.options.outbound.sendCardEntity(conversationId, cardId);
|
|
275
|
+
}
|
|
276
|
+
updateCardElementContent(cardId, elementId, content, sequence, uuid) {
|
|
277
|
+
return this.options.outbound.updateCardElementContent(cardId, elementId, content, sequence, uuid);
|
|
278
|
+
}
|
|
279
|
+
finishStreamingCard(cardId, sequence, summary) {
|
|
280
|
+
return this.options.outbound.finishStreamingCard(cardId, sequence, summary);
|
|
281
|
+
}
|
|
282
|
+
startTyping(messageId) {
|
|
283
|
+
return this.options.outbound.startTyping?.(messageId) ?? Promise.resolve();
|
|
284
|
+
}
|
|
285
|
+
stopTyping(messageId) {
|
|
286
|
+
return this.options.outbound.stopTyping?.(messageId) ?? Promise.resolve();
|
|
287
|
+
}
|
|
288
|
+
getChatType(conversationId) {
|
|
289
|
+
return this.options.outbound.getChatType?.(conversationId) ?? Promise.resolve(undefined);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/** Open id first (the id Harness sessions key on), then union/user ids. */
|
|
293
|
+
function senderIdOf(sender) {
|
|
294
|
+
const id = sender?.sender_id;
|
|
295
|
+
return id?.open_id ?? id?.union_id ?? id?.user_id;
|
|
296
|
+
}
|
|
297
|
+
function parseContent(content) {
|
|
298
|
+
if (!content)
|
|
299
|
+
return {};
|
|
300
|
+
try {
|
|
301
|
+
const parsed = z.record(z.string(), z.unknown()).safeParse(JSON.parse(content));
|
|
302
|
+
return parsed.success ? parsed.data : {};
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
return {};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function zodIssueSummary(error) {
|
|
309
|
+
return error.issues
|
|
310
|
+
.slice(0, 3)
|
|
311
|
+
.map((issue) => `${issue.path.join('.') || '<root>'}: ${issue.message}`)
|
|
312
|
+
.join('; ');
|
|
313
|
+
}
|
|
314
|
+
/** Best-effort plain-text extraction from a rich-text 'post' body. */
|
|
315
|
+
function postText(content) {
|
|
316
|
+
const rows = content.content;
|
|
317
|
+
if (!Array.isArray(rows))
|
|
318
|
+
return '';
|
|
319
|
+
const segments = [];
|
|
320
|
+
for (const row of rows) {
|
|
321
|
+
if (!Array.isArray(row))
|
|
322
|
+
continue;
|
|
323
|
+
for (const node of row) {
|
|
324
|
+
const item = node;
|
|
325
|
+
if (item && typeof item === 'object' && typeof item.text === 'string') {
|
|
326
|
+
segments.push(item.text);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return segments.join('');
|
|
331
|
+
}
|
|
332
|
+
function waitForAbort(signal) {
|
|
333
|
+
if (signal.aborted)
|
|
334
|
+
return Promise.resolve();
|
|
335
|
+
return new Promise((resolve) => {
|
|
336
|
+
signal.addEventListener('abort', () => resolve(), { once: true });
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
//# sourceMappingURL=lark-sdk-upstream.js.map
|