ylib-syim 0.0.6 → 0.0.8
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/bridges/logger.ts +117 -7
- package/bridges/main.ts +26 -9
- package/package.json +6 -5
package/bridges/logger.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { format } from "node:util";
|
|
4
|
+
import type { WriteStream } from "node:fs";
|
|
4
5
|
|
|
5
6
|
type ConsoleMethod = (...args: unknown[]) => void;
|
|
6
7
|
|
|
@@ -27,21 +28,130 @@ function patchConsole(writeLine: (line: string) => void): void {
|
|
|
27
28
|
const original = console[method] as ConsoleMethod;
|
|
28
29
|
console[method] = (...args: unknown[]) => {
|
|
29
30
|
const line = format(...args);
|
|
30
|
-
|
|
31
|
+
try {
|
|
32
|
+
writeLine(`[${new Date().toISOString()}] [${method}] ${line}`);
|
|
33
|
+
} catch {
|
|
34
|
+
// logger 必须永不影响主进程
|
|
35
|
+
}
|
|
31
36
|
original(...args);
|
|
32
37
|
};
|
|
33
38
|
}
|
|
34
39
|
}
|
|
35
40
|
|
|
41
|
+
function parsePositiveInt(value: unknown, fallback: number): number {
|
|
42
|
+
const n = Number(String(value ?? "").trim());
|
|
43
|
+
if (!Number.isFinite(n)) return fallback;
|
|
44
|
+
if (n <= 0) return fallback;
|
|
45
|
+
return Math.floor(n);
|
|
46
|
+
}
|
|
47
|
+
|
|
36
48
|
export function setupBridgeLogger(prefix: string): string {
|
|
37
49
|
const exists = globalThis.__imAgentHubBridgeLogger;
|
|
38
50
|
if (exists?.enabled) return exists.logFilePath;
|
|
39
51
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
52
|
+
try {
|
|
53
|
+
const logFilePath = buildLogFilePath(prefix);
|
|
54
|
+
const maxLines = parsePositiveInt(
|
|
55
|
+
process.env.IM_AGENT_HUB_LOG_MAX_LINES,
|
|
56
|
+
20000,
|
|
57
|
+
);
|
|
58
|
+
const trimBatch = parsePositiveInt(
|
|
59
|
+
process.env.IM_AGENT_HUB_LOG_TRIM_BATCH,
|
|
60
|
+
1000,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
let stream: WriteStream = fs.createWriteStream(logFilePath, { flags: "a" });
|
|
64
|
+
stream.on("error", () => {
|
|
65
|
+
// logger 必须永不影响主进程;出现 stream 错误后,后续写入直接丢弃
|
|
66
|
+
});
|
|
67
|
+
let streamBroken = false;
|
|
68
|
+
stream.on("error", () => {
|
|
69
|
+
streamBroken = true;
|
|
70
|
+
});
|
|
71
|
+
let writtenLines = 0;
|
|
72
|
+
let trimming = false;
|
|
73
|
+
let paused = false;
|
|
74
|
+
const buffer: string[] = [];
|
|
75
|
+
|
|
76
|
+
function writeRaw(lineWithNewline: string): void {
|
|
77
|
+
try {
|
|
78
|
+
if (streamBroken) return;
|
|
79
|
+
if (paused) {
|
|
80
|
+
buffer.push(lineWithNewline);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
stream.write(lineWithNewline);
|
|
84
|
+
} catch {
|
|
85
|
+
// swallow
|
|
86
|
+
}
|
|
87
|
+
}
|
|
43
88
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
89
|
+
async function trimToLastLines(): Promise<void> {
|
|
90
|
+
if (trimming) return;
|
|
91
|
+
trimming = true;
|
|
92
|
+
paused = true;
|
|
93
|
+
try {
|
|
94
|
+
await new Promise<void>((resolve) => {
|
|
95
|
+
try {
|
|
96
|
+
stream.end(() => resolve());
|
|
97
|
+
} catch {
|
|
98
|
+
resolve();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
const text = fs.readFileSync(logFilePath, "utf-8");
|
|
102
|
+
const lines = text.split("\n");
|
|
103
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
104
|
+
const kept = lines.slice(-maxLines);
|
|
105
|
+
fs.writeFileSync(
|
|
106
|
+
logFilePath,
|
|
107
|
+
kept.join("\n") + (kept.length ? "\n" : ""),
|
|
108
|
+
"utf-8",
|
|
109
|
+
);
|
|
110
|
+
stream = fs.createWriteStream(logFilePath, { flags: "a" });
|
|
111
|
+
streamBroken = false;
|
|
112
|
+
stream.on("error", () => {
|
|
113
|
+
streamBroken = true;
|
|
114
|
+
});
|
|
115
|
+
for (const item of buffer) {
|
|
116
|
+
try {
|
|
117
|
+
if (streamBroken) break;
|
|
118
|
+
stream.write(item);
|
|
119
|
+
} catch {
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
buffer.length = 0;
|
|
124
|
+
writtenLines = Math.min(maxLines, kept.length);
|
|
125
|
+
} catch {
|
|
126
|
+
try {
|
|
127
|
+
stream = fs.createWriteStream(logFilePath, { flags: "a" });
|
|
128
|
+
streamBroken = false;
|
|
129
|
+
stream.on("error", () => {
|
|
130
|
+
streamBroken = true;
|
|
131
|
+
});
|
|
132
|
+
} catch {
|
|
133
|
+
streamBroken = true;
|
|
134
|
+
}
|
|
135
|
+
} finally {
|
|
136
|
+
paused = false;
|
|
137
|
+
trimming = false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
writeRaw(`[${new Date().toISOString()}] [system] bridge logger started\n`);
|
|
142
|
+
writtenLines += 1;
|
|
143
|
+
|
|
144
|
+
patchConsole((line) => {
|
|
145
|
+
writeRaw(`${line}\n`);
|
|
146
|
+
writtenLines += 1;
|
|
147
|
+
if (maxLines > 0 && writtenLines > maxLines + trimBatch && !trimming) {
|
|
148
|
+
void trimToLastLines();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
globalThis.__imAgentHubBridgeLogger = { enabled: true, logFilePath };
|
|
152
|
+
return logFilePath;
|
|
153
|
+
} catch {
|
|
154
|
+
globalThis.__imAgentHubBridgeLogger = { enabled: false, logFilePath: "" };
|
|
155
|
+
return "";
|
|
156
|
+
}
|
|
47
157
|
}
|
package/bridges/main.ts
CHANGED
|
@@ -28,6 +28,11 @@ const runtimeConfigTenantId = (
|
|
|
28
28
|
const allowLocalRuntimeConfig =
|
|
29
29
|
process.env.ALLOW_LOCAL_RUNTIME_CONFIG === "1" ||
|
|
30
30
|
process.env.NODE_ENV === "development";
|
|
31
|
+
// 调试用:强制只读本地配置文件,即使设置了 RUNTIME_CONFIG_PULL_URL 也跳过远端拉取。
|
|
32
|
+
// 设置方式:LOCAL_CONFIG_ONLY=1 或命令行参数 --local-config-only
|
|
33
|
+
const localConfigOnly =
|
|
34
|
+
process.env.LOCAL_CONFIG_ONLY === "1" ||
|
|
35
|
+
process.argv.includes("--local-config-only");
|
|
31
36
|
const RUNTIME_ERROR_LOG_FALLBACK_LINES_DEFAULT = 10;
|
|
32
37
|
const runtimeErrorLogFallbackLines = Math.max(
|
|
33
38
|
1,
|
|
@@ -971,14 +976,18 @@ async function pullRuntimeConfigFromPython(forceFull = false): Promise<{
|
|
|
971
976
|
function startRuntimeConfigPollLoop(): void {
|
|
972
977
|
if (!runtimeConfigPullUrl) return;
|
|
973
978
|
if (runtimeConfigPollIntervalMs <= 0) return;
|
|
979
|
+
let recoveryHandled = false;
|
|
974
980
|
const timer = setInterval(() => {
|
|
975
981
|
void pullRuntimeConfigFromPython(false).then((result) => {
|
|
976
|
-
if (result.ok)
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
+
if (!result.ok) return;
|
|
983
|
+
if (recoveryHandled) return;
|
|
984
|
+
recoveryHandled = true;
|
|
985
|
+
console.log(
|
|
986
|
+
"[bridges/main] poll pull recovered success, stop polling and exit for hard restart",
|
|
987
|
+
);
|
|
988
|
+
clearInterval(timer);
|
|
989
|
+
// 配置源(Python)恢复后硬重启:退出进程,由 PM2/系统守护重新拉起,下次启动走正常拉配置与起 bridge 流程。
|
|
990
|
+
setTimeout(() => process.exit(0), 100);
|
|
982
991
|
});
|
|
983
992
|
}, runtimeConfigPollIntervalMs);
|
|
984
993
|
}
|
|
@@ -1084,6 +1093,7 @@ function readEffectiveWhitelistFromConfig(
|
|
|
1084
1093
|
"groupSessionScope",
|
|
1085
1094
|
"streaming",
|
|
1086
1095
|
"uploadHost",
|
|
1096
|
+
// "yuceImageCookieStrictOrigin",
|
|
1087
1097
|
],
|
|
1088
1098
|
},
|
|
1089
1099
|
"dingtalk-connector": {
|
|
@@ -1099,6 +1109,7 @@ function readEffectiveWhitelistFromConfig(
|
|
|
1099
1109
|
"gatewayPort",
|
|
1100
1110
|
"gatewayBaseUrl",
|
|
1101
1111
|
"uploadHost",
|
|
1112
|
+
// "yuceImageCookieStrictOrigin",
|
|
1102
1113
|
],
|
|
1103
1114
|
},
|
|
1104
1115
|
};
|
|
@@ -1929,10 +1940,10 @@ function loadOpenClawConfig(): {
|
|
|
1929
1940
|
configPath: string;
|
|
1930
1941
|
config: Record<string, unknown>;
|
|
1931
1942
|
} | null {
|
|
1932
|
-
if (runtimeConfigPullUrl) {
|
|
1943
|
+
if (runtimeConfigPullUrl && !localConfigOnly) {
|
|
1933
1944
|
return null;
|
|
1934
1945
|
}
|
|
1935
|
-
if (!allowLocalRuntimeConfig) {
|
|
1946
|
+
if (!allowLocalRuntimeConfig && !localConfigOnly) {
|
|
1936
1947
|
return null;
|
|
1937
1948
|
}
|
|
1938
1949
|
const configPaths = [
|
|
@@ -2002,6 +2013,9 @@ function printConfigBootstrapGuide(): void {
|
|
|
2002
2013
|
);
|
|
2003
2014
|
console.log("- channels.feishu.accounts.<id>.streaming: 是否流式回复。");
|
|
2004
2015
|
console.log("- channels.feishu.accounts.<id>.uploadHost: 上传服务地址。");
|
|
2016
|
+
// console.log(
|
|
2017
|
+
// "- channels.feishu.accounts.<id>.yuceImageCookieStrictOrigin: 可选,默认 false;true 时仅与 uploadHost 同源的 Markdown 图片拉取带 YCSESSIONID。",
|
|
2018
|
+
// );
|
|
2005
2019
|
console.log("- channels.dingtalk-connector.enabled: 是否启用钉钉渠道。");
|
|
2006
2020
|
console.log(
|
|
2007
2021
|
"- channels.dingtalk-connector.accounts.<id>.clientId/clientSecret: 钉钉应用凭据。",
|
|
@@ -2021,6 +2035,9 @@ function printConfigBootstrapGuide(): void {
|
|
|
2021
2035
|
console.log(
|
|
2022
2036
|
"- channels.dingtalk-connector.accounts.<id>.uploadHost: 上传服务地址。",
|
|
2023
2037
|
);
|
|
2038
|
+
// console.log(
|
|
2039
|
+
// "- channels.dingtalk-connector.accounts.<id>.yuceImageCookieStrictOrigin: 可选,默认 false;true 时仅与 uploadHost 同源的 Markdown 图片拉取带 YCSESSIONID。",
|
|
2040
|
+
// );
|
|
2024
2041
|
console.log("- bindings: 渠道路由规则。");
|
|
2025
2042
|
console.log("- bindings[].agentId: 目标 Agent ID。");
|
|
2026
2043
|
console.log("- bindings[].match.channel: 命中的渠道。");
|
|
@@ -2077,7 +2094,7 @@ async function main(): Promise<void> {
|
|
|
2077
2094
|
markConfiguredBotsAsConnecting(loaded.config);
|
|
2078
2095
|
}
|
|
2079
2096
|
|
|
2080
|
-
if (runtimeConfigPullUrl) {
|
|
2097
|
+
if (runtimeConfigPullUrl && !localConfigOnly) {
|
|
2081
2098
|
// 启动前必须先拉到配置,确保插件启动时读到的是最新本地文件。
|
|
2082
2099
|
const pullResult = await pullRuntimeConfigFromPython(false);
|
|
2083
2100
|
if (!pullResult.ok) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ylib-syim",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "多 IM / 多 Agent 的会话路由与上下文管理(支持 /new)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -31,17 +31,18 @@
|
|
|
31
31
|
"start:connector": "tsx src/connector-host.ts",
|
|
32
32
|
"start:stdio-bridge": "tsx bridges/dingtalk-stdio-bridge.ts",
|
|
33
33
|
"start:lark-bridge": "tsx bridges/lark-stdio-bridge.ts",
|
|
34
|
-
"start:bridges": "tsx bridges/main.ts",
|
|
34
|
+
"start:bridges": "tsx --inspect bridges/main.ts",
|
|
35
35
|
"start:feishu-bridge": "tsx scripts/feishu-yuce-bridge.ts",
|
|
36
36
|
"build:bridge": "node scripts/build-bridge.mjs",
|
|
37
37
|
"build:bridge:all": "node scripts/build-bridge.mjs --all",
|
|
38
38
|
"build:bridge:debug": "node scripts/build-bridge.mjs --no-minify",
|
|
39
|
-
"start:stdio-bridge:bundle": "node dist/dingtalk-stdio-bridge.cjs"
|
|
39
|
+
"start:stdio-bridge:bundle": "node dist/dingtalk-stdio-bridge.cjs",
|
|
40
|
+
"local": "export LOCAL_CONFIG_ONLY=1 && npm run start:bridges"
|
|
40
41
|
},
|
|
41
42
|
"dependencies": {
|
|
42
43
|
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
|
43
|
-
"ylib-dingtalk-connector": "0.7.10-beata.
|
|
44
|
-
"ylib-openclaw-lark": "2026.3.17-beata.
|
|
44
|
+
"ylib-dingtalk-connector": "0.7.10-beata.3",
|
|
45
|
+
"ylib-openclaw-lark": "2026.3.17-beata.7",
|
|
45
46
|
"axios": "^1.6.0",
|
|
46
47
|
"dingtalk-stream": "^2.1.4",
|
|
47
48
|
"fluent-ffmpeg": "^2.1.3",
|