@excitedjs/feishu-transport 0.0.1
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 +39 -0
- package/dist/contract/access-store.d.ts +23 -0
- package/dist/contract/access-store.d.ts.map +1 -0
- package/dist/contract/access-store.js +16 -0
- package/dist/contract/access-store.js.map +1 -0
- package/dist/contract/outbound.d.ts +39 -0
- package/dist/contract/outbound.d.ts.map +1 -0
- package/dist/contract/outbound.js +16 -0
- package/dist/contract/outbound.js.map +1 -0
- package/dist/contract/types.d.ts +86 -0
- package/dist/contract/types.d.ts.map +1 -0
- package/dist/contract/types.js +10 -0
- package/dist/contract/types.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/json.d.ts +10 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +14 -0
- package/dist/json.js.map +1 -0
- package/dist/parse/comment.d.ts +41 -0
- package/dist/parse/comment.d.ts.map +1 -0
- package/dist/parse/comment.js +51 -0
- package/dist/parse/comment.js.map +1 -0
- package/dist/parse/content.d.ts +42 -0
- package/dist/parse/content.d.ts.map +1 -0
- package/dist/parse/content.js +208 -0
- package/dist/parse/content.js.map +1 -0
- package/dist/policy/gate.d.ts +105 -0
- package/dist/policy/gate.d.ts.map +1 -0
- package/dist/policy/gate.js +276 -0
- package/dist/policy/gate.js.map +1 -0
- package/dist/policy/pairing.d.ts +12 -0
- package/dist/policy/pairing.d.ts.map +1 -0
- package/dist/policy/pairing.js +15 -0
- package/dist/policy/pairing.js.map +1 -0
- package/dist/render/render.d.ts +186 -0
- package/dist/render/render.d.ts.map +1 -0
- package/dist/render/render.js +630 -0
- package/dist/render/render.js.map +1 -0
- package/dist/transport/connection.d.ts +25 -0
- package/dist/transport/connection.d.ts.map +1 -0
- package/dist/transport/connection.js +42 -0
- package/dist/transport/connection.js.map +1 -0
- package/dist/transport/feishu.d.ts +222 -0
- package/dist/transport/feishu.d.ts.map +1 -0
- package/dist/transport/feishu.js +431 -0
- package/dist/transport/feishu.js.map +1 -0
- package/package.json +39 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render a Markdown source into one or more Feishu v2 interactive cards.
|
|
3
|
+
*
|
|
4
|
+
* Feishu's `tag: markdown` body element accepts only the "lark_md" subset —
|
|
5
|
+
* bold, italic, strikethrough, inline code, fenced code, links, and (on
|
|
6
|
+
* Feishu 7.6+) lists. It does NOT support headings or GFM tables: a `#` is
|
|
7
|
+
* shown as a literal `#`, and `| a | b |` is shown as the source pipes.
|
|
8
|
+
* Routing every block through `tag: markdown` therefore leaks raw markup into
|
|
9
|
+
* the rendered card whenever the body has a heading or a table.
|
|
10
|
+
*
|
|
11
|
+
* This module parses the source into block tokens (via `marked.lexer`) and
|
|
12
|
+
* routes each block to the v2 card component that actually renders it:
|
|
13
|
+
*
|
|
14
|
+
* - The first `# h1` becomes the card's `header.title` plain-text slot.
|
|
15
|
+
* A later `# h1`, and every `##`/`###`/... heading, becomes a body
|
|
16
|
+
* element wrapping the flattened heading text in `**...**` so lark_md
|
|
17
|
+
* renders it as visible bold.
|
|
18
|
+
* - A GFM `| ... |` table becomes a dedicated `tag: table` element with
|
|
19
|
+
* the header alignment markers mapped to each column's
|
|
20
|
+
* `horizontal_align`. Cells that contain inline markup flip the
|
|
21
|
+
* column's `data_type` from `text` to `lark_md`. A cell over
|
|
22
|
+
* `CELL_MAX_BYTES` is a render-time error — splitting one row's cell
|
|
23
|
+
* across multiple rows would break alignment, and silently truncating
|
|
24
|
+
* hides the data the caller asked to deliver.
|
|
25
|
+
* - A horizontal rule (`---`) becomes a `tag: hr` element.
|
|
26
|
+
* - Every other block — paragraphs, lists, blockquotes, fenced code —
|
|
27
|
+
* is emitted as a `tag: markdown` element with the block's raw source,
|
|
28
|
+
* since lark_md renders those natively.
|
|
29
|
+
*
|
|
30
|
+
* Elements are then packed greedily into cards by a dual budget: Feishu's
|
|
31
|
+
* ~30 KB request body cap AND the v2 card per-card element-count cap.
|
|
32
|
+
* Only the first card carries the header — splitting the body produces
|
|
33
|
+
* follow-up cards with no header, the way a long thread reads. A markdown
|
|
34
|
+
* element body too large for one card is split with `splitMarkdownByBytes`,
|
|
35
|
+
* which counts UTF-8 bytes (so a CJK-heavy body is not budgeted as if it
|
|
36
|
+
* were ASCII) and respects grapheme cluster boundaries (so a ZWJ emoji
|
|
37
|
+
* does not get cut in half).
|
|
38
|
+
*
|
|
39
|
+
* `renderMarkdownToCards` validates every produced card against both
|
|
40
|
+
* budgets before returning; if any card would still exceed them despite
|
|
41
|
+
* splitting, it throws. Callers therefore know that the array they get
|
|
42
|
+
* back is wire-ready — there is no path where the renderer hands the
|
|
43
|
+
* sender a card Feishu would reject. This is the structural half of the
|
|
44
|
+
* "atomic" semantic the channel offers; the network half (every send
|
|
45
|
+
* succeeds or all sends fail) cannot be guaranteed on Feishu's IM API,
|
|
46
|
+
* which has no message-batch transaction.
|
|
47
|
+
*
|
|
48
|
+
* The result is `RenderedCard[]`. `cardToContent(card)` turns one card into
|
|
49
|
+
* the JSON string the `im.message.create` `content` field expects.
|
|
50
|
+
*
|
|
51
|
+
* The 9-row × 9-column limit in `markdown-ref.md` belongs to a different
|
|
52
|
+
* surface (the docs-create pipeline that turns markdown into Feishu document
|
|
53
|
+
* blocks); v2 card tables accept up to 50 columns, paginate rows in-card via
|
|
54
|
+
* `page_size`, and have no per-table row cap of their own. This renderer
|
|
55
|
+
* therefore column-splits at 50 (with the first column preserved on each
|
|
56
|
+
* split half as the identifier) and never row-splits when row-splitting
|
|
57
|
+
* would not actually reduce a card's size.
|
|
58
|
+
*/
|
|
59
|
+
/** A v2 card's header field — only the plain-text title slot is populated. */
|
|
60
|
+
interface CardHeader {
|
|
61
|
+
title: {
|
|
62
|
+
tag: 'plain_text';
|
|
63
|
+
content: string;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Body element rendering a chunk of lark_md text. */
|
|
67
|
+
interface MarkdownElement {
|
|
68
|
+
tag: 'markdown';
|
|
69
|
+
content: string;
|
|
70
|
+
}
|
|
71
|
+
/** Body element rendering a horizontal rule. */
|
|
72
|
+
interface HrElement {
|
|
73
|
+
tag: 'hr';
|
|
74
|
+
}
|
|
75
|
+
/** Per-cell horizontal alignment, matched to GFM's `:---`, `:---:`, `---:`. */
|
|
76
|
+
type CellAlign = 'left' | 'center' | 'right';
|
|
77
|
+
/** One column of a `tag: table` element. */
|
|
78
|
+
interface TableColumn {
|
|
79
|
+
/** Key the row records use to look up the cell value. */
|
|
80
|
+
name: string;
|
|
81
|
+
/** Header label shown to the reader. */
|
|
82
|
+
display_name: string;
|
|
83
|
+
/**
|
|
84
|
+
* `text` for plain cells; `lark_md` when any cell in this column carries
|
|
85
|
+
* inline markup. Per the API, `data_type` is column-scoped, not cell-scoped.
|
|
86
|
+
*/
|
|
87
|
+
data_type: 'text' | 'lark_md';
|
|
88
|
+
/** Cell alignment, derived from the GFM header separator row. */
|
|
89
|
+
horizontal_align?: CellAlign;
|
|
90
|
+
}
|
|
91
|
+
/** Body element rendering a GFM table. */
|
|
92
|
+
interface TableElement {
|
|
93
|
+
tag: 'table';
|
|
94
|
+
page_size: number;
|
|
95
|
+
row_height: 'low';
|
|
96
|
+
header_style: {
|
|
97
|
+
bold: true;
|
|
98
|
+
background_style: 'grey';
|
|
99
|
+
};
|
|
100
|
+
columns: TableColumn[];
|
|
101
|
+
rows: Array<Record<string, string>>;
|
|
102
|
+
}
|
|
103
|
+
/** Any body element this renderer emits. */
|
|
104
|
+
type CardElement = MarkdownElement | HrElement | TableElement;
|
|
105
|
+
/** One v2 interactive card, ready to JSON-serialise into `content`. */
|
|
106
|
+
export interface RenderedCard {
|
|
107
|
+
schema: '2.0';
|
|
108
|
+
config: {
|
|
109
|
+
update_multi: true;
|
|
110
|
+
};
|
|
111
|
+
header?: CardHeader;
|
|
112
|
+
body: {
|
|
113
|
+
elements: CardElement[];
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Feishu's documented hard limit for a card request body. Past this the API
|
|
118
|
+
* rejects the call outright; the packer keeps each card's serialised content
|
|
119
|
+
* below `CARD_CONTENT_SAFE_BYTES` so HTTP headers and the request envelope
|
|
120
|
+
* still fit underneath the hard cap.
|
|
121
|
+
*/
|
|
122
|
+
export declare const FEISHU_CARD_REQUEST_LIMIT_BYTES: number;
|
|
123
|
+
/**
|
|
124
|
+
* v2 card per-card element-count cap. Observed in PR #73 review: a card with
|
|
125
|
+
* 250 short markdown elements is rejected by Feishu, though the JSON itself
|
|
126
|
+
* is well under the byte cap. The exact upper bound is not in the open
|
|
127
|
+
* docs; the reviewer-cited 200 figure is treated as the hard limit and a
|
|
128
|
+
* lower number is used as the safe budget so a card on the boundary does
|
|
129
|
+
* not silently fail under server-side counting differences.
|
|
130
|
+
*/
|
|
131
|
+
export declare const FEISHU_CARD_ELEMENT_HARD_CAP = 200;
|
|
132
|
+
/**
|
|
133
|
+
* Per-cell byte cap. A cell larger than this is rejected at render time:
|
|
134
|
+
* splitting one row's cell across multiple rows breaks alignment with the
|
|
135
|
+
* other columns, and silently truncating drops the data the caller asked
|
|
136
|
+
* to deliver. The author is expected to move the oversized content into a
|
|
137
|
+
* paragraph or fenced code block, where the byte-aware splitter handles
|
|
138
|
+
* arbitrary sizes.
|
|
139
|
+
*
|
|
140
|
+
* 4 KB is comfortably above any reasonable cell value (a long sentence is
|
|
141
|
+
* ~200 bytes) while small enough that a 50-column table of full-budget
|
|
142
|
+
* cells still leaves room for the card envelope inside the byte cap.
|
|
143
|
+
*/
|
|
144
|
+
export declare const CELL_MAX_BYTES: number;
|
|
145
|
+
/**
|
|
146
|
+
* Render a Markdown source into one or more v2 cards.
|
|
147
|
+
*
|
|
148
|
+
* The output is always non-empty: an empty source produces one card with a
|
|
149
|
+
* single empty `tag: markdown` element, so the caller always has something
|
|
150
|
+
* to send. Splitting happens automatically when the serialised card would
|
|
151
|
+
* exceed Feishu's 30 KB request cap, when the body would exceed the
|
|
152
|
+
* 200-element cap, or when a table has more than 50 columns.
|
|
153
|
+
*
|
|
154
|
+
* Throws when a single block cannot be made to fit even after splitting —
|
|
155
|
+
* for example a table whose row, after every cell has been validated under
|
|
156
|
+
* `CELL_MAX_BYTES`, still serialises above the per-card byte cap. The
|
|
157
|
+
* caller therefore sees a render-time error before any send is attempted,
|
|
158
|
+
* instead of the channel posting some cards and then failing on a later
|
|
159
|
+
* one (partial visible state).
|
|
160
|
+
*/
|
|
161
|
+
export declare function renderMarkdownToCards(text: string): RenderedCard[];
|
|
162
|
+
/**
|
|
163
|
+
* Serialise one card into the JSON string Feishu's `im.message.create`
|
|
164
|
+
* `content` field expects. Pure: no side effects, no I/O.
|
|
165
|
+
*/
|
|
166
|
+
export declare function cardToContent(card: RenderedCard): string;
|
|
167
|
+
/** Byte length of `card`'s serialised content, in UTF-8. */
|
|
168
|
+
export declare function cardContentBytes(card: RenderedCard): number;
|
|
169
|
+
/**
|
|
170
|
+
* Split a Markdown block's source into pieces whose UTF-8 byte length each
|
|
171
|
+
* stays at or under `byteBudget`. The input is one block's `raw` field from
|
|
172
|
+
* `marked.lexer`, so the block kind drives the split strategy:
|
|
173
|
+
*
|
|
174
|
+
* - A fenced code block (opens with ``` or ~~~ and closes with the same
|
|
175
|
+
* run) is split by line and the open / close lines are repeated on
|
|
176
|
+
* every piece, so each piece is itself a well-formed fenced block.
|
|
177
|
+
* - Any other block is split at line boundaries; a single line longer
|
|
178
|
+
* than the budget is split by grapheme cluster, so a ZWJ-bound emoji
|
|
179
|
+
* cluster or a Hangul syllable is not cut in half.
|
|
180
|
+
*
|
|
181
|
+
* Every returned piece is non-empty and fits the budget — the caller can
|
|
182
|
+
* use them directly as `tag: markdown` element bodies.
|
|
183
|
+
*/
|
|
184
|
+
export declare function splitMarkdownByBytes(text: string, byteBudget: number): string[];
|
|
185
|
+
export {};
|
|
186
|
+
//# sourceMappingURL=render.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/render/render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AAIH,8EAA8E;AAC9E,UAAU,UAAU;IAClB,KAAK,EAAE;QAAE,GAAG,EAAE,YAAY,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAC9C;AAED,sDAAsD;AACtD,UAAU,eAAe;IACvB,GAAG,EAAE,UAAU,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,gDAAgD;AAChD,UAAU,SAAS;IACjB,GAAG,EAAE,IAAI,CAAA;CACV;AAED,+EAA+E;AAC/E,KAAK,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAA;AAE5C,4CAA4C;AAC5C,UAAU,WAAW;IACnB,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAA;IACZ,wCAAwC;IACxC,YAAY,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;IAC7B,iEAAiE;IACjE,gBAAgB,CAAC,EAAE,SAAS,CAAA;CAC7B;AAED,0CAA0C;AAC1C,UAAU,YAAY;IACpB,GAAG,EAAE,OAAO,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,KAAK,CAAA;IACjB,YAAY,EAAE;QACZ,IAAI,EAAE,IAAI,CAAA;QACV,gBAAgB,EAAE,MAAM,CAAA;KACzB,CAAA;IACD,OAAO,EAAE,WAAW,EAAE,CAAA;IACtB,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CACpC;AAED,4CAA4C;AAC5C,KAAK,WAAW,GAAG,eAAe,GAAG,SAAS,GAAG,YAAY,CAAA;AAE7D,uEAAuE;AACvE,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,KAAK,CAAA;IACb,MAAM,EAAE;QAAE,YAAY,EAAE,IAAI,CAAA;KAAE,CAAA;IAC9B,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,IAAI,EAAE;QAAE,QAAQ,EAAE,WAAW,EAAE,CAAA;KAAE,CAAA;CAClC;AAED;;;;;GAKG;AACH,eAAO,MAAM,+BAA+B,QAAY,CAAA;AAGxD;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,MAAM,CAAA;AAa/C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,cAAc,QAAW,CAAA;AA4BtC;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,EAAE,CA2BlE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CAExD;AAED,4DAA4D;AAC5D,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CAE3D;AA0WD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAc/E"}
|