ylib-syim 0.0.32 → 0.0.34
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 +4 -4
- package/scripts/lark-stdio-bridge.ts +178 -0
- package/scripts/weixin-stdio-bridge.ts +161 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ylib-syim",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.34",
|
|
4
4
|
"description": "多 IM / 多 Agent 的会话路由与上下文管理(支持 /new)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
|
49
|
-
"ylib-dingtalk-connector": "0.7.10-beta.
|
|
50
|
-
"ylib-openclaw-lark": "2026.3.17-beta.
|
|
51
|
-
"ylib-openclaw-weixin": "2.1.7-beta.
|
|
49
|
+
"ylib-dingtalk-connector": "0.7.10-beta.20",
|
|
50
|
+
"ylib-openclaw-lark": "2026.3.17-beta.25",
|
|
51
|
+
"ylib-openclaw-weixin": "2.1.7-beta.12",
|
|
52
52
|
"axios": "^1.6.0",
|
|
53
53
|
"dingtalk-stream": "^2.1.4",
|
|
54
54
|
"fluent-ffmpeg": "^2.1.3",
|
|
@@ -513,6 +513,180 @@ function buildCfg(): {
|
|
|
513
513
|
// ---------------------------------------------------------------------------
|
|
514
514
|
|
|
515
515
|
function buildMinimalRuntime(cfg: Record<string, unknown>): unknown {
|
|
516
|
+
type BridgeGroupConfig = {
|
|
517
|
+
requireMention?: boolean;
|
|
518
|
+
};
|
|
519
|
+
type BridgeGroups = Record<string, BridgeGroupConfig | undefined>;
|
|
520
|
+
|
|
521
|
+
const normalizeAccountId = (value: unknown): string => {
|
|
522
|
+
const text = String(value || "").trim();
|
|
523
|
+
return text || "__default__";
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const resolveAccountEntry = (
|
|
527
|
+
accounts: Record<string, unknown> | undefined,
|
|
528
|
+
accountId: unknown,
|
|
529
|
+
): Record<string, unknown> | undefined => {
|
|
530
|
+
if (!accounts || typeof accounts !== "object") return undefined;
|
|
531
|
+
const normalized = normalizeAccountId(accountId);
|
|
532
|
+
const exact = accounts[normalized];
|
|
533
|
+
if (exact && typeof exact === "object" && !Array.isArray(exact)) {
|
|
534
|
+
return exact as Record<string, unknown>;
|
|
535
|
+
}
|
|
536
|
+
const fallback = accounts["__default__"];
|
|
537
|
+
if (fallback && typeof fallback === "object" && !Array.isArray(fallback)) {
|
|
538
|
+
return fallback as Record<string, unknown>;
|
|
539
|
+
}
|
|
540
|
+
return undefined;
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
const resolveChannelGroups = (
|
|
544
|
+
inCfg: Record<string, unknown>,
|
|
545
|
+
channel: string,
|
|
546
|
+
accountId?: unknown,
|
|
547
|
+
): BridgeGroups | undefined => {
|
|
548
|
+
const channelsObj =
|
|
549
|
+
inCfg.channels && typeof inCfg.channels === "object"
|
|
550
|
+
? (inCfg.channels as Record<string, unknown>)
|
|
551
|
+
: undefined;
|
|
552
|
+
const channelCfg =
|
|
553
|
+
channelsObj && channelsObj[channel] && typeof channelsObj[channel] === "object"
|
|
554
|
+
? (channelsObj[channel] as Record<string, unknown>)
|
|
555
|
+
: undefined;
|
|
556
|
+
if (!channelCfg) return undefined;
|
|
557
|
+
const accountsObj =
|
|
558
|
+
channelCfg.accounts && typeof channelCfg.accounts === "object"
|
|
559
|
+
? (channelCfg.accounts as Record<string, unknown>)
|
|
560
|
+
: undefined;
|
|
561
|
+
const accountEntry = resolveAccountEntry(accountsObj, accountId);
|
|
562
|
+
const accountGroups =
|
|
563
|
+
accountEntry && accountEntry.groups && typeof accountEntry.groups === "object"
|
|
564
|
+
? (accountEntry.groups as BridgeGroups)
|
|
565
|
+
: undefined;
|
|
566
|
+
if (accountGroups) return accountGroups;
|
|
567
|
+
if (channelCfg.groups && typeof channelCfg.groups === "object") {
|
|
568
|
+
return channelCfg.groups as BridgeGroups;
|
|
569
|
+
}
|
|
570
|
+
return undefined;
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
const resolveChannelGroupPolicyMode = (
|
|
574
|
+
inCfg: Record<string, unknown>,
|
|
575
|
+
channel: string,
|
|
576
|
+
accountId?: unknown,
|
|
577
|
+
): "open" | "allowlist" | "disabled" | undefined => {
|
|
578
|
+
const channelsObj =
|
|
579
|
+
inCfg.channels && typeof inCfg.channels === "object"
|
|
580
|
+
? (inCfg.channels as Record<string, unknown>)
|
|
581
|
+
: undefined;
|
|
582
|
+
const channelCfg =
|
|
583
|
+
channelsObj && channelsObj[channel] && typeof channelsObj[channel] === "object"
|
|
584
|
+
? (channelsObj[channel] as Record<string, unknown>)
|
|
585
|
+
: undefined;
|
|
586
|
+
if (!channelCfg) return undefined;
|
|
587
|
+
const accountsObj =
|
|
588
|
+
channelCfg.accounts && typeof channelCfg.accounts === "object"
|
|
589
|
+
? (channelCfg.accounts as Record<string, unknown>)
|
|
590
|
+
: undefined;
|
|
591
|
+
const accountEntry = resolveAccountEntry(accountsObj, accountId);
|
|
592
|
+
const accountPolicy = String(accountEntry?.groupPolicy || "").trim();
|
|
593
|
+
if (accountPolicy === "open" || accountPolicy === "allowlist" || accountPolicy === "disabled") {
|
|
594
|
+
return accountPolicy;
|
|
595
|
+
}
|
|
596
|
+
const topPolicy = String(channelCfg.groupPolicy || "").trim();
|
|
597
|
+
if (topPolicy === "open" || topPolicy === "allowlist" || topPolicy === "disabled") {
|
|
598
|
+
return topPolicy;
|
|
599
|
+
}
|
|
600
|
+
return undefined;
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
const resolveChannelGroupConfig = (
|
|
604
|
+
groups: BridgeGroups | undefined,
|
|
605
|
+
groupId: string,
|
|
606
|
+
caseInsensitive: boolean,
|
|
607
|
+
): BridgeGroupConfig | undefined => {
|
|
608
|
+
if (!groups) return undefined;
|
|
609
|
+
const direct = groups[groupId];
|
|
610
|
+
if (direct) return direct;
|
|
611
|
+
if (!caseInsensitive) return undefined;
|
|
612
|
+
const target = groupId.toLowerCase();
|
|
613
|
+
const matchedKey = Object.keys(groups).find(
|
|
614
|
+
(key) => key !== "*" && key.toLowerCase() === target,
|
|
615
|
+
);
|
|
616
|
+
return matchedKey ? groups[matchedKey] : undefined;
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
const resolveBridgeGroupPolicy = (params: {
|
|
620
|
+
cfg: Record<string, unknown>;
|
|
621
|
+
channel: string;
|
|
622
|
+
groupId?: string | null;
|
|
623
|
+
accountId?: string | null;
|
|
624
|
+
groupIdCaseInsensitive?: boolean;
|
|
625
|
+
hasGroupAllowFrom?: boolean;
|
|
626
|
+
}): {
|
|
627
|
+
allowlistEnabled: boolean;
|
|
628
|
+
allowed: boolean;
|
|
629
|
+
groupConfig?: BridgeGroupConfig;
|
|
630
|
+
defaultConfig?: BridgeGroupConfig;
|
|
631
|
+
} => {
|
|
632
|
+
const groups = resolveChannelGroups(params.cfg, params.channel, params.accountId);
|
|
633
|
+
const groupPolicy = resolveChannelGroupPolicyMode(
|
|
634
|
+
params.cfg,
|
|
635
|
+
params.channel,
|
|
636
|
+
params.accountId,
|
|
637
|
+
);
|
|
638
|
+
const hasGroups = Boolean(groups && Object.keys(groups).length > 0);
|
|
639
|
+
const allowlistEnabled = groupPolicy === "allowlist" || hasGroups;
|
|
640
|
+
const normalizedGroupId = String(params.groupId || "").trim();
|
|
641
|
+
const groupConfig = normalizedGroupId
|
|
642
|
+
? resolveChannelGroupConfig(groups, normalizedGroupId, Boolean(params.groupIdCaseInsensitive))
|
|
643
|
+
: undefined;
|
|
644
|
+
const defaultConfig = groups?.["*"];
|
|
645
|
+
const allowAll = allowlistEnabled && Boolean(groups && Object.hasOwn(groups, "*"));
|
|
646
|
+
const senderFilterBypass =
|
|
647
|
+
groupPolicy === "allowlist" && !hasGroups && Boolean(params.hasGroupAllowFrom);
|
|
648
|
+
const allowed =
|
|
649
|
+
groupPolicy === "disabled"
|
|
650
|
+
? false
|
|
651
|
+
: !allowlistEnabled || allowAll || Boolean(groupConfig) || senderFilterBypass;
|
|
652
|
+
return {
|
|
653
|
+
allowlistEnabled,
|
|
654
|
+
allowed,
|
|
655
|
+
groupConfig,
|
|
656
|
+
defaultConfig,
|
|
657
|
+
};
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
const resolveBridgeRequireMention = (params: {
|
|
661
|
+
cfg: Record<string, unknown>;
|
|
662
|
+
channel: string;
|
|
663
|
+
groupId?: string | null;
|
|
664
|
+
accountId?: string | null;
|
|
665
|
+
groupIdCaseInsensitive?: boolean;
|
|
666
|
+
requireMentionOverride?: boolean;
|
|
667
|
+
overrideOrder?: "before-config" | "after-config";
|
|
668
|
+
}): boolean => {
|
|
669
|
+
const { requireMentionOverride } = params;
|
|
670
|
+
const overrideOrder = params.overrideOrder || "after-config";
|
|
671
|
+
const { groupConfig, defaultConfig } = resolveBridgeGroupPolicy(params);
|
|
672
|
+
const configMention =
|
|
673
|
+
typeof groupConfig?.requireMention === "boolean"
|
|
674
|
+
? groupConfig.requireMention
|
|
675
|
+
: typeof defaultConfig?.requireMention === "boolean"
|
|
676
|
+
? defaultConfig.requireMention
|
|
677
|
+
: undefined;
|
|
678
|
+
if (overrideOrder === "before-config" && typeof requireMentionOverride === "boolean") {
|
|
679
|
+
return requireMentionOverride;
|
|
680
|
+
}
|
|
681
|
+
if (typeof configMention === "boolean") {
|
|
682
|
+
return configMention;
|
|
683
|
+
}
|
|
684
|
+
if (overrideOrder !== "before-config" && typeof requireMentionOverride === "boolean") {
|
|
685
|
+
return requireMentionOverride;
|
|
686
|
+
}
|
|
687
|
+
return true;
|
|
688
|
+
};
|
|
689
|
+
|
|
516
690
|
return {
|
|
517
691
|
log: (...args: unknown[]) => console.log("[Feishu]", ...args),
|
|
518
692
|
error: (...args: unknown[]) => console.error("[Feishu][ERR]", ...args),
|
|
@@ -531,6 +705,10 @@ function buildMinimalRuntime(cfg: Record<string, unknown>): unknown {
|
|
|
531
705
|
resolveCommandAuthorizedFromAuthorizers: async () => false,
|
|
532
706
|
isControlCommandMessage: () => false,
|
|
533
707
|
},
|
|
708
|
+
groups: {
|
|
709
|
+
resolveGroupPolicy: resolveBridgeGroupPolicy,
|
|
710
|
+
resolveRequireMention: resolveBridgeRequireMention,
|
|
711
|
+
},
|
|
534
712
|
reply: {
|
|
535
713
|
resolveEnvelopeFormatOptions: () => ({}),
|
|
536
714
|
finalizeInboundContext: () => ({}),
|
|
@@ -1,10 +1,168 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import fsPromises from "node:fs/promises";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
import { fileTypeFromBuffer } from "file-type";
|
|
4
7
|
|
|
5
8
|
const CHANNEL_KEY = "openclaw-weixin";
|
|
6
9
|
let bridgeDefaultAccountAlias = "weixin-single";
|
|
7
10
|
const bridgeAliasLogMemo = new Set<string>();
|
|
11
|
+
const MEDIA_FILE_MODE = 0o644;
|
|
12
|
+
const DEFAULT_MEDIA_MAX_BYTES = 5 * 1024 * 1024;
|
|
13
|
+
const EXT_BY_MIME: Record<string, string> = {
|
|
14
|
+
"image/heic": ".heic",
|
|
15
|
+
"image/heif": ".heif",
|
|
16
|
+
"image/jpeg": ".jpg",
|
|
17
|
+
"image/png": ".png",
|
|
18
|
+
"image/webp": ".webp",
|
|
19
|
+
"image/gif": ".gif",
|
|
20
|
+
"audio/ogg": ".ogg",
|
|
21
|
+
"audio/mpeg": ".mp3",
|
|
22
|
+
"audio/x-m4a": ".m4a",
|
|
23
|
+
"audio/mp4": ".m4a",
|
|
24
|
+
"video/mp4": ".mp4",
|
|
25
|
+
"video/quicktime": ".mov",
|
|
26
|
+
"application/pdf": ".pdf",
|
|
27
|
+
"application/json": ".json",
|
|
28
|
+
"application/zip": ".zip",
|
|
29
|
+
"application/gzip": ".gz",
|
|
30
|
+
"application/x-tar": ".tar",
|
|
31
|
+
"application/x-7z-compressed": ".7z",
|
|
32
|
+
"application/vnd.rar": ".rar",
|
|
33
|
+
"application/msword": ".doc",
|
|
34
|
+
"application/vnd.ms-excel": ".xls",
|
|
35
|
+
"application/vnd.ms-powerpoint": ".ppt",
|
|
36
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
|
37
|
+
".docx",
|
|
38
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
|
|
39
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation":
|
|
40
|
+
".pptx",
|
|
41
|
+
"text/csv": ".csv",
|
|
42
|
+
"text/plain": ".txt",
|
|
43
|
+
"text/markdown": ".md",
|
|
44
|
+
};
|
|
45
|
+
const MIME_BY_EXT: Record<string, string> = {
|
|
46
|
+
...Object.fromEntries(Object.entries(EXT_BY_MIME).map(([mime, ext]) => [ext, mime])),
|
|
47
|
+
".jpeg": "image/jpeg",
|
|
48
|
+
".js": "text/javascript",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function sanitizeFilename(name: string): string {
|
|
52
|
+
const trimmed = name.trim();
|
|
53
|
+
if (!trimmed) return "";
|
|
54
|
+
const sanitized = trimmed.replace(/[^\p{L}\p{N}._-]+/gu, "_");
|
|
55
|
+
return sanitized.replace(/_+/g, "_").replace(/^_|_$/g, "").slice(0, 60);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizeMimeType(mime?: string | null): string | undefined {
|
|
59
|
+
if (!mime) return undefined;
|
|
60
|
+
const cleaned = mime.split(";")[0]?.trim().toLowerCase();
|
|
61
|
+
return cleaned || undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function extensionForMime(mime?: string | null): string | undefined {
|
|
65
|
+
const normalized = normalizeMimeType(mime);
|
|
66
|
+
if (!normalized) return undefined;
|
|
67
|
+
return EXT_BY_MIME[normalized];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getFileExtension(filePath?: string | null): string | undefined {
|
|
71
|
+
if (!filePath) return undefined;
|
|
72
|
+
try {
|
|
73
|
+
if (/^https?:\/\//i.test(filePath)) {
|
|
74
|
+
const url = new URL(filePath);
|
|
75
|
+
return path.extname(url.pathname).toLowerCase() || undefined;
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
// fall back
|
|
79
|
+
}
|
|
80
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
81
|
+
return ext || undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isGenericMime(mime?: string): boolean {
|
|
85
|
+
if (!mime) return true;
|
|
86
|
+
const lower = mime.toLowerCase();
|
|
87
|
+
return lower === "application/octet-stream" || lower === "application/zip";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isMissingPathError(err: unknown): err is NodeJS.ErrnoException {
|
|
91
|
+
return err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function retryAfterRecreatingDir<T>(
|
|
95
|
+
dir: string,
|
|
96
|
+
run: () => Promise<T>,
|
|
97
|
+
): Promise<T> {
|
|
98
|
+
try {
|
|
99
|
+
return await run();
|
|
100
|
+
} catch (err) {
|
|
101
|
+
if (!isMissingPathError(err)) throw err;
|
|
102
|
+
await fsPromises.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
103
|
+
return await run();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function detectMime(opts: {
|
|
108
|
+
buffer?: Buffer;
|
|
109
|
+
headerMime?: string | null;
|
|
110
|
+
filePath?: string;
|
|
111
|
+
}): Promise<string | undefined> {
|
|
112
|
+
const ext = getFileExtension(opts.filePath);
|
|
113
|
+
const extMime = ext ? MIME_BY_EXT[ext] : undefined;
|
|
114
|
+
const headerMime = normalizeMimeType(opts.headerMime);
|
|
115
|
+
let sniffed: string | undefined;
|
|
116
|
+
if (opts.buffer) {
|
|
117
|
+
try {
|
|
118
|
+
const fileType = await fileTypeFromBuffer(opts.buffer);
|
|
119
|
+
sniffed = fileType?.mime ?? undefined;
|
|
120
|
+
} catch {
|
|
121
|
+
sniffed = undefined;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (sniffed && (!isGenericMime(sniffed) || !extMime)) return sniffed;
|
|
125
|
+
if (extMime) return extMime;
|
|
126
|
+
if (headerMime && !isGenericMime(headerMime)) return headerMime;
|
|
127
|
+
if (sniffed) return sniffed;
|
|
128
|
+
if (headerMime) return headerMime;
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function saveMediaBufferFromOpenclaw(
|
|
133
|
+
buffer: Buffer,
|
|
134
|
+
contentType?: string,
|
|
135
|
+
subdir = "inbound",
|
|
136
|
+
maxBytes = DEFAULT_MEDIA_MAX_BYTES,
|
|
137
|
+
originalFilename?: string,
|
|
138
|
+
): Promise<{ id: string; path: string; size: number; contentType?: string }> {
|
|
139
|
+
if (buffer.byteLength > maxBytes) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Media exceeds ${(maxBytes / (1024 * 1024)).toFixed(0)}MB limit`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const dir = path.join(os.tmpdir(), "openclaw-media", subdir);
|
|
145
|
+
await fsPromises.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
146
|
+
const uuid = crypto.randomUUID();
|
|
147
|
+
const headerExt = extensionForMime(contentType?.split(";")[0]?.trim());
|
|
148
|
+
const mime = await detectMime({ buffer, headerMime: contentType });
|
|
149
|
+
const ext = headerExt ?? extensionForMime(mime) ?? "";
|
|
150
|
+
|
|
151
|
+
let id: string;
|
|
152
|
+
if (originalFilename) {
|
|
153
|
+
const base = path.parse(originalFilename).name;
|
|
154
|
+
const sanitized = sanitizeFilename(base);
|
|
155
|
+
id = sanitized ? `${sanitized}---${uuid}${ext}` : `${uuid}${ext}`;
|
|
156
|
+
} else {
|
|
157
|
+
id = ext ? `${uuid}${ext}` : uuid;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const dest = path.join(dir, id);
|
|
161
|
+
await retryAfterRecreatingDir(dir, () =>
|
|
162
|
+
fsPromises.writeFile(dest, buffer, { mode: MEDIA_FILE_MODE }),
|
|
163
|
+
);
|
|
164
|
+
return { id, path: dest, size: buffer.byteLength, contentType: mime };
|
|
165
|
+
}
|
|
8
166
|
|
|
9
167
|
function logBridgeAlias(from: string, to: string, reason: string): void {
|
|
10
168
|
const key = `${from}->${to}:${reason}`;
|
|
@@ -503,6 +661,9 @@ function buildMinimalRuntime(
|
|
|
503
661
|
activity: {
|
|
504
662
|
record: () => {},
|
|
505
663
|
},
|
|
664
|
+
media: {
|
|
665
|
+
saveMediaBuffer: saveMediaBufferFromOpenclaw,
|
|
666
|
+
},
|
|
506
667
|
},
|
|
507
668
|
system: {
|
|
508
669
|
enqueueSystemEvent: () => {},
|