@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.
Files changed (62) hide show
  1. package/README.md +114 -0
  2. package/lib/adapter.d.ts +128 -0
  3. package/lib/adapter.d.ts.map +1 -0
  4. package/lib/adapter.js +347 -0
  5. package/lib/adapter.js.map +1 -0
  6. package/lib/auth/device-authorization.d.ts +12 -0
  7. package/lib/auth/device-authorization.d.ts.map +1 -0
  8. package/lib/auth/device-authorization.js +169 -0
  9. package/lib/auth/device-authorization.js.map +1 -0
  10. package/lib/card.d.ts +124 -0
  11. package/lib/card.d.ts.map +1 -0
  12. package/lib/card.js +347 -0
  13. package/lib/card.js.map +1 -0
  14. package/lib/config.d.ts +103 -0
  15. package/lib/config.d.ts.map +1 -0
  16. package/lib/config.js +60 -0
  17. package/lib/config.js.map +1 -0
  18. package/lib/definition.d.ts +74 -0
  19. package/lib/definition.d.ts.map +1 -0
  20. package/lib/definition.js +190 -0
  21. package/lib/definition.js.map +1 -0
  22. package/lib/inbound.d.ts +41 -0
  23. package/lib/inbound.d.ts.map +1 -0
  24. package/lib/inbound.js +102 -0
  25. package/lib/inbound.js.map +1 -0
  26. package/lib/index.d.ts +58 -0
  27. package/lib/index.d.ts.map +1 -0
  28. package/lib/index.js +104 -0
  29. package/lib/index.js.map +1 -0
  30. package/lib/lark-sdk-upstream.d.ts +128 -0
  31. package/lib/lark-sdk-upstream.d.ts.map +1 -0
  32. package/lib/lark-sdk-upstream.js +339 -0
  33. package/lib/lark-sdk-upstream.js.map +1 -0
  34. package/lib/manifest.d.ts +23 -0
  35. package/lib/manifest.d.ts.map +1 -0
  36. package/lib/manifest.js +36 -0
  37. package/lib/manifest.js.map +1 -0
  38. package/lib/mapper.d.ts +47 -0
  39. package/lib/mapper.d.ts.map +1 -0
  40. package/lib/mapper.js +223 -0
  41. package/lib/mapper.js.map +1 -0
  42. package/lib/media-hydrator.d.ts +55 -0
  43. package/lib/media-hydrator.d.ts.map +1 -0
  44. package/lib/media-hydrator.js +172 -0
  45. package/lib/media-hydrator.js.map +1 -0
  46. package/lib/openapi-outbound.d.ts +216 -0
  47. package/lib/openapi-outbound.d.ts.map +1 -0
  48. package/lib/openapi-outbound.js +384 -0
  49. package/lib/openapi-outbound.js.map +1 -0
  50. package/lib/outbound.d.ts +21 -0
  51. package/lib/outbound.d.ts.map +1 -0
  52. package/lib/outbound.js +93 -0
  53. package/lib/outbound.js.map +1 -0
  54. package/lib/upstream/media-port.d.ts +154 -0
  55. package/lib/upstream/media-port.d.ts.map +1 -0
  56. package/lib/upstream/media-port.js +168 -0
  57. package/lib/upstream/media-port.js.map +1 -0
  58. package/lib/upstream.d.ts +119 -0
  59. package/lib/upstream.d.ts.map +1 -0
  60. package/lib/upstream.js +2 -0
  61. package/lib/upstream.js.map +1 -0
  62. package/package.json +46 -0
