@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.
Files changed (50) hide show
  1. package/README.md +39 -0
  2. package/dist/contract/access-store.d.ts +23 -0
  3. package/dist/contract/access-store.d.ts.map +1 -0
  4. package/dist/contract/access-store.js +16 -0
  5. package/dist/contract/access-store.js.map +1 -0
  6. package/dist/contract/outbound.d.ts +39 -0
  7. package/dist/contract/outbound.d.ts.map +1 -0
  8. package/dist/contract/outbound.js +16 -0
  9. package/dist/contract/outbound.js.map +1 -0
  10. package/dist/contract/types.d.ts +86 -0
  11. package/dist/contract/types.d.ts.map +1 -0
  12. package/dist/contract/types.js +10 -0
  13. package/dist/contract/types.js.map +1 -0
  14. package/dist/index.d.ts +29 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +31 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/json.d.ts +10 -0
  19. package/dist/json.d.ts.map +1 -0
  20. package/dist/json.js +14 -0
  21. package/dist/json.js.map +1 -0
  22. package/dist/parse/comment.d.ts +41 -0
  23. package/dist/parse/comment.d.ts.map +1 -0
  24. package/dist/parse/comment.js +51 -0
  25. package/dist/parse/comment.js.map +1 -0
  26. package/dist/parse/content.d.ts +42 -0
  27. package/dist/parse/content.d.ts.map +1 -0
  28. package/dist/parse/content.js +208 -0
  29. package/dist/parse/content.js.map +1 -0
  30. package/dist/policy/gate.d.ts +105 -0
  31. package/dist/policy/gate.d.ts.map +1 -0
  32. package/dist/policy/gate.js +276 -0
  33. package/dist/policy/gate.js.map +1 -0
  34. package/dist/policy/pairing.d.ts +12 -0
  35. package/dist/policy/pairing.d.ts.map +1 -0
  36. package/dist/policy/pairing.js +15 -0
  37. package/dist/policy/pairing.js.map +1 -0
  38. package/dist/render/render.d.ts +186 -0
  39. package/dist/render/render.d.ts.map +1 -0
  40. package/dist/render/render.js +630 -0
  41. package/dist/render/render.js.map +1 -0
  42. package/dist/transport/connection.d.ts +25 -0
  43. package/dist/transport/connection.d.ts.map +1 -0
  44. package/dist/transport/connection.js +42 -0
  45. package/dist/transport/connection.js.map +1 -0
  46. package/dist/transport/feishu.d.ts +222 -0
  47. package/dist/transport/feishu.d.ts.map +1 -0
  48. package/dist/transport/feishu.js +431 -0
  49. package/dist/transport/feishu.js.map +1 -0
  50. 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"}