@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 +1 -1
- package/src/bot.ts +9 -1
- package/src/config-schema.ts +5 -0
- package/src/feishu-renderer.ts +61 -2
package/package.json
CHANGED
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",
|
package/src/config-schema.ts
CHANGED
|
@@ -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) => {
|
package/src/feishu-renderer.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
387
|
+
return this.renderTextTokenWithStrongFallback((token as Tokens.Text).text);
|
|
329
388
|
|
|
330
389
|
case "strong": {
|
|
331
390
|
const strongToken = token as Tokens.Strong;
|