@@ -0,0 +1,169 @@
1
+ import { ControlError, } from '@krischoichoi/channel-control';
2
+ import { z } from 'zod';
3
+ const deviceAuthorizationResponseSchema = z
4
+ .object({
5
+ device_code: z.string().trim().min(1).optional(),
6
+ verification_uri_complete: z.string().trim().min(1).optional(),
7
+ verification_uri: z.string().trim().min(1).optional(),
8
+ expires_in: z.number().positive().optional(),
9
+ interval: z.number().positive().optional(),
10
+ error: z.string().trim().min(1).optional(),
11
+ error_description: z.string().trim().min(1).optional(),
12
+ })
13
+ .passthrough();
14
+ const tokenResponseSchema = z
15
+ .object({
16
+ access_token: z.string().trim().min(1).optional(),
17
+ refresh_token: z.string().trim().min(1).optional(),
18
+ error: z.string().trim().min(1).optional(),
19
+ error_description: z.string().trim().min(1).optional(),
20
+ code: z.union([z.string(), z.number()]).optional(),
21
+ msg: z.string().trim().min(1).optional(),
22
+ })
23
+ .passthrough();
24
+ const providerStateSchema = z.object({
25
+ brand: z.enum(['feishu', 'lark']),
26
+ appId: z.string().trim().min(1),
27
+ appSecret: z.string().trim().min(1),
28
+ deviceCode: z.string().trim().min(1),
29
+ scope: z.string().trim().min(1),
30
+ });
31
+ function authError(message, cause) {
32
+ return new ControlError('CONTROL_ERROR', message, cause === undefined ? undefined : { cause });
33
+ }
34
+ function endpoint(domain, resource) {
35
+ return domain === 'lark'
36
+ ? `https://accounts.larksuite.com/oauth/v1/${resource}`
37
+ : `https://accounts.feishu.cn/oauth/v1/${resource}`;
38
+ }
39
+ function tokenEndpoints(brand) {
40
+ return brand === 'lark'
41
+ ? [
42
+ 'https://accounts.larksuite.com/oauth/v1/token',
43
+ 'https://open.larksuite.com/open-apis/authen/v2/oauth/token',
44
+ ]
45
+ : [
46
+ 'https://accounts.feishu.cn/oauth/v1/token',
47
+ 'https://open.feishu.cn/open-apis/authen/v2/oauth/token',
48
+ ];
49
+ }
50
+ async function readJson(response, provider) {
51
+ try {
52
+ return await response.json();
53
+ }
54
+ catch (error) {
55
+ throw authError(`${provider} authorization returned invalid JSON`, error);
56
+ }
57
+ }
58
+ function parseDeviceResponse(value) {
59
+ const parsed = deviceAuthorizationResponseSchema.safeParse(value);
60
+ if (!parsed.success)
61
+ throw authError('lark device authorization returned an invalid response', parsed.error);
62
+ return parsed.data;
63
+ }
64
+ function parseTokenResponse(value) {
65
+ const parsed = tokenResponseSchema.safeParse(value);
66
+ if (!parsed.success)
67
+ throw authError('lark authorization polling returned an invalid response', parsed.error);
68
+ return parsed.data;
69
+ }
70
+ function parseProviderState(value) {
71
+ const parsed = providerStateSchema.safeParse(value);
72
+ if (!parsed.success)
73
+ throw authError('lark auth session is missing provider state', parsed.error);
74
+ return parsed.data;
75
+ }
76
+ /** Begin Feishu/Lark device authorization. Secrets remain in opaque host state. */
77
+ export async function beginLarkDeviceAuthorization(options) {
78
+ const appId = options.appId.trim();
79
+ const appSecret = options.appSecret.trim();
80
+ if (!appId || !appSecret)
81
+ throw new ControlError('AUTH_NOT_READY', 'lark QR authorization requires configured appId and appSecret');
82
+ const fetcher = options.fetcher ?? fetch;
83
+ const brand = options.domain === 'lark' ? 'lark' : 'feishu';
84
+ const scope = 'offline_access';
85
+ let response;
86
+ try {
87
+ response = await fetcher(endpoint(options.domain, 'device_authorization'), {
88
+ method: 'POST',
89
+ headers: {
90
+ authorization: `Basic ${Buffer.from(`${appId}:${appSecret}`).toString('base64')}`,
91
+ 'content-type': 'application/x-www-form-urlencoded',
92
+ },
93
+ body: new URLSearchParams({ client_id: appId, scope }).toString(),
94
+ });
95
+ }
96
+ catch (error) {
97
+ throw authError('lark device authorization request failed', error);
98
+ }
99
+ const payload = parseDeviceResponse(await readJson(response, 'lark'));
100
+ if (!response.ok || payload.error)
101
+ throw authError('lark device authorization request failed');
102
+ const deviceCode = payload.device_code;
103
+ const completeUrl = payload.verification_uri_complete ?? payload.verification_uri;
104
+ if (!deviceCode || !completeUrl)
105
+ throw authError('lark did not return a device authorization challenge');
106
+ const expiresAt = Date.now() + (payload.expires_in ?? 240) * 1000;
107
+ const providerState = { brand, appId, appSecret, deviceCode, scope };
108
+ return {
109
+ provider: 'lark-device-authorization',
110
+ expiresAt,
111
+ pollingIntervalMs: Math.max(3000, (payload.interval ?? 5) * 1000),
112
+ qr: { kind: 'content', value: completeUrl, expiresAt },
113
+ prompt: { kind: 'confirm-on-phone', message: '请使用飞书/Lark 扫码并确认授权' },
114
+ deviceCode,
115
+ providerState,
116
+ };
117
+ }
118
+ /** Poll a device authorization session without exposing provider tokens. */
119
+ export async function pollLarkDeviceAuthorization(session, options = {}) {
120
+ const provider = parseProviderState(session.providerState);
121
+ const fetcher = options.fetcher ?? fetch;
122
+ const attempts = [
123
+ { grant_type: 'urn:ietf:params:oauth:grant-type:device_code', device_code: provider.deviceCode, client_id: provider.appId },
124
+ { grant_type: 'device_code', device_code: provider.deviceCode, client_id: provider.appId },
125
+ { grant_type: 'urn:ietf:params:oauth:grant-type:device_code', device_code: provider.deviceCode },
126
+ { grant_type: 'device_code', device_code: provider.deviceCode },
127
+ ];
128
+ let lastError;
129
+ for (const tokenUrl of tokenEndpoints(provider.brand)) {
130
+ for (const attempt of attempts) {
131
+ let response;
132
+ try {
133
+ response = await fetcher(tokenUrl, {
134
+ method: 'POST',
135
+ headers: {
136
+ authorization: `Basic ${Buffer.from(`${provider.appId}:${provider.appSecret}`).toString('base64')}`,
137
+ 'content-type': 'application/x-www-form-urlencoded',
138
+ },
139
+ body: new URLSearchParams(attempt).toString(),
140
+ });
141
+ }
142
+ catch (error) {
143
+ lastError = error;
144
+ continue;
145
+ }
146
+ let payload;
147
+ try {
148
+ payload = parseTokenResponse(await readJson(response, 'lark'));
149
+ }
150
+ catch (error) {
151
+ lastError = error;
152
+ continue;
153
+ }
154
+ if (response.ok && (payload.access_token || payload.refresh_token)) {
155
+ return { state: 'authenticated', phase: 'authorized', expiresAt: session.expiresAt, detail: '飞书/Lark 扫码授权已完成' };
156
+ }
157
+ const code = payload.error ?? (payload.code === undefined ? '' : String(payload.code));
158
+ if (code === 'authorization_pending' || code === 'slow_down') {
159
+ return { state: 'pending', phase: 'waiting-scan', expiresAt: session.expiresAt, detail: '等待飞书/Lark 扫码授权中' };
160
+ }
161
+ if (code === 'expired_token' || code === 'invalid_grant' || response.status === 401) {
162
+ return { state: 'expired', phase: 'expired', expiresAt: session.expiresAt, detail: '二维码已失效,请重新生成' };
163
+ }
164
+ lastError = new Error(payload.error_description ?? payload.msg ?? `HTTP ${response.status}`);
165
+ }
166
+ }
167
+ throw authError('lark authorization polling failed', lastError);
168
+ }
169
+ //# sourceMappingURL=device-authorization.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device-authorization.js","sourceRoot":"","sources":["../../src/auth/device-authorization.ts"],"names":[],"mappings":"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"}
package/lib/card.d.ts ADDED
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Lark CardKit 2.0 native streaming reply — the streaming reply
3
+ * (`ReplyHandle`).
4
+ *
5
+ * Lark streaming rides the OFFICIAL CardKit native typewriter flow — never the
6
+ * legacy `message.patch` pseudo-stream:
7
+ *
8
+ * 1. create a Card JSON 2.0 card entity with `streaming_mode: true`
9
+ * (`cardkit.v1.card.create`);
10
+ * 2. send it as an interactive card reference
11
+ * (`im.v1.message.create` with `{ type: 'card', data: { card_id } }`);
12
+ * 3. for every streamed delta, replace the markdown element's full content
13
+ * via `cardkit.v1.cardElement.content` with a monotonically increasing
14
+ * `sequence` and a stable `uuid` (idempotency);
15
+ * 4. `finish()`/`fail()` close the stream by setting `streaming_mode: false`
16
+ * through `cardkit.v1.card.settings`, writing the final summary.
17
+ *
18
+ * Rollover: when a single markdown element approaches the platform's
19
+ * ~30,000-char element cap (the official SDK's `Channel` guard), the handle
20
+ * finalizes the current card, creates + sends a fresh streaming card, and
21
+ * continues on it. `chunkIds` records every rollover card's message id.
22
+ *
23
+ * The handle does NOT throttle internally — ReplyRouter owns throttling via
24
+ * config `reply.updateIntervalMs`. It guards no-op updates (skip when the text
25
+ * is unchanged) and serializes card operations so concurrent router flushes
26
+ * cannot interleave. State is observable for tests: `status`, `cardId`,
27
+ * `text`, `error`, `sequence` and an `updates` record of applied operations.
28
+ */
29
+ import type { ChannelLogger, ChannelTarget, OutboundMessage, ReplyHandle } from '@krischoichoi/channel-core';
30
+ import type { LarkUpstream } from './upstream.js';
31
+ export type LarkCardStatus = 'idle' | 'active' | 'finished' | 'failed';
32
+ /** One applied card operation (observable test surface). */
33
+ export interface LarkCardUpdate {
34
+ kind: 'created' | 'streamed' | 'rollover' | 'finished' | 'failed';
35
+ /** Card text at the time of the operation. */
36
+ text?: string;
37
+ /** Monotonic element sequence after the operation. */
38
+ sequence?: number;
39
+ /** Previous card entity id that a rollover finalized. */
40
+ cardId?: string;
41
+ /** Clock timestamp (injectable). */
42
+ at: number;
43
+ /** Error detail when the operation itself failed. */
44
+ error?: string;
45
+ }
46
+ export interface LarkCardReplyOptions {
47
+ /** The upstream outbound driver (official OpenAPI or an injected fake). */
48
+ upstream: LarkUpstream;
49
+ /** Conversation the card is attached to. */
50
+ target: ChannelTarget;
51
+ logger: ChannelLogger;
52
+ /** Create the card on the first delta; otherwise only at `finish`. */
53
+ createOnFirstDelta: boolean;
54
+ /** Injectable clock (tests). */
55
+ now?: () => number;
56
+ }
57
+ /**
58
+ * Hard cap for ONE markdown element before rolling over into a fresh card.
59
+ * Feishu rejects oversized elements (error 230099); the official SDK's
60
+ * high-level Channel uses the same 30,000-char default threshold.
61
+ */
62
+ export declare const LARK_STREAM_ELEMENT_MAX_CHARS = 30000;
63
+ export declare class LarkCardReply implements ReplyHandle {
64
+ private readonly options;
65
+ status: LarkCardStatus;
66
+ /** Current (latest) CardKit card entity; undefined until first create. */
67
+ cardId?: string;
68
+ /** Message id of the current card reference (changes on rollover). */
69
+ messageId?: string;
70
+ text: string;
71
+ /** Monotonic per-element sequence (restarts on each rollover card). */
72
+ sequence: number;
73
+ /** Every card sent during this reply, in order (the first is the primary one). */
74
+ readonly chunkIds: string[];
75
+ /** Original failure value passed to `fail()`, if any. */
76
+ error?: unknown;
77
+ /** Ordered record of applied card operations. */
78
+ readonly updates: LarkCardUpdate[];
79
+ private readonly now;
80
+ private queue;
81
+ /** Text currently owned by the active card; finalized card heads are excluded. */
82
+ private content;
83
+ private hasRolledOver;
84
+ /** A previous failure was already written to the card — do not overwrite it. */
85
+ private failedShown;
86
+ constructor(options: LarkCardReplyOptions);
87
+ append(delta: string): Promise<void>;
88
+ replace(message: OutboundMessage): Promise<void>;
89
+ finish(message?: OutboundMessage): Promise<void>;
90
+ fail(error: unknown): Promise<void>;
91
+ private get finalized();
92
+ private push;
93
+ /** Create a CardKit entity and send its card reference (one-shot per card). */
94
+ private createCardAndSend;
95
+ /**
96
+ * Push the full element content through `cardElement.content`, rolling over
97
+ * into a fresh card when the element exceeds the platform cap. Sequence is
98
+ * monotonically increased and a stable uuid is derived from cardId +
99
+ * sequence (idempotent on retry).
100
+ */
101
+ private pushContent;
102
+ /**
103
+ * Finalize the current card with the head content and start a fresh
104
+ * streaming card for the tail. Mirrors the official SDK Channel rollover:
105
+ * the current element is pinned to the head, streaming is closed on it, and
106
+ * a new streaming entity is created + sent seeded with the tail.
107
+ */
108
+ private rollover;
109
+ /** Close the stream: `streaming_mode: false` + final summary. */
110
+ private closeStreaming;
111
+ private record;
112
+ private enqueue;
113
+ private replaceText;
114
+ }
115
+ /** Shorten markdown to a single-line preview for the card summary. */
116
+ export declare function truncateSummary(text: string, max?: number): string;
117
+ /**
118
+ * Split long content at ~`limit` chars while keeping fenced code blocks whole
119
+ * (mirrors the official SDK Channel splitter). A single line that alone
120
+ * exceeds `limit` (an unbreakable token) is hard-wrapped so the caller can
121
+ * always roll over instead of being stuck with an oversized element.
122
+ */
123
+ export declare function splitForRollover(text: string, limit: number): string[];
124
+ //# sourceMappingURL=card.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"card.d.ts","sourceRoot":"","sources":["../src/card.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,KAAK,EACV,aAAa,EACb,aAAa,EACb,eAAe,EACf,WAAW,EACZ,MAAM,4BAA4B,CAAC;AAMpC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,QAAQ,GAAG,UAAU,GAAG,QAAQ,CAAC;AAEvE,4DAA4D;AAC5D,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,UAAU,GAAG,QAAQ,CAAC;IAClE,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yDAAyD;IACzD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,2EAA2E;IAC3E,QAAQ,EAAE,YAAY,CAAC;IACvB,4CAA4C;IAC5C,MAAM,EAAE,aAAa,CAAC;IACtB,MAAM,EAAE,aAAa,CAAC;IACtB,sEAAsE;IACtE,kBAAkB,EAAE,OAAO,CAAC;IAC5B,gCAAgC;IAChC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,QAAS,CAAC;AAKpD,qBAAa,aAAc,YAAW,WAAW;IAwBnC,OAAO,CAAC,QAAQ,CAAC,OAAO;IAvBpC,MAAM,EAAE,cAAc,CAAU;IAChC,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,SAAM;IACV,uEAAuE;IACvE,QAAQ,SAAK;IACb,kFAAkF;IAClF,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAM;IACjC,yDAAyD;IACzD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,iDAAiD;IACjD,QAAQ,CAAC,OAAO,EAAE,cAAc,EAAE,CAAM;IAExC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,KAAK,CAAoC;IACjD,kFAAkF;IAClF,OAAO,CAAC,OAAO,CAAM;IACrB,OAAO,CAAC,aAAa,CAAS;IAC9B,gFAAgF;IAChF,OAAO,CAAC,WAAW,CAAS;gBAEC,OAAO,EAAE,oBAAoB;IAI1D,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAapC,OAAO,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAWhD,MAAM,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IA8BhD,IAAI,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAgCnC,OAAO,KAAK,SAAS,GAEpB;YAEa,IAAI;IAUlB,+EAA+E;YACjE,iBAAiB;IAY/B;;;;;OAKG;YACW,WAAW;IAiBzB;;;;;OAKG;YACW,QAAQ;IA6CtB,iEAAiE;YACnD,cAAc;IAU5B,OAAO,CAAC,MAAM;IAkBd,OAAO,CAAC,OAAO;IASf,OAAO,CAAC,WAAW;CAepB;AAOD,sEAAsE;AACtE,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,SAAK,GAAG,MAAM,CAI9D;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAgDtE"}
package/lib/card.js ADDED
@@ -0,0 +1,347 @@
1
+ import { ChannelError } from '@krischoichoi/channel-core';
2
+ import { STREAM_MARKDOWN_ELEMENT_ID, streamingCardJson, } from './openapi-outbound.js';
3
+ /**
4
+ * Hard cap for ONE markdown element before rolling over into a fresh card.
5
+ * Feishu rejects oversized elements (error 230099); the official SDK's
6
+ * high-level Channel uses the same 30,000-char default threshold.
7
+ */
8
+ export const LARK_STREAM_ELEMENT_MAX_CHARS = 30_000;
9
+ /** Stable summary shown in chat previews while the card is still streaming. */
10
+ const GENERATING_SUMMARY = '[Generating...]';
11
+ export class LarkCardReply {
12
+ options;
13
+ status = 'idle';
14
+ /** Current (latest) CardKit card entity; undefined until first create. */
15
+ cardId;
16
+ /** Message id of the current card reference (changes on rollover). */
17
+ messageId;
18
+ text = '';
19
+ /** Monotonic per-element sequence (restarts on each rollover card). */
20
+ sequence = 0;
21
+ /** Every card sent during this reply, in order (the first is the primary one). */
22
+ chunkIds = [];
23
+ /** Original failure value passed to `fail()`, if any. */
24
+ error;
25
+ /** Ordered record of applied card operations. */
26
+ updates = [];
27
+ now;
28
+ queue = Promise.resolve();
29
+ /** Text currently owned by the active card; finalized card heads are excluded. */
30
+ content = '';
31
+ hasRolledOver = false;
32
+ /** A previous failure was already written to the card — do not overwrite it. */
33
+ failedShown = false;
34
+ constructor(options) {
35
+ this.options = options;
36
+ this.now = options.now ?? Date.now;
37
+ }
38
+ append(delta) {
39
+ return this.enqueue(async () => {
40
+ if (this.finalized)
41
+ return;
42
+ if (!delta)
43
+ return;
44
+ const merged = mergeStreamingText(this.text, delta);
45
+ const appended = merged.slice(this.text.length);
46
+ if (!appended)
47
+ return;
48
+ this.text = merged;
49
+ this.content += appended;
50
+ await this.push();
51
+ });
52
+ }
53
+ replace(message) {
54
+ return this.enqueue(async () => {
55
+ if (this.finalized)
56
+ return;
57
+ if (message.text === undefined)
58
+ return;
59
+ // No-op guard: skip the network round-trip when nothing changed.
60
+ if (message.text === this.text)
61
+ return;
62
+ this.replaceText(message.text);
63
+ await this.push();
64
+ });
65
+ }
66
+ finish(message) {
67
+ return this.enqueue(async () => {
68
+ if (this.finalized)
69
+ return;
70
+ const finalText = message?.text;
71
+ let needsSync = false;
72
+ if (finalText !== undefined && finalText !== this.text) {
73
+ this.replaceText(finalText);
74
+ needsSync = true;
75
+ }
76
+ if (!this.cardId) {
77
+ // Nothing was streamed eagerly: create the final card now and write
78
+ // the accumulated content into its markdown element (or do nothing
79
+ // when there is no content to show).
80
+ if (this.text) {
81
+ await this.createCardAndSend();
82
+ await this.pushContent(this.content);
83
+ }
84
+ }
85
+ else if (needsSync) {
86
+ await this.pushContent(this.content);
87
+ }
88
+ if (this.cardId) {
89
+ await this.closeStreaming(this.content);
90
+ this.record('finished', this.text);
91
+ }
92
+ else {
93
+ this.record('finished', undefined);
94
+ }
95
+ this.status = 'finished';
96
+ });
97
+ }
98
+ fail(error) {
99
+ return this.enqueue(async () => {
100
+ if (this.finalized)
101
+ return;
102
+ this.error = error;
103
+ if (this.cardId && !this.failedShown) {
104
+ const failureContent = this.content ? `${this.content}\n\n— _(Generation interrupted)_` : '— _(Generation interrupted)_';
105
+ let updateError;
106
+ try {
107
+ // Write the failure into the card content first, then close the
108
+ // stream. A secondary failure must NOT overwrite the original error.
109
+ this.failedShown = true;
110
+ await this.pushContent(failureContent);
111
+ }
112
+ catch (secondary) {
113
+ updateError = secondary;
114
+ }
115
+ try {
116
+ await this.closeStreaming(failureContent);
117
+ }
118
+ catch (secondary) {
119
+ updateError ??= secondary;
120
+ }
121
+ if (updateError) {
122
+ // Marking the card failed must not mask the original error.
123
+ this.options.logger.error('[channel-lark] failed to mark card failed', updateError);
124
+ this.record('failed', this.text, updateError);
125
+ }
126
+ else
127
+ this.record('failed', this.text);
128
+ }
129
+ else {
130
+ this.record('failed', this.text);
131
+ }
132
+ this.status = 'failed';
133
+ });
134
+ }
135
+ get finalized() {
136
+ return this.status === 'finished' || this.status === 'failed';
137
+ }
138
+ async push() {
139
+ if (this.cardId) {
140
+ await this.pushContent(this.content);
141
+ return;
142
+ }
143
+ if (!this.options.createOnFirstDelta)
144
+ return; // buffer until finish
145
+ await this.createCardAndSend();
146
+ await this.pushContent(this.content);
147
+ }
148
+ /** Create a CardKit entity and send its card reference (one-shot per card). */
149
+ async createCardAndSend() {
150
+ const cardJson = streamingCardJson('...', GENERATING_SUMMARY);
151
+ const entity = await this.options.upstream.createCardEntity(cardJson);
152
+ const sent = await this.options.upstream.sendCardEntity(this.options.target.conversationId, entity.cardId);
153
+ this.cardId = entity.cardId;
154
+ this.messageId = sent.messageId;
155
+ this.sequence = 0;
156
+ this.chunkIds.push(sent.messageId);
157
+ this.status = 'active';
158
+ this.record('created', this.text);
159
+ }
160
+ /**
161
+ * Push the full element content through `cardElement.content`, rolling over
162
+ * into a fresh card when the element exceeds the platform cap. Sequence is
163
+ * monotonically increased and a stable uuid is derived from cardId +
164
+ * sequence (idempotent on retry).
165
+ */
166
+ async pushContent(content) {
167
+ if (!this.cardId)
168
+ return;
169
+ if (content.length > LARK_STREAM_ELEMENT_MAX_CHARS) {
170
+ await this.rollover(content);
171
+ return;
172
+ }
173
+ this.sequence += 1;
174
+ await this.options.upstream.updateCardElementContent(this.cardId, STREAM_MARKDOWN_ELEMENT_ID, content || ' ', this.sequence, uuidFor(this.cardId, this.sequence));
175
+ this.record('streamed', content, undefined, this.sequence);
176
+ }
177
+ /**
178
+ * Finalize the current card with the head content and start a fresh
179
+ * streaming card for the tail. Mirrors the official SDK Channel rollover:
180
+ * the current element is pinned to the head, streaming is closed on it, and
181
+ * a new streaming entity is created + sent seeded with the tail.
182
+ */
183
+ async rollover(content) {
184
+ if (!this.cardId)
185
+ return;
186
+ // A rollover cannot produce a single splittable piece below the cap.
187
+ const chunks = splitForRollover(content, LARK_STREAM_ELEMENT_MAX_CHARS);
188
+ if (chunks.length < 2)
189
+ throw new Error('rollover: content not splittable below limit');
190
+ const head = chunks[0];
191
+ const joinedWithNewlines = chunks.slice(1).join('\n');
192
+ const joinedWithoutSeparators = chunks.slice(1).join('');
193
+ // Keep the splitter's original character sequence intact. Line-oriented
194
+ // chunks need their delimiters; hard-wrapped tokens must not gain '\n'.
195
+ const tail = joinedWithNewlines.length + head.length === content.length
196
+ ? joinedWithNewlines
197
+ : joinedWithoutSeparators;
198
+ // 1. Pin the current element to the head.
199
+ this.sequence += 1;
200
+ await this.options.upstream.updateCardElementContent(this.cardId, STREAM_MARKDOWN_ELEMENT_ID, head, this.sequence, uuidFor(this.cardId, this.sequence));
201
+ // 2. Close streaming on the old card (best-effort).
202
+ try {
203
+ await this.closeStreaming(head);
204
+ }
205
+ catch {
206
+ // Feishu auto-closes an idle stream; a transient failure is recoverable.
207
+ }
208
+ this.hasRolledOver = true;
209
+ // 3. Create + send the fresh streaming card seeded with the tail.
210
+ const previousCardId = this.cardId;
211
+ const cardJson = streamingCardJson(tail || '...', GENERATING_SUMMARY);
212
+ const entity = await this.options.upstream.createCardEntity(cardJson);
213
+ const sent = await this.options.upstream.sendCardEntity(this.options.target.conversationId, entity.cardId);
214
+ this.cardId = entity.cardId;
215
+ this.messageId = sent.messageId;
216
+ this.sequence = 0;
217
+ this.content = tail;
218
+ this.chunkIds.push(sent.messageId);
219
+ this.record('rollover', tail, undefined, 0, previousCardId);
220
+ // Continue streaming the tail immediately.
221
+ await this.pushContent(tail);
222
+ }
223
+ /** Close the stream: `streaming_mode: false` + final summary. */
224
+ async closeStreaming(summaryText) {
225
+ if (!this.cardId)
226
+ return;
227
+ this.sequence += 1;
228
+ await this.options.upstream.finishStreamingCard(this.cardId, this.sequence, truncateSummary(summaryText));
229
+ }
230
+ record(kind, text, error, sequence, cardId) {
231
+ const update = {
232
+ kind,
233
+ text,
234
+ at: this.now(),
235
+ };
236
+ if (sequence !== undefined)
237
+ update.sequence = sequence;
238
+ if (cardId !== undefined)
239
+ update.cardId = cardId;
240
+ if (error !== undefined)
241
+ update.error = String(error);
242
+ this.updates.push(update);
243
+ }
244
+ enqueue(task) {
245
+ const next = this.queue.then(task, task);
246
+ this.queue = next.then(() => undefined, () => undefined);
247
+ return next;
248
+ }
249
+ replaceText(next) {
250
+ if (!this.hasRolledOver) {
251
+ this.text = next;
252
+ this.content = next;
253
+ return;
254
+ }
255
+ if (!next.startsWith(this.text)) {
256
+ throw new ChannelError('CHANNEL_ERROR', 'lark streaming replacement cannot rewrite content already finalized by card rollover');
257
+ }
258
+ this.content += next.slice(this.text.length);
259
+ this.text = next;
260
+ }
261
+ }
262
+ /** Stable request uuid derived from the card entity id + sequence. */
263
+ function uuidFor(cardId, sequence) {
264
+ return `c_${cardId}_${sequence}`;
265
+ }
266
+ /** Shorten markdown to a single-line preview for the card summary. */
267
+ export function truncateSummary(text, max = 50) {
268
+ if (!text)
269
+ return '';
270
+ const cleaned = text.replace(/\s+/g, ' ').trim();
271
+ return cleaned.length <= max ? cleaned : `${cleaned.slice(0, max - 1)}…`;
272
+ }
273
+ /**
274
+ * Split long content at ~`limit` chars while keeping fenced code blocks whole
275
+ * (mirrors the official SDK Channel splitter). A single line that alone
276
+ * exceeds `limit` (an unbreakable token) is hard-wrapped so the caller can
277
+ * always roll over instead of being stuck with an oversized element.
278
+ */
279
+ export function splitForRollover(text, limit) {
280
+ if (text.length <= limit)
281
+ return [text];
282
+ const lines = text.split('\n');
283
+ const out = [];
284
+ let buf = [];
285
+ let bufLen = 0;
286
+ let fenceLang = null;
287
+ const flush = () => {
288
+ if (buf.length === 0)
289
+ return;
290
+ let chunk = buf.join('\n');
291
+ if (fenceLang !== null)
292
+ chunk += '\n```';
293
+ out.push(chunk);
294
+ buf = [];
295
+ bufLen = 0;
296
+ if (fenceLang !== null) {
297
+ // Reopen the fence in the next chunk.
298
+ buf.push(`\`\`\`${fenceLang}`);
299
+ bufLen = buf[0].length;
300
+ }
301
+ };
302
+ for (const line of lines) {
303
+ const match = line.match(/^```(\w*)$/);
304
+ const lineLen = line.length + (buf.length > 0 ? 1 : 0);
305
+ const isHeading = /^#{1,6}\s/.test(line);
306
+ const nearFull = bufLen > limit * 0.75;
307
+ if (bufLen + lineLen > limit || (isHeading && nearFull && buf.length > 0)) {
308
+ flush();
309
+ }
310
+ buf.push(line);
311
+ bufLen += lineLen;
312
+ if (match) {
313
+ fenceLang = fenceLang === null ? (match[1] ?? '') : null;
314
+ }
315
+ }
316
+ flush();
317
+ // Hard-wrap any remaining oversized chunk (a single unbreakable line/token
318
+ // cannot be split at a line boundary — never leave it above the cap).
319
+ const final = [];
320
+ for (const chunk of out) {
321
+ if (chunk.length <= limit) {
322
+ final.push(chunk);
323
+ continue;
324
+ }
325
+ for (let i = 0; i < chunk.length; i += limit) {
326
+ final.push(chunk.slice(i, i + limit));
327
+ }
328
+ }
329
+ return final.length > 0 ? final : [text];
330
+ }
331
+ function mergeStreamingText(previous, next) {
332
+ if (!previous)
333
+ return next;
334
+ if (!next)
335
+ return previous;
336
+ if (next.startsWith(previous))
337
+ return next;
338
+ if (previous.startsWith(next))
339
+ return previous;
340
+ const maxOverlap = Math.min(previous.length, next.length);
341
+ for (let length = maxOverlap; length > 0; length -= 1) {
342
+ if (previous.endsWith(next.slice(0, length)))
343
+ return previous + next.slice(length);
344
+ }
345
+ return previous + next;
346
+ }
347
+ //# sourceMappingURL=card.js.map