@max1874/feishu 0.2.26 → 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.26",
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) => {
package/src/docx.ts CHANGED
@@ -868,6 +868,13 @@ const SetPermissionSchema = Type.Object({
868
868
  }),
869
869
  });
870
870
 
871
+ const TransferOwnerSchema = Type.Object({
872
+ doc_token: Type.String({ description: "Document token" }),
873
+ new_owner_open_id: Type.Optional(
874
+ Type.String({ description: "Open ID of the new owner. Omit to transfer to the current conversation sender." }),
875
+ ),
876
+ });
877
+
871
878
  // Wiki Schemas
872
879
  const WikiTokenSchema = Type.Object({
873
880
  wiki_token: Type.String({ description: "Wiki token (extract from URL /wiki/XXX)" }),
@@ -1355,5 +1362,37 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
1355
1362
  { name: "feishu_chat_messages" },
1356
1363
  );
1357
1364
 
1358
- api.logger.info?.(`feishu_doc: Registered 15 document/wiki/messaging tools`);
1365
+ // Tool 16: feishu_doc_transfer_owner
1366
+ api.registerTool(
1367
+ {
1368
+ name: "feishu_doc_transfer_owner",
1369
+ label: "Feishu Doc Transfer Owner",
1370
+ description:
1371
+ "Transfer ownership of a Feishu document to another user. " +
1372
+ "If new_owner_open_id is omitted, transfers to the current conversation sender.",
1373
+ parameters: TransferOwnerSchema,
1374
+ async execute(_toolCallId, params) {
1375
+ const { doc_token, new_owner_open_id } = params as {
1376
+ doc_token: string;
1377
+ new_owner_open_id?: string;
1378
+ };
1379
+ try {
1380
+ const openId = new_owner_open_id || getConversationContext()?.senderOpenId;
1381
+ if (!openId) {
1382
+ return json({
1383
+ error:
1384
+ "No new_owner_open_id provided and no current conversation context available.",
1385
+ });
1386
+ }
1387
+ const result = await transferOwner(getClient(), doc_token, openId);
1388
+ return json(result);
1389
+ } catch (err) {
1390
+ return json({ error: err instanceof Error ? err.message : String(err) });
1391
+ }
1392
+ },
1393
+ },
1394
+ { name: "feishu_doc_transfer_owner" },
1395
+ );
1396
+
1397
+ api.logger.info?.(`feishu_doc: Registered 16 document/wiki/messaging tools`);
1359
1398
  }
@@ -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;