@xopcai/xopc 0.0.15 → 0.0.16
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/dist/extensions/feishu/src/adapters/onboard-cli.d.ts +7 -0
- package/dist/extensions/feishu/src/adapters/onboard-cli.js +432 -0
- package/dist/extensions/feishu/src/adapters/onboard-cli.js.map +1 -0
- package/dist/extensions/feishu/src/auth/pairing.d.ts +7 -0
- package/dist/extensions/feishu/src/auth/pairing.js +45 -0
- package/dist/extensions/feishu/src/auth/pairing.js.map +1 -0
- package/dist/extensions/feishu/src/auth/paths.d.ts +2 -0
- package/dist/extensions/feishu/src/auth/paths.js +18 -0
- package/dist/extensions/feishu/src/auth/paths.js.map +1 -0
- package/dist/extensions/feishu/src/directory/directory-adapter.d.ts +2 -0
- package/dist/extensions/feishu/src/directory/directory-adapter.js +27 -0
- package/dist/extensions/feishu/src/directory/directory-adapter.js.map +1 -0
- package/dist/extensions/feishu/src/format.d.ts +21 -0
- package/dist/extensions/feishu/src/format.js +99 -0
- package/dist/extensions/feishu/src/format.js.map +1 -0
- package/dist/extensions/feishu/src/index.d.ts +5 -0
- package/dist/extensions/feishu/src/index.js +3 -0
- package/dist/extensions/feishu/src/outbound/actions.d.ts +51 -0
- package/dist/extensions/feishu/src/outbound/actions.js +62 -0
- package/dist/extensions/feishu/src/outbound/actions.js.map +1 -0
- package/dist/extensions/feishu/src/outbound/media-load.d.ts +12 -0
- package/dist/extensions/feishu/src/outbound/media-load.js +125 -0
- package/dist/extensions/feishu/src/outbound/media-load.js.map +1 -0
- package/dist/extensions/feishu/src/outbound/outbound-adapter.d.ts +2 -0
- package/dist/extensions/feishu/src/outbound/outbound-adapter.js +201 -0
- package/dist/extensions/feishu/src/outbound/outbound-adapter.js.map +1 -0
- package/dist/extensions/feishu/src/plugin.d.ts +70 -0
- package/dist/extensions/feishu/src/plugin.js +313 -0
- package/dist/extensions/feishu/src/plugin.js.map +1 -0
- package/dist/extensions/feishu/src/schema/config-schema.d.ts +215 -0
- package/dist/extensions/feishu/src/schema/config-schema.js +198 -0
- package/dist/extensions/feishu/src/schema/config-schema.js.map +1 -0
- package/dist/extensions/feishu/src/state/accounts.d.ts +38 -0
- package/dist/extensions/feishu/src/state/accounts.js +96 -0
- package/dist/extensions/feishu/src/state/accounts.js.map +1 -0
- package/dist/extensions/feishu/src/state/message-bindings.d.ts +11 -0
- package/dist/extensions/feishu/src/state/message-bindings.js +41 -0
- package/dist/extensions/feishu/src/state/message-bindings.js.map +1 -0
- package/dist/extensions/feishu/src/state/thread-bindings.js +46 -0
- package/dist/extensions/feishu/src/state/thread-bindings.js.map +1 -0
- package/dist/extensions/feishu/src/status/doctor.d.ts +2 -0
- package/dist/extensions/feishu/src/status/doctor.js +38 -0
- package/dist/extensions/feishu/src/status/doctor.js.map +1 -0
- package/dist/extensions/feishu/src/status/status-adapter.d.ts +3 -0
- package/dist/extensions/feishu/src/status/status-adapter.js +45 -0
- package/dist/extensions/feishu/src/status/status-adapter.js.map +1 -0
- package/dist/extensions/feishu/src/streaming/streaming-adapter.d.ts +3 -0
- package/dist/extensions/feishu/src/streaming/streaming-adapter.js +242 -0
- package/dist/extensions/feishu/src/streaming/streaming-adapter.js.map +1 -0
- package/dist/extensions/feishu/src/subagent-hooks.js +52 -0
- package/dist/extensions/feishu/src/subagent-hooks.js.map +1 -0
- package/dist/extensions/feishu/src/tools/docx/docx-batch-insert.js +95 -0
- package/dist/extensions/feishu/src/tools/docx/docx-batch-insert.js.map +1 -0
- package/dist/extensions/feishu/src/tools/docx/docx-color-text.js +75 -0
- package/dist/extensions/feishu/src/tools/docx/docx-color-text.js.map +1 -0
- package/dist/extensions/feishu/src/tools/docx/docx-table-ops.js +173 -0
- package/dist/extensions/feishu/src/tools/docx/docx-table-ops.js.map +1 -0
- package/dist/extensions/feishu/src/tools/docx/docx-types.js +1 -0
- package/dist/extensions/feishu/src/tools/tools.d.ts +5 -0
- package/dist/extensions/feishu/src/tools/tools.js +46 -0
- package/dist/extensions/feishu/src/tools/tools.js.map +1 -0
- package/dist/extensions/feishu/src/transport/client/client.d.ts +6 -0
- package/dist/extensions/feishu/src/transport/client/client.js +41 -0
- package/dist/extensions/feishu/src/transport/client/client.js.map +1 -0
- package/dist/extensions/feishu/src/transport/client/lark-sdk-logger.d.ts +13 -0
- package/dist/extensions/feishu/src/transport/client/lark-sdk-logger.js +104 -0
- package/dist/extensions/feishu/src/transport/client/lark-sdk-logger.js.map +1 -0
- package/dist/extensions/feishu/src/transport/reliability/dedupe.d.ts +7 -0
- package/dist/extensions/feishu/src/transport/reliability/dedupe.js +30 -0
- package/dist/extensions/feishu/src/transport/reliability/dedupe.js.map +1 -0
- package/dist/extensions/feishu/src/transport/socket-mode/monitor.d.ts +19 -0
- package/dist/extensions/feishu/src/transport/socket-mode/monitor.js +326 -0
- package/dist/extensions/feishu/src/transport/socket-mode/monitor.js.map +1 -0
- package/dist/extensions/feishu/src/transport/socket-mode/retry.d.ts +1 -0
- package/dist/extensions/feishu/src/transport/socket-mode/retry.js +10 -0
- package/dist/extensions/feishu/src/transport/socket-mode/retry.js.map +1 -0
- package/dist/extensions/feishu/src/transport/text/mentions.d.ts +1 -0
- package/dist/extensions/feishu/src/transport/text/mentions.js +9 -0
- package/dist/extensions/feishu/src/transport/text/mentions.js.map +1 -0
- package/dist/extensions/feishu/src/transport/webhook/monitor.d.ts +19 -0
- package/dist/extensions/feishu/src/transport/webhook/monitor.js +271 -0
- package/dist/extensions/feishu/src/transport/webhook/monitor.js.map +1 -0
- package/dist/extensions/feishu/src/ui/config-surface.d.ts +2 -0
- package/dist/extensions/feishu/src/ui/config-surface.js +6 -0
- package/dist/extensions/feishu/src/ui/config-surface.js.map +1 -0
- package/dist/extensions/feishu/xopc.extension.json +18 -0
- package/dist/extensions/telegram/xopc.extension.json +20 -0
- package/dist/extensions/weixin/xopc.extension.json +17 -0
- package/dist/gateway/static/root/assets/{agents-Be8iYqc2.js → agents-Dy5cGVVQ.js} +2 -2
- package/dist/gateway/static/root/assets/{agents-Be8iYqc2.js.map → agents-Dy5cGVVQ.js.map} +1 -1
- package/dist/gateway/static/root/assets/{apps-page-BLNTewgA.js → apps-page-BOpDR0Lz.js} +2 -2
- package/dist/gateway/static/root/assets/{apps-page-BLNTewgA.js.map → apps-page-BOpDR0Lz.js.map} +1 -1
- package/dist/gateway/static/root/assets/channels-settings-CrCesccB.js +9 -0
- package/dist/gateway/static/root/assets/channels-settings-CrCesccB.js.map +1 -0
- package/dist/gateway/static/root/assets/{cron-page-HoSnHbPX.js → cron-page-B_XY0gPt.js} +2 -2
- package/dist/gateway/static/root/assets/{cron-page-HoSnHbPX.js.map → cron-page-B_XY0gPt.js.map} +1 -1
- package/dist/gateway/static/root/assets/{cron-utils-D1mrWEee.js → cron-utils-BYdnLwhl.js} +2 -2
- package/dist/gateway/static/root/assets/{cron-utils-D1mrWEee.js.map → cron-utils-BYdnLwhl.js.map} +1 -1
- package/dist/gateway/static/root/assets/{dist-CowQhLuH.js → dist-DvaA5uNp.js} +2 -2
- package/dist/gateway/static/root/assets/{dist-CowQhLuH.js.map → dist-DvaA5uNp.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-bjoNo6JH.js → extension-debug-page-CPSk7gFW.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-debug-page-bjoNo6JH.js.map → extension-debug-page-CPSk7gFW.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-NU-MTrtq.js → extension-page-COdbk9I6.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-page-NU-MTrtq.js.map → extension-page-COdbk9I6.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-_kAL2Nt-.js → extension-settings-page-BlEz2Ily.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-settings-page-_kAL2Nt-.js.map → extension-settings-page-BlEz2Ily.js.map} +1 -1
- package/dist/gateway/static/root/assets/index-BQNdJlkw.css +1 -0
- package/dist/gateway/static/root/assets/index-tm9ZY35l.js +144 -0
- package/dist/gateway/static/root/assets/{index-DoudkP0H.js.map → index-tm9ZY35l.js.map} +1 -1
- package/dist/gateway/static/root/assets/{logs-page-BYPAmUPJ.js → logs-page-LSa0jmLO.js} +2 -2
- package/dist/gateway/static/root/assets/{logs-page-BYPAmUPJ.js.map → logs-page-LSa0jmLO.js.map} +1 -1
- package/dist/gateway/static/root/assets/sessions-page-cn2fi_V3.js +2 -0
- package/dist/gateway/static/root/assets/sessions-page-cn2fi_V3.js.map +1 -0
- package/dist/gateway/static/root/assets/{settings-page-BWE5R4rm.js → settings-page-CyHd5szQ.js} +2 -2
- package/dist/gateway/static/root/assets/{settings-page-BWE5R4rm.js.map → settings-page-CyHd5szQ.js.map} +1 -1
- package/dist/gateway/static/root/assets/{skills-page-CA2NcCUa.js → skills-page-irjxwW9u.js} +2 -2
- package/dist/gateway/static/root/assets/{skills-page-CA2NcCUa.js.map → skills-page-irjxwW9u.js.map} +1 -1
- package/dist/gateway/static/root/channel-icons/feishu.svg +12 -0
- package/dist/gateway/static/root/channel-icons/lark.svg +12 -0
- package/dist/gateway/static/root/channel-icons/telegram.svg +1 -0
- package/dist/gateway/static/root/channel-icons/wechat.svg +1 -0
- package/dist/gateway/static/root/channel-icons/weixin.svg +1 -0
- package/dist/gateway/static/root/index.html +2 -2
- package/dist/package.js +1 -1
- package/dist/src/agent/agent-manager.d.ts +1 -0
- package/dist/src/agent/agent-manager.js +1 -0
- package/dist/src/agent/agent-manager.js.map +1 -1
- package/dist/src/agent/service.js +1 -0
- package/dist/src/agent/service.js.map +1 -1
- package/dist/src/agent/tools/delegate-tool.d.ts +8 -0
- package/dist/src/agent/tools/delegate-tool.js +60 -2
- package/dist/src/agent/tools/delegate-tool.js.map +1 -1
- package/dist/src/agent/tools/factory.d.ts +1 -0
- package/dist/src/agent/tools/factory.js +2 -0
- package/dist/src/agent/tools/factory.js.map +1 -1
- package/dist/src/channels/envelope-timestamp.d.ts +5 -0
- package/dist/src/channels/envelope-timestamp.js +10 -1
- package/dist/src/channels/envelope-timestamp.js.map +1 -1
- package/dist/src/channels/feishu/index.d.ts +5 -0
- package/dist/src/channels/feishu/index.js +4 -0
- package/dist/src/chat-commands/types.d.ts +1 -1
- package/dist/src/extensions/types/hooks.d.ts +46 -1
- package/dist/src/extensions/types/hooks.js +3 -0
- package/dist/src/extensions/types/hooks.js.map +1 -1
- package/dist/src/gateway/service.js +1 -1
- package/dist/src/generated/bundled-channel-plugins.d.ts +2 -1
- package/dist/src/generated/bundled-channel-plugins.js +8 -2
- package/dist/src/generated/bundled-channel-plugins.js.map +1 -1
- package/dist/src/session/session-title.js +2 -1
- package/dist/src/session/session-title.js.map +1 -1
- package/package.json +2 -2
- package/dist/gateway/static/root/assets/channels-settings-p-9taPc_.js +0 -9
- package/dist/gateway/static/root/assets/channels-settings-p-9taPc_.js.map +0 -1
- package/dist/gateway/static/root/assets/index-CbNEU6bw.css +0 -1
- package/dist/gateway/static/root/assets/index-DoudkP0H.js +0 -144
- package/dist/gateway/static/root/assets/sessions-page-cNZK0pkU.js +0 -2
- package/dist/gateway/static/root/assets/sessions-page-cNZK0pkU.js.map +0 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { markdownToIR } from "../../../src/markdown/ir.js";
|
|
2
|
+
import { renderMarkdownWithMarkers } from "../../../src/markdown/render.js";
|
|
3
|
+
import "../../../src/markdown/index.js";
|
|
4
|
+
//#region extensions/feishu/src/format.ts
|
|
5
|
+
/**
|
|
6
|
+
* Feishu / Lark outbound text formatting.
|
|
7
|
+
*
|
|
8
|
+
* Agent output is often Markdown. Feishu card markdown (`tag: markdown`) expects
|
|
9
|
+
* Lark-flavored Markdown (bold/italic/strikethrough, links, code fences, etc.), not
|
|
10
|
+
* CommonMark-only constructs like `**bold**` left uninterpreted in plain `text`
|
|
11
|
+
* messages — and plain text messages do not render Markdown at all.
|
|
12
|
+
*
|
|
13
|
+
* Pipeline mirrors Telegram: Markdown → shared IR → channel-specific rendering.
|
|
14
|
+
*/
|
|
15
|
+
const FEISHU_STYLE_MARKERS = {
|
|
16
|
+
bold: {
|
|
17
|
+
open: "**",
|
|
18
|
+
close: "**"
|
|
19
|
+
},
|
|
20
|
+
italic: {
|
|
21
|
+
open: "*",
|
|
22
|
+
close: "*"
|
|
23
|
+
},
|
|
24
|
+
strikethrough: {
|
|
25
|
+
open: "~~",
|
|
26
|
+
close: "~~"
|
|
27
|
+
},
|
|
28
|
+
code: {
|
|
29
|
+
open: "`",
|
|
30
|
+
close: "`"
|
|
31
|
+
},
|
|
32
|
+
code_block: {
|
|
33
|
+
open: "```\n",
|
|
34
|
+
close: "\n```"
|
|
35
|
+
},
|
|
36
|
+
blockquote: {
|
|
37
|
+
open: "> ",
|
|
38
|
+
close: ""
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
function escapeFeishuMarkdownPlain(text) {
|
|
42
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\*/g, "*").replace(/_/g, "_").replace(/~/g, "~").replace(/\[/g, "[").replace(/\]/g, "]").replace(/`/g, "`");
|
|
43
|
+
}
|
|
44
|
+
function buildFeishuLinkRenderer(_irText) {
|
|
45
|
+
return (link, _text) => {
|
|
46
|
+
const href = link.href.trim();
|
|
47
|
+
if (!href) return null;
|
|
48
|
+
if (!/^(https?|mailto):/i.test(href) && !href.startsWith("#")) return null;
|
|
49
|
+
if (_text.trim() === href) return {
|
|
50
|
+
start: link.start,
|
|
51
|
+
end: link.end,
|
|
52
|
+
open: "<",
|
|
53
|
+
close: `>`
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
start: link.start,
|
|
57
|
+
end: link.end,
|
|
58
|
+
open: "[",
|
|
59
|
+
close: `](${href})`
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function renderFeishuCardMarkdown(markdown, options = {}) {
|
|
64
|
+
const ir = markdownToIR(markdown, {
|
|
65
|
+
linkify: true,
|
|
66
|
+
enableSpoilers: false,
|
|
67
|
+
tableMode: options.tableMode ?? "bullets"
|
|
68
|
+
});
|
|
69
|
+
return renderMarkdownWithMarkers(ir, {
|
|
70
|
+
styleMarkers: FEISHU_STYLE_MARKERS,
|
|
71
|
+
escapeText: escapeFeishuMarkdownPlain,
|
|
72
|
+
buildLink: buildFeishuLinkRenderer(ir.text)
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function markdownToFeishuPlainText(markdown) {
|
|
76
|
+
const ir = markdownToIR(markdown, {
|
|
77
|
+
linkify: true,
|
|
78
|
+
enableSpoilers: false,
|
|
79
|
+
tableMode: "bullets"
|
|
80
|
+
});
|
|
81
|
+
let result = ir.text;
|
|
82
|
+
const sortedLinks = [...ir.links].toSorted((a, b) => b.start - a.start);
|
|
83
|
+
for (const link of sortedLinks) {
|
|
84
|
+
const replacement = `${ir.text.slice(link.start, link.end)} (${link.href})`;
|
|
85
|
+
result = result.slice(0, link.start) + replacement + result.slice(link.end);
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
function formatFeishuOutboundText(input) {
|
|
90
|
+
const raw = input.text ?? "";
|
|
91
|
+
if (!raw.trim()) return raw;
|
|
92
|
+
if (input.renderMode === "raw") return raw;
|
|
93
|
+
if (input.forCardMarkdown) return renderFeishuCardMarkdown(raw);
|
|
94
|
+
return markdownToFeishuPlainText(raw);
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
export { formatFeishuOutboundText, markdownToFeishuPlainText, renderFeishuCardMarkdown };
|
|
98
|
+
|
|
99
|
+
//# sourceMappingURL=format.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.js","names":[],"sources":["../../../../extensions/feishu/src/format.ts"],"sourcesContent":["/**\n * Feishu / Lark outbound text formatting.\n *\n * Agent output is often Markdown. Feishu card markdown (`tag: markdown`) expects\n * Lark-flavored Markdown (bold/italic/strikethrough, links, code fences, etc.), not\n * CommonMark-only constructs like `**bold**` left uninterpreted in plain `text`\n * messages — and plain text messages do not render Markdown at all.\n *\n * Pipeline mirrors Telegram: Markdown → shared IR → channel-specific rendering.\n */\n\nimport {\n markdownToIR,\n renderMarkdownWithMarkers,\n type MarkdownLinkSpan,\n type RenderStyleMap,\n} from '@xopcai/xopc/markdown/index.js';\n\nimport type { MarkdownTableMode } from '@xopcai/xopc/config/types.base.js';\n\nconst FEISHU_STYLE_MARKERS: RenderStyleMap = {\n bold: { open: '**', close: '**' },\n italic: { open: '*', close: '*' },\n strikethrough: { open: '~~', close: '~~' },\n code: { open: '`', close: '`' },\n code_block: { open: '```\\n', close: '\\n```' },\n // Lark markdown supports `> ...` block quotes; IR text omits leading `>`.\n blockquote: { open: '> ', close: '' },\n};\n\nfunction escapeFeishuMarkdownPlain(text: string): string {\n // Lark docs: escape markdown metacharacters with HTML entities when needed.\n return text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\\*/g, '*')\n .replace(/_/g, '_')\n .replace(/~/g, '~')\n .replace(/\\[/g, '[')\n .replace(/\\]/g, ']')\n .replace(/`/g, '`');\n}\n\nfunction buildFeishuLinkRenderer(\n _irText: string\n): (link: MarkdownLinkSpan, text: string) => { start: number; end: number; open: string; close: string } | null {\n return (link, _text) => {\n const href = link.href.trim();\n if (!href) return null;\n\n const allowedSchemes = /^(https?|mailto):/i;\n if (!allowedSchemes.test(href) && !href.startsWith('#')) {\n return null;\n }\n\n // Lark examples commonly use `<https://...>` for bare URLs; labeled links use markdown syntax.\n const isBareUrl = _text.trim() === href;\n if (isBareUrl) {\n return {\n start: link.start,\n end: link.end,\n open: '<',\n close: `>`,\n };\n }\n\n return {\n start: link.start,\n end: link.end,\n open: '[',\n close: `](${href})`,\n };\n };\n}\n\nexport function renderFeishuCardMarkdown(\n markdown: string,\n options: { tableMode?: MarkdownTableMode } = {}\n): string {\n const ir = markdownToIR(markdown, {\n linkify: true,\n enableSpoilers: false,\n tableMode: options.tableMode ?? 'bullets',\n });\n\n return renderMarkdownWithMarkers(ir, {\n styleMarkers: FEISHU_STYLE_MARKERS,\n escapeText: escapeFeishuMarkdownPlain,\n buildLink: buildFeishuLinkRenderer(ir.text),\n });\n}\n\nexport function markdownToFeishuPlainText(markdown: string): string {\n const ir = markdownToIR(markdown, {\n linkify: true,\n enableSpoilers: false,\n tableMode: 'bullets',\n });\n\n let result = ir.text;\n const sortedLinks = [...ir.links].toSorted((a, b) => b.start - a.start);\n for (const link of sortedLinks) {\n const linkText = ir.text.slice(link.start, link.end);\n const replacement = `${linkText} (${link.href})`;\n result = result.slice(0, link.start) + replacement + result.slice(link.end);\n }\n\n return result;\n}\n\nexport function formatFeishuOutboundText(input: {\n text: string;\n renderMode?: 'auto' | 'raw' | 'card';\n /** When true, target is Feishu interactive markdown (card / card element). */\n forCardMarkdown: boolean;\n}): string {\n const raw = input.text ?? '';\n if (!raw.trim()) return raw;\n\n if (input.renderMode === 'raw') {\n return raw;\n }\n\n if (input.forCardMarkdown) {\n return renderFeishuCardMarkdown(raw);\n }\n\n return markdownToFeishuPlainText(raw);\n}\n"],"mappings":";;;;;;;;;;;;;;AAoBA,MAAM,uBAAuC;CAC3C,MAAM;EAAE,MAAM;EAAM,OAAO;EAAM;CACjC,QAAQ;EAAE,MAAM;EAAK,OAAO;EAAK;CACjC,eAAe;EAAE,MAAM;EAAM,OAAO;EAAM;CAC1C,MAAM;EAAE,MAAM;EAAK,OAAO;EAAK;CAC/B,YAAY;EAAE,MAAM;EAAS,OAAO;EAAS;CAE7C,YAAY;EAAE,MAAM;EAAM,OAAO;EAAI;CACtC;AAED,SAAS,0BAA0B,MAAsB;AAEvD,QAAO,KACJ,QAAQ,MAAM,QAAQ,CACtB,QAAQ,MAAM,OAAO,CACrB,QAAQ,MAAM,OAAO,CACrB,QAAQ,OAAO,QAAQ,CACvB,QAAQ,MAAM,QAAQ,CACtB,QAAQ,MAAM,SAAS,CACvB,QAAQ,OAAO,QAAQ,CACvB,QAAQ,OAAO,QAAQ,CACvB,QAAQ,MAAM,QAAQ;;AAG3B,SAAS,wBACP,SAC8G;AAC9G,SAAQ,MAAM,UAAU;EACtB,MAAM,OAAO,KAAK,KAAK,MAAM;AAC7B,MAAI,CAAC,KAAM,QAAO;AAGlB,MAAI,CAAC,qBAAe,KAAK,KAAK,IAAI,CAAC,KAAK,WAAW,IAAI,CACrD,QAAO;AAKT,MADkB,MAAM,MAAM,KAAK,KAEjC,QAAO;GACL,OAAO,KAAK;GACZ,KAAK,KAAK;GACV,MAAM;GACN,OAAO;GACR;AAGH,SAAO;GACL,OAAO,KAAK;GACZ,KAAK,KAAK;GACV,MAAM;GACN,OAAO,KAAK,KAAK;GAClB;;;AAIL,SAAgB,yBACd,UACA,UAA6C,EAAE,EACvC;CACR,MAAM,KAAK,aAAa,UAAU;EAChC,SAAS;EACT,gBAAgB;EAChB,WAAW,QAAQ,aAAa;EACjC,CAAC;AAEF,QAAO,0BAA0B,IAAI;EACnC,cAAc;EACd,YAAY;EACZ,WAAW,wBAAwB,GAAG,KAAK;EAC5C,CAAC;;AAGJ,SAAgB,0BAA0B,UAA0B;CAClE,MAAM,KAAK,aAAa,UAAU;EAChC,SAAS;EACT,gBAAgB;EAChB,WAAW;EACZ,CAAC;CAEF,IAAI,SAAS,GAAG;CAChB,MAAM,cAAc,CAAC,GAAG,GAAG,MAAM,CAAC,UAAU,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM;AACvE,MAAK,MAAM,QAAQ,aAAa;EAE9B,MAAM,cAAc,GADH,GAAG,KAAK,MAAM,KAAK,OAAO,KAAK,IACjB,CAAC,IAAI,KAAK,KAAK;AAC9C,WAAS,OAAO,MAAM,GAAG,KAAK,MAAM,GAAG,cAAc,OAAO,MAAM,KAAK,IAAI;;AAG7E,QAAO;;AAGT,SAAgB,yBAAyB,OAK9B;CACT,MAAM,MAAM,MAAM,QAAQ;AAC1B,KAAI,CAAC,IAAI,MAAM,CAAE,QAAO;AAExB,KAAI,MAAM,eAAe,MACvB,QAAO;AAGT,KAAI,MAAM,gBACR,QAAO,yBAAyB,IAAI;AAGtC,QAAO,0BAA0B,IAAI"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Config } from '@xopcai/xopc/config/schema.js';
|
|
2
|
+
export declare function editMessageFeishu(params: {
|
|
3
|
+
cfg: Config;
|
|
4
|
+
accountId?: string;
|
|
5
|
+
messageId: string;
|
|
6
|
+
text: string;
|
|
7
|
+
}): Promise<{
|
|
8
|
+
ok: boolean;
|
|
9
|
+
}>;
|
|
10
|
+
export declare function getMessageFeishu(params: {
|
|
11
|
+
cfg: Config;
|
|
12
|
+
accountId?: string;
|
|
13
|
+
messageId: string;
|
|
14
|
+
}): Promise<any>;
|
|
15
|
+
export declare function addReactionFeishu(params: {
|
|
16
|
+
cfg: Config;
|
|
17
|
+
accountId?: string;
|
|
18
|
+
messageId: string;
|
|
19
|
+
emojiType: string;
|
|
20
|
+
}): Promise<any>;
|
|
21
|
+
export declare function listReactionsFeishu(params: {
|
|
22
|
+
cfg: Config;
|
|
23
|
+
accountId?: string;
|
|
24
|
+
messageId: string;
|
|
25
|
+
emojiType?: string;
|
|
26
|
+
}): Promise<any>;
|
|
27
|
+
export declare function removeReactionFeishu(params: {
|
|
28
|
+
cfg: Config;
|
|
29
|
+
accountId?: string;
|
|
30
|
+
messageId: string;
|
|
31
|
+
reactionId: string;
|
|
32
|
+
}): Promise<any>;
|
|
33
|
+
export declare function pinMessageFeishu(params: {
|
|
34
|
+
cfg: Config;
|
|
35
|
+
accountId?: string;
|
|
36
|
+
messageId: string;
|
|
37
|
+
}): Promise<any>;
|
|
38
|
+
export declare function unpinMessageFeishu(params: {
|
|
39
|
+
cfg: Config;
|
|
40
|
+
accountId?: string;
|
|
41
|
+
messageId: string;
|
|
42
|
+
}): Promise<any>;
|
|
43
|
+
export declare function listPinsFeishu(params: {
|
|
44
|
+
cfg: Config;
|
|
45
|
+
accountId?: string;
|
|
46
|
+
chatId: string;
|
|
47
|
+
startTime?: string;
|
|
48
|
+
endTime?: string;
|
|
49
|
+
pageSize?: number;
|
|
50
|
+
pageToken?: string;
|
|
51
|
+
}): Promise<any>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { resolveFeishuAccount } from "../state/accounts.js";
|
|
2
|
+
import { createFeishuClient } from "../transport/client/client.js";
|
|
3
|
+
//#region extensions/feishu/src/outbound/actions.ts
|
|
4
|
+
async function editMessageFeishu(params) {
|
|
5
|
+
const { api } = createFeishuClient(resolveFeishuAccount(params.cfg, params.accountId ?? "default"));
|
|
6
|
+
await api.im.v1.message.update({
|
|
7
|
+
path: { message_id: params.messageId },
|
|
8
|
+
data: {
|
|
9
|
+
msg_type: "text",
|
|
10
|
+
content: JSON.stringify({ text: params.text })
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
return { ok: true };
|
|
14
|
+
}
|
|
15
|
+
async function getMessageFeishu(params) {
|
|
16
|
+
const { api } = createFeishuClient(resolveFeishuAccount(params.cfg, params.accountId ?? "default"));
|
|
17
|
+
const res = await api.im.message.get({ path: { message_id: params.messageId } });
|
|
18
|
+
return res?.data ?? res;
|
|
19
|
+
}
|
|
20
|
+
async function addReactionFeishu(params) {
|
|
21
|
+
const { api } = createFeishuClient(resolveFeishuAccount(params.cfg, params.accountId ?? "default"));
|
|
22
|
+
return await api.im.v1.messageReaction.create({
|
|
23
|
+
path: { message_id: params.messageId },
|
|
24
|
+
data: { reaction_type: { emoji_type: params.emojiType } }
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
async function listReactionsFeishu(params) {
|
|
28
|
+
const { api } = createFeishuClient(resolveFeishuAccount(params.cfg, params.accountId ?? "default"));
|
|
29
|
+
return await api.im.v1.messageReaction.list({
|
|
30
|
+
path: { message_id: params.messageId },
|
|
31
|
+
params: params.emojiType ? { reaction_type: params.emojiType } : {}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async function removeReactionFeishu(params) {
|
|
35
|
+
const { api } = createFeishuClient(resolveFeishuAccount(params.cfg, params.accountId ?? "default"));
|
|
36
|
+
return await api.im.v1.messageReaction.delete({ path: {
|
|
37
|
+
message_id: params.messageId,
|
|
38
|
+
reaction_id: params.reactionId
|
|
39
|
+
} });
|
|
40
|
+
}
|
|
41
|
+
async function pinMessageFeishu(params) {
|
|
42
|
+
const { api } = createFeishuClient(resolveFeishuAccount(params.cfg, params.accountId ?? "default"));
|
|
43
|
+
return await api.im.v1.pin.create({ data: { message_id: params.messageId } });
|
|
44
|
+
}
|
|
45
|
+
async function unpinMessageFeishu(params) {
|
|
46
|
+
const { api } = createFeishuClient(resolveFeishuAccount(params.cfg, params.accountId ?? "default"));
|
|
47
|
+
return await api.im.v1.pin.delete({ path: { message_id: params.messageId } });
|
|
48
|
+
}
|
|
49
|
+
async function listPinsFeishu(params) {
|
|
50
|
+
const { api } = createFeishuClient(resolveFeishuAccount(params.cfg, params.accountId ?? "default"));
|
|
51
|
+
return await api.im.v1.pin.list({ params: {
|
|
52
|
+
chat_id: params.chatId,
|
|
53
|
+
...params.startTime ? { start_time: params.startTime } : {},
|
|
54
|
+
...params.endTime ? { end_time: params.endTime } : {},
|
|
55
|
+
...params.pageSize ? { page_size: params.pageSize } : {},
|
|
56
|
+
...params.pageToken ? { page_token: params.pageToken } : {}
|
|
57
|
+
} });
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
export { addReactionFeishu, editMessageFeishu, getMessageFeishu, listPinsFeishu, listReactionsFeishu, pinMessageFeishu, removeReactionFeishu, unpinMessageFeishu };
|
|
61
|
+
|
|
62
|
+
//# sourceMappingURL=actions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actions.js","names":[],"sources":["../../../../../extensions/feishu/src/outbound/actions.ts"],"sourcesContent":["import type { Config } from '@xopcai/xopc/config/schema.js';\n\nimport { resolveFeishuAccount } from '../state/accounts.js';\nimport { createFeishuClient } from '../transport/client/client.js';\n\nexport async function editMessageFeishu(params: {\n cfg: Config;\n accountId?: string;\n messageId: string;\n text: string;\n}): Promise<{ ok: boolean }> {\n const account = resolveFeishuAccount(params.cfg, params.accountId ?? 'default');\n const { api } = createFeishuClient(account);\n await (api as any).im.v1.message.update({\n path: { message_id: params.messageId },\n data: {\n msg_type: 'text',\n content: JSON.stringify({ text: params.text }),\n },\n });\n return { ok: true };\n}\n\nexport async function getMessageFeishu(params: {\n cfg: Config;\n accountId?: string;\n messageId: string;\n}): Promise<any> {\n const account = resolveFeishuAccount(params.cfg, params.accountId ?? 'default');\n const { api } = createFeishuClient(account);\n const res = await (api as any).im.message.get({\n path: { message_id: params.messageId },\n });\n return res?.data ?? res;\n}\n\nexport async function addReactionFeishu(params: {\n cfg: Config;\n accountId?: string;\n messageId: string;\n emojiType: string;\n}): Promise<any> {\n const account = resolveFeishuAccount(params.cfg, params.accountId ?? 'default');\n const { api } = createFeishuClient(account);\n return await (api as any).im.v1.messageReaction.create({\n path: { message_id: params.messageId },\n data: {\n reaction_type: { emoji_type: params.emojiType },\n },\n });\n}\n\nexport async function listReactionsFeishu(params: {\n cfg: Config;\n accountId?: string;\n messageId: string;\n emojiType?: string;\n}): Promise<any> {\n const account = resolveFeishuAccount(params.cfg, params.accountId ?? 'default');\n const { api } = createFeishuClient(account);\n return await (api as any).im.v1.messageReaction.list({\n path: { message_id: params.messageId },\n params: params.emojiType ? { reaction_type: params.emojiType } : {},\n });\n}\n\nexport async function removeReactionFeishu(params: {\n cfg: Config;\n accountId?: string;\n messageId: string;\n reactionId: string;\n}): Promise<any> {\n const account = resolveFeishuAccount(params.cfg, params.accountId ?? 'default');\n const { api } = createFeishuClient(account);\n return await (api as any).im.v1.messageReaction.delete({\n path: { message_id: params.messageId, reaction_id: params.reactionId },\n });\n}\n\nexport async function pinMessageFeishu(params: {\n cfg: Config;\n accountId?: string;\n messageId: string;\n}): Promise<any> {\n const account = resolveFeishuAccount(params.cfg, params.accountId ?? 'default');\n const { api } = createFeishuClient(account);\n return await (api as any).im.v1.pin.create({\n data: { message_id: params.messageId },\n });\n}\n\nexport async function unpinMessageFeishu(params: {\n cfg: Config;\n accountId?: string;\n messageId: string;\n}): Promise<any> {\n const account = resolveFeishuAccount(params.cfg, params.accountId ?? 'default');\n const { api } = createFeishuClient(account);\n return await (api as any).im.v1.pin.delete({\n path: { message_id: params.messageId },\n });\n}\n\nexport async function listPinsFeishu(params: {\n cfg: Config;\n accountId?: string;\n chatId: string;\n startTime?: string;\n endTime?: string;\n pageSize?: number;\n pageToken?: string;\n}): Promise<any> {\n const account = resolveFeishuAccount(params.cfg, params.accountId ?? 'default');\n const { api } = createFeishuClient(account);\n return await (api as any).im.v1.pin.list({\n params: {\n chat_id: params.chatId,\n ...(params.startTime ? { start_time: params.startTime } : {}),\n ...(params.endTime ? { end_time: params.endTime } : {}),\n ...(params.pageSize ? { page_size: params.pageSize } : {}),\n ...(params.pageToken ? { page_token: params.pageToken } : {}),\n },\n });\n}\n\n"],"mappings":";;;AAKA,eAAsB,kBAAkB,QAKX;CAE3B,MAAM,EAAE,QAAQ,mBADA,qBAAqB,OAAO,KAAK,OAAO,aAAa,UAC3B,CAAC;AAC3C,OAAO,IAAY,GAAG,GAAG,QAAQ,OAAO;EACtC,MAAM,EAAE,YAAY,OAAO,WAAW;EACtC,MAAM;GACJ,UAAU;GACV,SAAS,KAAK,UAAU,EAAE,MAAM,OAAO,MAAM,CAAC;GAC/C;EACF,CAAC;AACF,QAAO,EAAE,IAAI,MAAM;;AAGrB,eAAsB,iBAAiB,QAItB;CAEf,MAAM,EAAE,QAAQ,mBADA,qBAAqB,OAAO,KAAK,OAAO,aAAa,UAC3B,CAAC;CAC3C,MAAM,MAAM,MAAO,IAAY,GAAG,QAAQ,IAAI,EAC5C,MAAM,EAAE,YAAY,OAAO,WAAW,EACvC,CAAC;AACF,QAAO,KAAK,QAAQ;;AAGtB,eAAsB,kBAAkB,QAKvB;CAEf,MAAM,EAAE,QAAQ,mBADA,qBAAqB,OAAO,KAAK,OAAO,aAAa,UAC3B,CAAC;AAC3C,QAAO,MAAO,IAAY,GAAG,GAAG,gBAAgB,OAAO;EACrD,MAAM,EAAE,YAAY,OAAO,WAAW;EACtC,MAAM,EACJ,eAAe,EAAE,YAAY,OAAO,WAAW,EAChD;EACF,CAAC;;AAGJ,eAAsB,oBAAoB,QAKzB;CAEf,MAAM,EAAE,QAAQ,mBADA,qBAAqB,OAAO,KAAK,OAAO,aAAa,UAC3B,CAAC;AAC3C,QAAO,MAAO,IAAY,GAAG,GAAG,gBAAgB,KAAK;EACnD,MAAM,EAAE,YAAY,OAAO,WAAW;EACtC,QAAQ,OAAO,YAAY,EAAE,eAAe,OAAO,WAAW,GAAG,EAAE;EACpE,CAAC;;AAGJ,eAAsB,qBAAqB,QAK1B;CAEf,MAAM,EAAE,QAAQ,mBADA,qBAAqB,OAAO,KAAK,OAAO,aAAa,UAC3B,CAAC;AAC3C,QAAO,MAAO,IAAY,GAAG,GAAG,gBAAgB,OAAO,EACrD,MAAM;EAAE,YAAY,OAAO;EAAW,aAAa,OAAO;EAAY,EACvE,CAAC;;AAGJ,eAAsB,iBAAiB,QAItB;CAEf,MAAM,EAAE,QAAQ,mBADA,qBAAqB,OAAO,KAAK,OAAO,aAAa,UAC3B,CAAC;AAC3C,QAAO,MAAO,IAAY,GAAG,GAAG,IAAI,OAAO,EACzC,MAAM,EAAE,YAAY,OAAO,WAAW,EACvC,CAAC;;AAGJ,eAAsB,mBAAmB,QAIxB;CAEf,MAAM,EAAE,QAAQ,mBADA,qBAAqB,OAAO,KAAK,OAAO,aAAa,UAC3B,CAAC;AAC3C,QAAO,MAAO,IAAY,GAAG,GAAG,IAAI,OAAO,EACzC,MAAM,EAAE,YAAY,OAAO,WAAW,EACvC,CAAC;;AAGJ,eAAsB,eAAe,QAQpB;CAEf,MAAM,EAAE,QAAQ,mBADA,qBAAqB,OAAO,KAAK,OAAO,aAAa,UAC3B,CAAC;AAC3C,QAAO,MAAO,IAAY,GAAG,GAAG,IAAI,KAAK,EACvC,QAAQ;EACN,SAAS,OAAO;EAChB,GAAI,OAAO,YAAY,EAAE,YAAY,OAAO,WAAW,GAAG,EAAE;EAC5D,GAAI,OAAO,UAAU,EAAE,UAAU,OAAO,SAAS,GAAG,EAAE;EACtD,GAAI,OAAO,WAAW,EAAE,WAAW,OAAO,UAAU,GAAG,EAAE;EACzD,GAAI,OAAO,YAAY,EAAE,YAAY,OAAO,WAAW,GAAG,EAAE;EAC7D,EACF,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import type { Config } from '@xopcai/xopc/config/schema.js';
|
|
3
|
+
export type LoadedMedia = {
|
|
4
|
+
buffer: Buffer;
|
|
5
|
+
mimeType: string;
|
|
6
|
+
filename: string;
|
|
7
|
+
stream: Readable;
|
|
8
|
+
};
|
|
9
|
+
export declare function loadMediaForFeishu(cfg: Config, rawInput: string, opts: {
|
|
10
|
+
maxBytes: number;
|
|
11
|
+
localRoots?: readonly string[];
|
|
12
|
+
}): Promise<LoadedMedia>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { getWorkspacePath, init_schema } from "../../../../src/config/schema.js";
|
|
2
|
+
import { checkFileSafety } from "../../../../src/agent/prompt/safety.js";
|
|
3
|
+
import { getMimeType } from "../../../../src/channels/media.js";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promises } from "node:fs";
|
|
6
|
+
import { Readable } from "node:stream";
|
|
7
|
+
//#region extensions/feishu/src/outbound/media-load.ts
|
|
8
|
+
init_schema();
|
|
9
|
+
function isPathUnderRoots(resolved, roots) {
|
|
10
|
+
const norm = path.normalize(resolved);
|
|
11
|
+
for (const root of roots) {
|
|
12
|
+
const r = path.normalize(root);
|
|
13
|
+
if (norm === r || norm.startsWith(r + path.sep)) return true;
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
async function loadMediaForFeishu(cfg, rawInput, opts) {
|
|
18
|
+
const input = rawInput.trim().startsWith("@") ? rawInput.trim().slice(1).trim() : rawInput.trim();
|
|
19
|
+
if (!input) throw new Error("empty media reference");
|
|
20
|
+
const isHttpUrl = /^https?:\/\//i.test(input);
|
|
21
|
+
const isFileUrl = /^file:\/\//i.test(input);
|
|
22
|
+
const isDataUrl = /^data:/i.test(input);
|
|
23
|
+
if (isHttpUrl) {
|
|
24
|
+
const res = await fetch(input, { redirect: "follow" });
|
|
25
|
+
if (!res.ok) throw new Error(`Failed to fetch media: HTTP ${res.status}`);
|
|
26
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
27
|
+
if (buf.length > opts.maxBytes) throw new Error(`Media too large (${buf.length} bytes, max ${opts.maxBytes})`);
|
|
28
|
+
return {
|
|
29
|
+
buffer: buf,
|
|
30
|
+
mimeType: (res.headers.get("content-type") || "application/octet-stream").split(";")[0]?.trim() || "application/octet-stream",
|
|
31
|
+
filename: safeBasename(new URL(input).pathname) || "media.bin",
|
|
32
|
+
stream: Readable.from(buf)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (isDataUrl) {
|
|
36
|
+
const m = /^data:([^;,]+)?(;base64)?,/i.exec(input);
|
|
37
|
+
if (!m) throw new Error("Invalid data URL");
|
|
38
|
+
const mime = (m[1] || "application/octet-stream").trim();
|
|
39
|
+
const isB64 = Boolean(m[2]);
|
|
40
|
+
const dataPart = input.slice(m[0].length);
|
|
41
|
+
const buf = isB64 ? Buffer.from(dataPart, "base64") : Buffer.from(decodeURIComponent(dataPart), "utf8");
|
|
42
|
+
if (buf.length > opts.maxBytes) throw new Error(`Media too large (${buf.length} bytes, max ${opts.maxBytes})`);
|
|
43
|
+
return {
|
|
44
|
+
buffer: buf,
|
|
45
|
+
mimeType: mime,
|
|
46
|
+
filename: mime.startsWith("image/") ? `image.${mime.split("/")[1] || "bin"}` : "media.bin",
|
|
47
|
+
stream: Readable.from(buf)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const b64Candidate = input.replace(/^['"]|['"]$/g, "");
|
|
51
|
+
if (b64Candidate.length >= 256 && b64Candidate.length % 4 === 0 && /^[A-Za-z0-9+/=\r\n]+$/.test(b64Candidate) && !b64Candidate.includes(path.sep)) {
|
|
52
|
+
const buf = Buffer.from(b64Candidate, "base64");
|
|
53
|
+
if (buf.length > 0) {
|
|
54
|
+
if (buf.length > opts.maxBytes) throw new Error(`Media too large (${buf.length} bytes, max ${opts.maxBytes})`);
|
|
55
|
+
const { mimeType, filename } = sniffBufferType(buf);
|
|
56
|
+
return {
|
|
57
|
+
buffer: buf,
|
|
58
|
+
mimeType,
|
|
59
|
+
filename,
|
|
60
|
+
stream: Readable.from(buf)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
let filePath = input;
|
|
65
|
+
if (isFileUrl) filePath = input.slice(7);
|
|
66
|
+
if (!path.isAbsolute(filePath)) filePath = path.resolve(getWorkspacePath(cfg), filePath);
|
|
67
|
+
filePath = path.normalize(filePath);
|
|
68
|
+
const safety = checkFileSafety("read", filePath);
|
|
69
|
+
if (!safety.allowed) throw new Error(safety.message ?? "File path not allowed");
|
|
70
|
+
const workspace = getWorkspacePath(cfg);
|
|
71
|
+
const roots = [workspace, ...opts.localRoots ?? []].filter(Boolean);
|
|
72
|
+
const realPath = await promises.realpath(filePath).catch(() => filePath);
|
|
73
|
+
if (!isPathUnderRoots(realPath, [await promises.realpath(workspace).catch(() => workspace), ...await Promise.all(roots.map((r) => promises.realpath(r).catch(() => r)))])) throw new Error(`Path not under workspace or allowed roots: ${filePath}`);
|
|
74
|
+
const st = await promises.stat(realPath).catch((err) => {
|
|
75
|
+
const em = err instanceof Error ? err.message : String(err);
|
|
76
|
+
throw new Error(`Media file not found or unreadable: ${filePath} (resolved: ${realPath}). ${em}`);
|
|
77
|
+
});
|
|
78
|
+
if (!st.isFile()) throw new Error(`Not a file: ${filePath}`);
|
|
79
|
+
if (st.size > opts.maxBytes) throw new Error(`Media too large (${st.size} bytes, max ${opts.maxBytes})`);
|
|
80
|
+
const buffer = await promises.readFile(realPath);
|
|
81
|
+
const filename = path.basename(realPath) || "media.bin";
|
|
82
|
+
return {
|
|
83
|
+
buffer,
|
|
84
|
+
mimeType: getMimeType("document", realPath),
|
|
85
|
+
filename,
|
|
86
|
+
stream: Readable.from(buffer)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function sniffBufferType(buf) {
|
|
90
|
+
if (buf.length >= 8 && buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71 && buf[4] === 13 && buf[5] === 10 && buf[6] === 26 && buf[7] === 10) return {
|
|
91
|
+
mimeType: "image/png",
|
|
92
|
+
filename: "image.png"
|
|
93
|
+
};
|
|
94
|
+
if (buf.length >= 3 && buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return {
|
|
95
|
+
mimeType: "image/jpeg",
|
|
96
|
+
filename: "image.jpg"
|
|
97
|
+
};
|
|
98
|
+
if (buf.length >= 6 && buf.subarray(0, 6).toString("ascii") === "GIF89a") return {
|
|
99
|
+
mimeType: "image/gif",
|
|
100
|
+
filename: "image.gif"
|
|
101
|
+
};
|
|
102
|
+
if (buf.length >= 12 && buf.subarray(0, 4).toString("ascii") === "RIFF" && buf.subarray(8, 12).toString("ascii") === "WEBP") return {
|
|
103
|
+
mimeType: "image/webp",
|
|
104
|
+
filename: "image.webp"
|
|
105
|
+
};
|
|
106
|
+
if (buf.length >= 4 && buf.subarray(0, 4).toString("ascii") === "%PDF") return {
|
|
107
|
+
mimeType: "application/pdf",
|
|
108
|
+
filename: "file.pdf"
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
mimeType: "application/octet-stream",
|
|
112
|
+
filename: "media.bin"
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function safeBasename(p) {
|
|
116
|
+
try {
|
|
117
|
+
return path.basename(p).replace(/[\\/:*?"<>|]/g, "_");
|
|
118
|
+
} catch {
|
|
119
|
+
return "";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
export { loadMediaForFeishu };
|
|
124
|
+
|
|
125
|
+
//# sourceMappingURL=media-load.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-load.js","names":["fs"],"sources":["../../../../../extensions/feishu/src/outbound/media-load.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { Readable } from 'node:stream';\n\nimport { getWorkspacePath } from '@xopcai/xopc/config/schema.js';\nimport type { Config } from '@xopcai/xopc/config/schema.js';\nimport { checkFileSafety } from '@xopcai/xopc/agent/prompt/safety.js';\nimport { getMimeType } from '@xopcai/xopc/channels/media.js';\n\nexport type LoadedMedia = {\n buffer: Buffer;\n mimeType: string;\n filename: string;\n stream: Readable;\n};\n\nfunction isPathUnderRoots(resolved: string, roots: string[]): boolean {\n const norm = path.normalize(resolved);\n for (const root of roots) {\n const r = path.normalize(root);\n if (norm === r || norm.startsWith(r + path.sep)) {\n return true;\n }\n }\n return false;\n}\n\nexport async function loadMediaForFeishu(\n cfg: Config,\n rawInput: string,\n opts: { maxBytes: number; localRoots?: readonly string[] },\n): Promise<LoadedMedia> {\n const input = rawInput.trim().startsWith('@') ? rawInput.trim().slice(1).trim() : rawInput.trim();\n if (!input) throw new Error('empty media reference');\n\n const isHttpUrl = /^https?:\\/\\//i.test(input);\n const isFileUrl = /^file:\\/\\//i.test(input);\n const isDataUrl = /^data:/i.test(input);\n\n if (isHttpUrl) {\n const res = await fetch(input, { redirect: 'follow' });\n if (!res.ok) throw new Error(`Failed to fetch media: HTTP ${res.status}`);\n const buf = Buffer.from(await res.arrayBuffer());\n if (buf.length > opts.maxBytes) throw new Error(`Media too large (${buf.length} bytes, max ${opts.maxBytes})`);\n const ct = res.headers.get('content-type') || 'application/octet-stream';\n const mime = ct.split(';')[0]?.trim() || 'application/octet-stream';\n const nameFromUrl = safeBasename(new URL(input).pathname) || 'media.bin';\n return { buffer: buf, mimeType: mime, filename: nameFromUrl, stream: Readable.from(buf) };\n }\n\n if (isDataUrl) {\n // data:[<mime>][;base64],<data>\n const m = /^data:([^;,]+)?(;base64)?,/i.exec(input);\n if (!m) throw new Error('Invalid data URL');\n const mime = (m[1] || 'application/octet-stream').trim();\n const isB64 = Boolean(m[2]);\n const dataPart = input.slice(m[0].length);\n const buf = isB64 ? Buffer.from(dataPart, 'base64') : Buffer.from(decodeURIComponent(dataPart), 'utf8');\n if (buf.length > opts.maxBytes) throw new Error(`Media too large (${buf.length} bytes, max ${opts.maxBytes})`);\n const filename = mime.startsWith('image/') ? `image.${mime.split('/')[1] || 'bin'}` : 'media.bin';\n return { buffer: buf, mimeType: mime, filename, stream: Readable.from(buf) };\n }\n\n // Heuristic: some callers persist raw base64 (no data: prefix). If it looks like base64, treat it as such.\n // This avoids mis-classifying huge base64 blobs as file paths during outbound replay.\n const b64Candidate = input.replace(/^['\"]|['\"]$/g, '');\n const looksBase64 =\n b64Candidate.length >= 256 &&\n b64Candidate.length % 4 === 0 &&\n /^[A-Za-z0-9+/=\\r\\n]+$/.test(b64Candidate) &&\n !b64Candidate.includes(path.sep);\n if (looksBase64) {\n const buf = Buffer.from(b64Candidate, 'base64');\n if (buf.length > 0) {\n if (buf.length > opts.maxBytes) throw new Error(`Media too large (${buf.length} bytes, max ${opts.maxBytes})`);\n const { mimeType, filename } = sniffBufferType(buf);\n return { buffer: buf, mimeType, filename, stream: Readable.from(buf) };\n }\n }\n\n let filePath = input;\n if (isFileUrl) filePath = input.slice('file://'.length);\n if (!path.isAbsolute(filePath)) {\n filePath = path.resolve(getWorkspacePath(cfg), filePath);\n }\n filePath = path.normalize(filePath);\n\n const safety = checkFileSafety('read', filePath);\n if (!safety.allowed) throw new Error(safety.message ?? 'File path not allowed');\n\n const workspace = getWorkspacePath(cfg);\n const roots = [workspace, ...(opts.localRoots ?? [])].filter(Boolean) as string[];\n const realPath = await fs.realpath(filePath).catch(() => filePath);\n const workspaceReal = await fs.realpath(workspace).catch(() => workspace);\n const resolvedRoots = await Promise.all(roots.map((r) => fs.realpath(r).catch(() => r)));\n if (!isPathUnderRoots(realPath, [workspaceReal, ...resolvedRoots])) {\n throw new Error(`Path not under workspace or allowed roots: ${filePath}`);\n }\n\n const st = await fs.stat(realPath).catch((err) => {\n const em = err instanceof Error ? err.message : String(err);\n throw new Error(`Media file not found or unreadable: ${filePath} (resolved: ${realPath}). ${em}`);\n });\n if (!st.isFile()) throw new Error(`Not a file: ${filePath}`);\n if (st.size > opts.maxBytes) throw new Error(`Media too large (${st.size} bytes, max ${opts.maxBytes})`);\n\n const buffer = await fs.readFile(realPath);\n const filename = path.basename(realPath) || 'media.bin';\n const mimeType = getMimeType('document', realPath);\n return { buffer, mimeType, filename, stream: Readable.from(buffer) };\n}\n\nfunction sniffBufferType(buf: Buffer): { mimeType: string; filename: string } {\n // PNG\n if (\n buf.length >= 8 &&\n buf[0] === 0x89 &&\n buf[1] === 0x50 &&\n buf[2] === 0x4e &&\n buf[3] === 0x47 &&\n buf[4] === 0x0d &&\n buf[5] === 0x0a &&\n buf[6] === 0x1a &&\n buf[7] === 0x0a\n ) {\n return { mimeType: 'image/png', filename: 'image.png' };\n }\n // JPEG\n if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) {\n return { mimeType: 'image/jpeg', filename: 'image.jpg' };\n }\n // GIF\n if (buf.length >= 6 && buf.subarray(0, 6).toString('ascii') === 'GIF89a') {\n return { mimeType: 'image/gif', filename: 'image.gif' };\n }\n // WEBP (RIFF....WEBP)\n if (\n buf.length >= 12 &&\n buf.subarray(0, 4).toString('ascii') === 'RIFF' &&\n buf.subarray(8, 12).toString('ascii') === 'WEBP'\n ) {\n return { mimeType: 'image/webp', filename: 'image.webp' };\n }\n // PDF\n if (buf.length >= 4 && buf.subarray(0, 4).toString('ascii') === '%PDF') {\n return { mimeType: 'application/pdf', filename: 'file.pdf' };\n }\n return { mimeType: 'application/octet-stream', filename: 'media.bin' };\n}\n\nfunction safeBasename(p: string): string {\n try {\n const b = path.basename(p);\n return b.replace(/[\\\\/:*?\"<>|]/g, '_');\n } catch {\n return '';\n }\n}\n\n"],"mappings":"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"}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { resolveFeishuAccount } from "../state/accounts.js";
|
|
2
|
+
import { createFeishuClient } from "../transport/client/client.js";
|
|
3
|
+
import { getFeishuBindingByMessageId, recordFeishuMessageBinding } from "../state/message-bindings.js";
|
|
4
|
+
import { formatFeishuOutboundText } from "../format.js";
|
|
5
|
+
import { loadMediaForFeishu } from "./media-load.js";
|
|
6
|
+
//#region extensions/feishu/src/outbound/outbound-adapter.ts
|
|
7
|
+
function createFeishuOutboundAdapter() {
|
|
8
|
+
return {
|
|
9
|
+
deliveryMode: "direct",
|
|
10
|
+
chunkerMode: "text",
|
|
11
|
+
textChunkLimit: 4e3,
|
|
12
|
+
async sendText(ctx) {
|
|
13
|
+
const cfg = ctx.cfg;
|
|
14
|
+
const account = resolveFeishuAccount(cfg, ctx.accountId ?? "default");
|
|
15
|
+
if (!account.configured) return {
|
|
16
|
+
success: false,
|
|
17
|
+
messageId: "",
|
|
18
|
+
chatId: ctx.to,
|
|
19
|
+
error: "Feishu account is not configured"
|
|
20
|
+
};
|
|
21
|
+
const { api } = createFeishuClient(account);
|
|
22
|
+
const to = ctx.to;
|
|
23
|
+
const raw = ctx.text ?? "";
|
|
24
|
+
const renderMode = account.renderMode ?? "auto";
|
|
25
|
+
const preferCard = renderMode === "card" || renderMode === "auto";
|
|
26
|
+
const cardBody = formatFeishuOutboundText({
|
|
27
|
+
text: raw,
|
|
28
|
+
renderMode,
|
|
29
|
+
forCardMarkdown: true
|
|
30
|
+
});
|
|
31
|
+
const plainBody = formatFeishuOutboundText({
|
|
32
|
+
text: raw,
|
|
33
|
+
renderMode,
|
|
34
|
+
forCardMarkdown: false
|
|
35
|
+
});
|
|
36
|
+
const receive_id_type = isProbablyOpenId(to) ? "open_id" : "chat_id";
|
|
37
|
+
const payloadText = {
|
|
38
|
+
msg_type: "text",
|
|
39
|
+
content: JSON.stringify({ text: plainBody })
|
|
40
|
+
};
|
|
41
|
+
const payloadCard = {
|
|
42
|
+
msg_type: "interactive",
|
|
43
|
+
content: JSON.stringify({
|
|
44
|
+
schema: "2.0",
|
|
45
|
+
config: { update_multi: true },
|
|
46
|
+
body: { elements: [{
|
|
47
|
+
tag: "markdown",
|
|
48
|
+
element_id: "md_1",
|
|
49
|
+
content: cardBody
|
|
50
|
+
}] }
|
|
51
|
+
})
|
|
52
|
+
};
|
|
53
|
+
const send = async (useCard) => {
|
|
54
|
+
const p = useCard ? payloadCard : payloadText;
|
|
55
|
+
return ctx.replyToId ? await api.im.message.reply({
|
|
56
|
+
path: { message_id: ctx.replyToId },
|
|
57
|
+
data: {
|
|
58
|
+
...p,
|
|
59
|
+
...ctx.threadId ? { reply_in_thread: true } : {}
|
|
60
|
+
}
|
|
61
|
+
}) : await api.im.message.create({
|
|
62
|
+
params: { receive_id_type },
|
|
63
|
+
data: {
|
|
64
|
+
receive_id: to,
|
|
65
|
+
...p
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
const res = preferCard ? await send(true).catch(async () => await send(false)) : await send(false);
|
|
70
|
+
const messageId = res?.data?.message_id ?? res?.message_id ?? "";
|
|
71
|
+
if (messageId && ctx.replyToId) {
|
|
72
|
+
const parent = getFeishuBindingByMessageId(ctx.replyToId);
|
|
73
|
+
if (parent) recordFeishuMessageBinding({
|
|
74
|
+
...parent,
|
|
75
|
+
messageId
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
success: true,
|
|
80
|
+
messageId,
|
|
81
|
+
chatId: to
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
async sendMedia(ctx) {
|
|
85
|
+
const cfg = ctx.cfg;
|
|
86
|
+
const account = resolveFeishuAccount(cfg, ctx.accountId ?? "default");
|
|
87
|
+
if (!account.configured) return {
|
|
88
|
+
success: false,
|
|
89
|
+
messageId: "",
|
|
90
|
+
chatId: ctx.to,
|
|
91
|
+
error: "Feishu account is not configured"
|
|
92
|
+
};
|
|
93
|
+
if (!ctx.mediaUrl?.trim()) return {
|
|
94
|
+
success: false,
|
|
95
|
+
messageId: "",
|
|
96
|
+
chatId: ctx.to,
|
|
97
|
+
error: "Feishu sendMedia requires mediaUrl"
|
|
98
|
+
};
|
|
99
|
+
const loaded = await loadMediaForFeishu(cfg, ctx.mediaUrl, {
|
|
100
|
+
maxBytes: 20 * 1024 * 1024,
|
|
101
|
+
localRoots: ctx.mediaLocalRoots
|
|
102
|
+
});
|
|
103
|
+
const { api } = createFeishuClient(account);
|
|
104
|
+
if (loaded.mimeType.startsWith("image/")) {
|
|
105
|
+
const up = await api.im.v1.image.create({ data: {
|
|
106
|
+
image_type: "message",
|
|
107
|
+
image: loaded.stream
|
|
108
|
+
} });
|
|
109
|
+
const imageKey = up?.data?.image_key ?? up?.image_key;
|
|
110
|
+
if (!imageKey) return {
|
|
111
|
+
success: false,
|
|
112
|
+
messageId: "",
|
|
113
|
+
chatId: ctx.to,
|
|
114
|
+
error: "Feishu image upload failed"
|
|
115
|
+
};
|
|
116
|
+
const res = ctx.replyToId ? await api.im.message.reply({
|
|
117
|
+
path: { message_id: ctx.replyToId },
|
|
118
|
+
data: {
|
|
119
|
+
msg_type: "image",
|
|
120
|
+
content: JSON.stringify({ image_key: imageKey }),
|
|
121
|
+
...ctx.threadId ? { reply_in_thread: true } : {}
|
|
122
|
+
}
|
|
123
|
+
}) : await api.im.message.create({
|
|
124
|
+
params: { receive_id_type: isProbablyOpenId(ctx.to) ? "open_id" : "chat_id" },
|
|
125
|
+
data: {
|
|
126
|
+
receive_id: ctx.to,
|
|
127
|
+
msg_type: "image",
|
|
128
|
+
content: JSON.stringify({ image_key: imageKey })
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
const messageId = res?.data?.message_id ?? res?.message_id ?? "";
|
|
132
|
+
if (messageId && ctx.replyToId) {
|
|
133
|
+
const parent = getFeishuBindingByMessageId(ctx.replyToId);
|
|
134
|
+
if (parent) recordFeishuMessageBinding({
|
|
135
|
+
...parent,
|
|
136
|
+
messageId
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
success: true,
|
|
141
|
+
messageId,
|
|
142
|
+
chatId: ctx.to
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const fileType = guessFileType(loaded.filename);
|
|
146
|
+
const up = await api.im.v1.file.create({ data: {
|
|
147
|
+
file_type: fileType,
|
|
148
|
+
file_name: loaded.filename,
|
|
149
|
+
file: loaded.stream
|
|
150
|
+
} });
|
|
151
|
+
const fileKey = up?.data?.file_key ?? up?.file_key;
|
|
152
|
+
if (!fileKey) return {
|
|
153
|
+
success: false,
|
|
154
|
+
messageId: "",
|
|
155
|
+
chatId: ctx.to,
|
|
156
|
+
error: "Feishu file upload failed"
|
|
157
|
+
};
|
|
158
|
+
const res = ctx.replyToId ? await api.im.message.reply({
|
|
159
|
+
path: { message_id: ctx.replyToId },
|
|
160
|
+
data: {
|
|
161
|
+
msg_type: "file",
|
|
162
|
+
content: JSON.stringify({ file_key: fileKey }),
|
|
163
|
+
...ctx.threadId ? { reply_in_thread: true } : {}
|
|
164
|
+
}
|
|
165
|
+
}) : await api.im.message.create({
|
|
166
|
+
params: { receive_id_type: isProbablyOpenId(ctx.to) ? "open_id" : "chat_id" },
|
|
167
|
+
data: {
|
|
168
|
+
receive_id: ctx.to,
|
|
169
|
+
msg_type: "file",
|
|
170
|
+
content: JSON.stringify({ file_key: fileKey })
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
const messageId = res?.data?.message_id ?? res?.message_id ?? "";
|
|
174
|
+
if (messageId && ctx.replyToId) {
|
|
175
|
+
const parent = getFeishuBindingByMessageId(ctx.replyToId);
|
|
176
|
+
if (parent) recordFeishuMessageBinding({
|
|
177
|
+
...parent,
|
|
178
|
+
messageId
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
success: true,
|
|
183
|
+
messageId,
|
|
184
|
+
chatId: ctx.to
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function isProbablyOpenId(to) {
|
|
190
|
+
const t = to.trim();
|
|
191
|
+
return t.startsWith("ou_") || t.startsWith("on_") || t.startsWith("open_id:") || t.startsWith("user:");
|
|
192
|
+
}
|
|
193
|
+
function guessFileType(filename) {
|
|
194
|
+
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
|
|
195
|
+
if (!ext) return "stream";
|
|
196
|
+
return ext;
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
export { createFeishuOutboundAdapter };
|
|
200
|
+
|
|
201
|
+
//# sourceMappingURL=outbound-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outbound-adapter.js","names":[],"sources":["../../../../../extensions/feishu/src/outbound/outbound-adapter.ts"],"sourcesContent":["import type {\n ChannelOutboundAdapter,\n ChannelOutboundContext,\n OutboundDeliveryResult,\n} from '@xopcai/xopc/channels/plugin-types.js';\nimport type { Config } from '@xopcai/xopc/config/schema.js';\n\nimport { resolveFeishuAccount } from '../state/accounts.js';\nimport { createFeishuClient } from '../transport/client/client.js';\nimport { formatFeishuOutboundText } from '../format.js';\nimport { loadMediaForFeishu } from './media-load.js';\nimport { getFeishuBindingByMessageId, recordFeishuMessageBinding } from '../state/message-bindings.js';\n\nexport function createFeishuOutboundAdapter(): ChannelOutboundAdapter {\n return {\n deliveryMode: 'direct',\n chunkerMode: 'text',\n textChunkLimit: 4000,\n\n async sendText(ctx: ChannelOutboundContext): Promise<OutboundDeliveryResult> {\n const cfg = ctx.cfg as Config;\n const account = resolveFeishuAccount(cfg, ctx.accountId ?? 'default');\n if (!account.configured) {\n return { success: false, messageId: '', chatId: ctx.to, error: 'Feishu account is not configured' };\n }\n\n const { api } = createFeishuClient(account);\n const to = ctx.to;\n const raw = ctx.text ?? '';\n const renderMode = account.renderMode ?? 'auto';\n const preferCard = renderMode === 'card' || renderMode === 'auto';\n\n const cardBody = formatFeishuOutboundText({\n text: raw,\n renderMode,\n forCardMarkdown: true,\n });\n const plainBody = formatFeishuOutboundText({\n text: raw,\n renderMode,\n forCardMarkdown: false,\n });\n\n const receive_id_type = isProbablyOpenId(to) ? 'open_id' : 'chat_id';\n const payloadText = {\n msg_type: 'text',\n content: JSON.stringify({ text: plainBody }),\n };\n const payloadCard = {\n msg_type: 'interactive',\n content: JSON.stringify({\n schema: '2.0',\n config: { update_multi: true },\n body: {\n elements: [\n {\n tag: 'markdown',\n element_id: 'md_1',\n content: cardBody,\n },\n ],\n },\n }),\n };\n\n const send = async (useCard: boolean) => {\n const p = useCard ? payloadCard : payloadText;\n return ctx.replyToId\n ? await (api as any).im.message.reply({\n path: { message_id: ctx.replyToId },\n data: {\n ...p,\n ...(ctx.threadId ? { reply_in_thread: true } : {}),\n },\n })\n : await (api as any).im.message.create({\n params: { receive_id_type },\n data: {\n receive_id: to,\n ...p,\n },\n });\n };\n\n const res = preferCard\n ? await send(true).catch(async () => await send(false))\n : await send(false);\n\n const messageId = res?.data?.message_id ?? res?.message_id ?? '';\n if (messageId && ctx.replyToId) {\n const parent = getFeishuBindingByMessageId(ctx.replyToId);\n if (parent) {\n recordFeishuMessageBinding({\n ...parent,\n messageId,\n });\n }\n }\n return { success: true, messageId, chatId: to };\n },\n\n async sendMedia(ctx: ChannelOutboundContext): Promise<OutboundDeliveryResult> {\n const cfg = ctx.cfg as Config;\n const account = resolveFeishuAccount(cfg, ctx.accountId ?? 'default');\n if (!account.configured) {\n return { success: false, messageId: '', chatId: ctx.to, error: 'Feishu account is not configured' };\n }\n if (!ctx.mediaUrl?.trim()) {\n return { success: false, messageId: '', chatId: ctx.to, error: 'Feishu sendMedia requires mediaUrl' };\n }\n\n const loaded = await loadMediaForFeishu(cfg, ctx.mediaUrl, {\n maxBytes: 20 * 1024 * 1024,\n localRoots: ctx.mediaLocalRoots,\n });\n\n const { api } = createFeishuClient(account);\n\n const isImage = loaded.mimeType.startsWith('image/');\n if (isImage) {\n const up = await (api as any).im.v1.image.create({\n data: {\n image_type: 'message',\n image: loaded.stream,\n },\n });\n const imageKey = up?.data?.image_key ?? up?.image_key;\n if (!imageKey) {\n return { success: false, messageId: '', chatId: ctx.to, error: 'Feishu image upload failed' };\n }\n\n const res = ctx.replyToId\n ? await (api as any).im.message.reply({\n path: { message_id: ctx.replyToId },\n data: {\n msg_type: 'image',\n content: JSON.stringify({ image_key: imageKey }),\n ...(ctx.threadId ? { reply_in_thread: true } : {}),\n },\n })\n : await (api as any).im.message.create({\n params: { receive_id_type: isProbablyOpenId(ctx.to) ? 'open_id' : 'chat_id' },\n data: {\n receive_id: ctx.to,\n msg_type: 'image',\n content: JSON.stringify({ image_key: imageKey }),\n },\n });\n const messageId = res?.data?.message_id ?? res?.message_id ?? '';\n if (messageId && ctx.replyToId) {\n const parent = getFeishuBindingByMessageId(ctx.replyToId);\n if (parent) {\n recordFeishuMessageBinding({ ...parent, messageId });\n }\n }\n return { success: true, messageId, chatId: ctx.to };\n }\n\n const fileType = guessFileType(loaded.filename);\n const up = await (api as any).im.v1.file.create({\n data: {\n file_type: fileType,\n file_name: loaded.filename,\n file: loaded.stream,\n },\n });\n const fileKey = up?.data?.file_key ?? up?.file_key;\n if (!fileKey) {\n return { success: false, messageId: '', chatId: ctx.to, error: 'Feishu file upload failed' };\n }\n\n const res = ctx.replyToId\n ? await (api as any).im.message.reply({\n path: { message_id: ctx.replyToId },\n data: {\n msg_type: 'file',\n content: JSON.stringify({ file_key: fileKey }),\n ...(ctx.threadId ? { reply_in_thread: true } : {}),\n },\n })\n : await (api as any).im.message.create({\n params: { receive_id_type: isProbablyOpenId(ctx.to) ? 'open_id' : 'chat_id' },\n data: {\n receive_id: ctx.to,\n msg_type: 'file',\n content: JSON.stringify({ file_key: fileKey }),\n },\n });\n\n const messageId = res?.data?.message_id ?? res?.message_id ?? '';\n if (messageId && ctx.replyToId) {\n const parent = getFeishuBindingByMessageId(ctx.replyToId);\n if (parent) {\n recordFeishuMessageBinding({ ...parent, messageId });\n }\n }\n return { success: true, messageId, chatId: ctx.to };\n },\n };\n}\n\nfunction isProbablyOpenId(to: string): boolean {\n const t = to.trim();\n return t.startsWith('ou_') || t.startsWith('on_') || t.startsWith('open_id:') || t.startsWith('user:');\n}\n\nfunction guessFileType(filename: string): string {\n const ext = filename.split('.').pop()?.toLowerCase() ?? '';\n if (!ext) return 'stream';\n return ext;\n}\n\n"],"mappings":"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"}
|