@max1874/feishu 0.2.27 → 0.2.28

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@max1874/feishu",
3
- "version": "0.2.27",
3
+ "version": "0.2.28",
4
4
  "type": "module",
5
5
  "description": "OpenClaw Feishu/Lark channel plugin",
6
6
  "license": "MIT",
package/src/bot.ts CHANGED
@@ -613,8 +613,16 @@ export async function handleFeishuMessage(params: {
613
613
  const feishuFrom = `feishu:${ctx.senderOpenId}`;
614
614
  const feishuTo = isGroup ? `chat:${ctx.chatId}` : `user:${ctx.senderOpenId}`;
615
615
 
616
+ // Default DM scope to per-channel-peer so each user gets their own session.
617
+ // The framework default ("main") would merge all DMs into one shared session.
618
+ const dmScope = feishuCfg?.dmScope ?? "per-channel-peer";
619
+ const routingCfg = cfg.session?.dmScope ? cfg : {
620
+ ...cfg,
621
+ session: { ...cfg.session, dmScope },
622
+ };
623
+
616
624
  const route = core.channel.routing.resolveAgentRoute({
617
- cfg,
625
+ cfg: routingCfg,
618
626
  channel: "feishu",
619
627
  peer: {
620
628
  kind: isGroup ? "group" : "dm",
@@ -33,6 +33,10 @@ const MarkdownConfigSchema = z
33
33
  // Message render mode: auto (default) = detect markdown, raw = plain text, card = always card
34
34
  const RenderModeSchema = z.enum(["auto", "raw", "card"]).optional();
35
35
 
36
+ // DM session scope: controls how session keys are computed for private chats.
37
+ // Framework default is "main" (all DMs share one session), which is rarely desired.
38
+ const DmScopeSchema = z.enum(["main", "per-peer", "per-channel-peer", "per-account-channel-peer"]).optional();
39
+
36
40
  // Reply-to (threading) mode: controls whether bot replies are sent as threaded replies.
37
41
  // - "all" (default): always reply in thread (current behavior)
38
42
  // - "off": reply in main chat; only thread if the triggering message was already in a thread
@@ -106,6 +110,7 @@ export const FeishuConfigSchema = z
106
110
  renderMode: RenderModeSchema, // raw = plain text (default), card = interactive card with markdown
107
111
  replyToMode: ReplyToModeSchema, // "all" = always thread (default), "off" = main chat unless already in thread
108
112
  replyToModeByChatType: ReplyToModeByChatTypeSchema, // per-chat-type overrides for replyToMode
113
+ dmScope: DmScopeSchema, // override session.dmScope for this channel (default: per-channel-peer)
109
114
  })
110
115
  .strict()
111
116
  .superRefine((value, ctx) => {
@@ -111,6 +111,63 @@ export class FeishuRenderer {
111
111
  return `block_${this.blockId}`;
112
112
  }
113
113
 
114
+ /**
115
+ * marked follows CommonMark emphasis delimiter rules. One sharp edge:
116
+ * sequences like `总花费**$122.64**` (no whitespace before `**` and the
117
+ * inner text starts with punctuation like `$` or `(`) are tokenized as
118
+ * plain text, so `**` would be rendered literally.
119
+ *
120
+ * For Feishu doc rendering we treat `**...**` inside text tokens as bold
121
+ * as a permissive fallback, which makes LLM-generated Markdown more robust.
122
+ */
123
+ private renderTextTokenWithStrongFallback(text: string): TextElement | TextElement[] {
124
+ if (!text.includes("**")) return { text_run: { content: text } };
125
+
126
+ const elements: TextElement[] = [];
127
+ let cursor = 0;
128
+
129
+ while (cursor < text.length) {
130
+ const openIdx = text.indexOf("**", cursor);
131
+ if (openIdx === -1) {
132
+ const tail = text.slice(cursor);
133
+ if (tail) elements.push({ text_run: { content: tail } });
134
+ break;
135
+ }
136
+
137
+ // Plain text before `**`
138
+ if (openIdx > cursor) {
139
+ const plain = text.slice(cursor, openIdx);
140
+ if (plain) elements.push({ text_run: { content: plain } });
141
+ }
142
+
143
+ const contentStart = openIdx + 2;
144
+ const closeIdx = text.indexOf("**", contentStart);
145
+ if (closeIdx === -1) {
146
+ // No matching close delimiter; render the rest literally.
147
+ const rest = text.slice(openIdx);
148
+ if (rest) elements.push({ text_run: { content: rest } });
149
+ break;
150
+ }
151
+
152
+ const boldContent = text.slice(contentStart, closeIdx);
153
+ if (boldContent) {
154
+ elements.push({
155
+ text_run: {
156
+ content: boldContent,
157
+ text_element_style: { bold: true },
158
+ },
159
+ });
160
+ } else {
161
+ // Degenerate `****` case; keep delimiters literal.
162
+ elements.push({ text_run: { content: "****" } });
163
+ }
164
+
165
+ cursor = closeIdx + 2;
166
+ }
167
+
168
+ return elements.length > 0 ? elements : { text_run: { content: text } };
169
+ }
170
+
114
171
  /**
115
172
  * Render markdown tokens to Feishu blocks.
116
173
  */
@@ -244,7 +301,9 @@ export class FeishuRenderer {
244
301
  if (textToken.tokens) {
245
302
  elements.push(...this.renderInlineTokens(textToken.tokens));
246
303
  } else {
247
- elements.push({ text_run: { content: textToken.raw } });
304
+ const fallback = this.renderTextTokenWithStrongFallback(textToken.raw);
305
+ if (Array.isArray(fallback)) elements.push(...fallback);
306
+ else elements.push(fallback);
248
307
  }
249
308
  } else if (child.type === "paragraph") {
250
309
  const paraToken = child as Tokens.Paragraph;
@@ -325,7 +384,7 @@ export class FeishuRenderer {
325
384
  private renderInline(token: Token): TextElement | TextElement[] | null {
326
385
  switch (token.type) {
327
386
  case "text":
328
- return { text_run: { content: (token as Tokens.Text).text } };
387
+ return this.renderTextTokenWithStrongFallback((token as Tokens.Text).text);
329
388
 
330
389
  case "strong": {
331
390
  const strongToken = token as Tokens.Strong;