@wu529778790/open-im 1.11.10 → 1.11.11-beta.0
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/config-web-auth.d.ts +1 -1
- package/dist/config-web-auth.js +24 -20
- package/dist/config-web.js +3 -1
- package/dist/index.js +2 -2
- package/dist/logger.js +54 -19
- package/dist/platform/handle-text-flow.js +10 -12
- package/dist/sanitize.js +26 -3
- package/dist/shared/keepalive.js +3 -1
- package/dist/shared/sentry.js +3 -1
- package/package.json +9 -2
|
@@ -9,5 +9,5 @@ export declare function consumeLoginToken(loginToken: string): LoginTokenInfo |
|
|
|
9
9
|
export declare function createLoginToken(ttlMs: number): string;
|
|
10
10
|
export declare function createSession(request: IncomingMessage, ttlMs: number): string;
|
|
11
11
|
export declare function isSessionValid(request: IncomingMessage): boolean;
|
|
12
|
-
export declare function buildSessionCookie(sessionId: string, ttlMs: number): string;
|
|
12
|
+
export declare function buildSessionCookie(sessionId: string, ttlMs: number, isHttps?: boolean): string;
|
|
13
13
|
export declare function generateLoginUrl(host: string, port: number, loginTtlMs: number): string;
|
package/dist/config-web-auth.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { parse, serialize } from "cookie";
|
|
2
3
|
const pendingLogins = new Map();
|
|
3
4
|
const activeSessions = new Map();
|
|
4
5
|
export function getWebConfigHost() {
|
|
@@ -58,17 +59,16 @@ function parseCookies(request) {
|
|
|
58
59
|
const header = request.headers.cookie;
|
|
59
60
|
if (!header)
|
|
60
61
|
return {};
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
cookies[key] = decodeURIComponent(value);
|
|
62
|
+
// 使用成熟的 cookie 库,更安全可靠
|
|
63
|
+
const cookies = parse(header);
|
|
64
|
+
// 转换为 Record<string, string>,过滤掉 undefined 值
|
|
65
|
+
const result = {};
|
|
66
|
+
for (const [key, value] of Object.entries(cookies)) {
|
|
67
|
+
if (value !== undefined) {
|
|
68
|
+
result[key] = value;
|
|
69
|
+
}
|
|
70
70
|
}
|
|
71
|
-
return
|
|
71
|
+
return result;
|
|
72
72
|
}
|
|
73
73
|
function getSessionIdFromRequest(request) {
|
|
74
74
|
const cookies = parseCookies(request);
|
|
@@ -94,17 +94,21 @@ export function isSessionValid(request) {
|
|
|
94
94
|
}
|
|
95
95
|
return true;
|
|
96
96
|
}
|
|
97
|
-
export function buildSessionCookie(sessionId, ttlMs) {
|
|
97
|
+
export function buildSessionCookie(sessionId, ttlMs, isHttps = false) {
|
|
98
98
|
const maxAgeSec = Math.floor(ttlMs / 1000);
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
"
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
//
|
|
107
|
-
|
|
99
|
+
const options = {
|
|
100
|
+
path: "/",
|
|
101
|
+
httpOnly: true,
|
|
102
|
+
sameSite: "lax",
|
|
103
|
+
maxAge: maxAgeSec,
|
|
104
|
+
};
|
|
105
|
+
// 根据请求协议动态设置 Secure 标志
|
|
106
|
+
// 在生产环境(HTTPS 反代后)应该设置为 true
|
|
107
|
+
if (isHttps || process.env.NODE_ENV === "production") {
|
|
108
|
+
options.secure = true;
|
|
109
|
+
}
|
|
110
|
+
// 使用成熟的 cookie 库的 serialize 函数
|
|
111
|
+
return serialize("openim_session", sessionId, options);
|
|
108
112
|
}
|
|
109
113
|
export function generateLoginUrl(host, port, loginTtlMs) {
|
|
110
114
|
const loginToken = createLoginToken(loginTtlMs);
|
package/dist/config-web.js
CHANGED
|
@@ -68,7 +68,9 @@ export async function startWebConfigServer(options) {
|
|
|
68
68
|
// 有效的一次性登录 token:创建会话,设置 Cookie,并重定向到去掉 login_token 的 URL
|
|
69
69
|
const sessionTtlMs = 24 * 60 * 60 * 1000; // 24 小时
|
|
70
70
|
const sessionId = createSession(request, sessionTtlMs);
|
|
71
|
-
|
|
71
|
+
// 检查是否通过 HTTPS 反代访问
|
|
72
|
+
const isHttps = request.headers["x-forwarded-proto"] === "https";
|
|
73
|
+
const cookie = buildSessionCookie(sessionId, sessionTtlMs, isHttps);
|
|
72
74
|
requestUrl.searchParams.delete("login_token");
|
|
73
75
|
const redirectPath = requestUrl.pathname + (requestUrl.search ? requestUrl.search : "");
|
|
74
76
|
response.writeHead(302, mergeCors(request, {
|
package/dist/index.js
CHANGED
|
@@ -370,8 +370,8 @@ export async function main() {
|
|
|
370
370
|
if (existsSync(portFile))
|
|
371
371
|
unlinkSync(portFile);
|
|
372
372
|
}
|
|
373
|
-
catch {
|
|
374
|
-
|
|
373
|
+
catch (err) {
|
|
374
|
+
log.debug('Failed to remove port file:', err);
|
|
375
375
|
}
|
|
376
376
|
// Stop each platform: abort running tasks, then handle.stop() then module.stop()
|
|
377
377
|
for (const platform of successfulPlatforms) {
|
package/dist/logger.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { createWriteStream, mkdirSync, existsSync, readdirSync, statSync, unlinkSync } from 'node:fs';
|
|
1
|
+
import { createWriteStream, mkdirSync, existsSync, readdirSync, statSync, unlinkSync, createReadStream } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { finished } from 'node:stream/promises';
|
|
4
|
+
import { pipeline } from 'node:stream';
|
|
5
|
+
import { createGzip } from 'node:zlib';
|
|
4
6
|
import { sanitize } from './sanitize.js';
|
|
5
7
|
import { APP_HOME } from './constants.js';
|
|
6
8
|
const DEFAULT_LOG_DIR = join(APP_HOME, 'logs');
|
|
7
9
|
const MAX_LOG_FILES = 10;
|
|
10
|
+
const MAX_LOG_SIZE = 10 * 1024 * 1024; // 10MB
|
|
8
11
|
const LOG_LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
|
|
9
12
|
let logDir = DEFAULT_LOG_DIR;
|
|
10
13
|
let minLevel = LOG_LEVELS.DEBUG;
|
|
@@ -17,10 +20,6 @@ function getLogFileName() {
|
|
|
17
20
|
const d = new Date();
|
|
18
21
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}.log`;
|
|
19
22
|
}
|
|
20
|
-
function getEventsFileName() {
|
|
21
|
-
const d = new Date();
|
|
22
|
-
return `events-${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}.jsonl`;
|
|
23
|
-
}
|
|
24
23
|
function rotateOldLogs() {
|
|
25
24
|
try {
|
|
26
25
|
const files = readdirSync(logDir)
|
|
@@ -31,37 +30,71 @@ function rotateOldLogs() {
|
|
|
31
30
|
unlinkSync(join(logDir, files[i].name));
|
|
32
31
|
}
|
|
33
32
|
}
|
|
34
|
-
catch {
|
|
35
|
-
|
|
33
|
+
catch (err) {
|
|
34
|
+
// 日志轮转失败不影响主流程
|
|
35
|
+
console.error('Failed to rotate log files:', err);
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
/**
|
|
39
|
+
* 压缩日志文件
|
|
40
|
+
*/
|
|
41
|
+
function compressFile(filePath) {
|
|
39
42
|
try {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
const gzip = createGzip();
|
|
44
|
+
const source = createReadStream(filePath);
|
|
45
|
+
const destination = createWriteStream(filePath + '.gz');
|
|
46
|
+
pipeline(source, gzip, destination, (err) => {
|
|
47
|
+
if (err) {
|
|
48
|
+
console.error('Failed to compress log file:', err);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
// 压缩成功,删除原文件
|
|
52
|
+
try {
|
|
53
|
+
unlinkSync(filePath);
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
console.error('Failed to delete original log file:', e);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
console.error('Failed to compress log file:', err);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 检查日志文件大小,必要时轮转
|
|
67
|
+
*/
|
|
68
|
+
function checkLogSize() {
|
|
69
|
+
if (!logStream)
|
|
70
|
+
return;
|
|
71
|
+
try {
|
|
72
|
+
const currentLogPath = join(logDir, getLogFileName());
|
|
73
|
+
const stats = statSync(currentLogPath);
|
|
74
|
+
if (stats.size > MAX_LOG_SIZE) {
|
|
75
|
+
// 关闭当前日志流
|
|
76
|
+
logStream.end();
|
|
77
|
+
logStream = undefined;
|
|
78
|
+
// 压缩当前日志文件
|
|
79
|
+
compressFile(currentLogPath);
|
|
80
|
+
// 创建新的日志文件
|
|
81
|
+
logStream = createWriteStream(join(logDir, getLogFileName()), { flags: 'a' });
|
|
46
82
|
}
|
|
47
83
|
}
|
|
48
|
-
catch {
|
|
49
|
-
|
|
84
|
+
catch (err) {
|
|
85
|
+
console.error('Failed to check log size:', err);
|
|
50
86
|
}
|
|
51
87
|
}
|
|
52
88
|
export function initLogger(dirOrOpts, level, telemetry) {
|
|
53
89
|
let dir;
|
|
54
90
|
let lev;
|
|
55
|
-
let tel;
|
|
56
91
|
if (dirOrOpts && typeof dirOrOpts === 'object' && !Array.isArray(dirOrOpts)) {
|
|
57
92
|
dir = dirOrOpts.logDir;
|
|
58
93
|
lev = dirOrOpts.logLevel;
|
|
59
|
-
tel = dirOrOpts.telemetry;
|
|
60
94
|
}
|
|
61
95
|
else {
|
|
62
96
|
dir = dirOrOpts;
|
|
63
97
|
lev = level;
|
|
64
|
-
tel = telemetry;
|
|
65
98
|
}
|
|
66
99
|
if (dir)
|
|
67
100
|
logDir = dir;
|
|
@@ -90,6 +123,8 @@ function write(level, tag, msg, ...args) {
|
|
|
90
123
|
else
|
|
91
124
|
process.stdout.write(line);
|
|
92
125
|
logStream?.write(line);
|
|
126
|
+
// 检查日志文件大小
|
|
127
|
+
checkLogSize();
|
|
93
128
|
}
|
|
94
129
|
export function createLogger(tag) {
|
|
95
130
|
return {
|
|
@@ -20,24 +20,22 @@ import { setChatUser } from '../shared/chat-user-map.js';
|
|
|
20
20
|
import { createLogger, auditLog } from '../logger.js';
|
|
21
21
|
import { handleEnqueueResult, DEFAULT_QUEUE_FULL_MESSAGE, DEFAULT_QUEUED_MESSAGE } from '../shared/utils.js';
|
|
22
22
|
import { walWrite, walCommit } from '../shared/message-wal.js';
|
|
23
|
+
import { TTLCache } from '@isaacs/ttlcache';
|
|
23
24
|
/* ── 幂等性:消息去重 ── */
|
|
24
25
|
const DEDUP_TTL_MS = 60_000; // 1 分钟内的相同 msgId 视为重复
|
|
25
|
-
const
|
|
26
|
+
const DEDUP_MAX_SIZE = 1000; // 最大缓存条目数
|
|
27
|
+
// 使用 TTLCache 自动管理过期条目,避免内存泄漏
|
|
28
|
+
const dedupCache = new TTLCache({
|
|
29
|
+
max: DEDUP_MAX_SIZE,
|
|
30
|
+
ttl: DEDUP_TTL_MS,
|
|
31
|
+
updateAgeOnGet: false, // 访问时不更新过期时间
|
|
32
|
+
});
|
|
26
33
|
function isDuplicate(msgId) {
|
|
27
34
|
if (!msgId)
|
|
28
35
|
return false;
|
|
29
|
-
|
|
30
|
-
const prev = dedupCache.get(msgId);
|
|
31
|
-
if (prev && now - prev < DEDUP_TTL_MS)
|
|
36
|
+
if (dedupCache.has(msgId))
|
|
32
37
|
return true;
|
|
33
|
-
dedupCache.set(msgId,
|
|
34
|
-
// 清理过期条目
|
|
35
|
-
if (dedupCache.size > 1000) {
|
|
36
|
-
for (const [k, v] of dedupCache) {
|
|
37
|
-
if (now - v > DEDUP_TTL_MS)
|
|
38
|
-
dedupCache.delete(k);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
38
|
+
dedupCache.set(msgId, true);
|
|
41
39
|
return false;
|
|
42
40
|
}
|
|
43
41
|
const log = createLogger('TextFlow');
|
package/dist/sanitize.js
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
const PATTERNS = [
|
|
2
|
-
|
|
3
|
-
[/\
|
|
4
|
-
|
|
2
|
+
// OpenAI / Anthropic / Claude API keys
|
|
3
|
+
[/\bsk-[a-zA-Z0-9]{32,}\b/g, (m) => 'sk-****' + m.slice(-4)],
|
|
4
|
+
// AWS Access Key
|
|
5
|
+
[/\b(AKIA|ASIA)[A-Z0-9]{16}\b/g, (m) => m.slice(0, 4) + '****'],
|
|
6
|
+
// GitHub Personal Access Token
|
|
7
|
+
[/\b(ghp|github_pat)_[a-zA-Z0-9_]{36,}\b/g, (m) => m.slice(0, 4) + '_****'],
|
|
8
|
+
// GitLab Personal Access Token
|
|
9
|
+
[/\bglpat-[a-zA-Z0-9_-]{20,}\b/g, (m) => 'glpat-****'],
|
|
10
|
+
// Telegram Bot Token
|
|
11
|
+
[/\b[0-9]{8,}:[a-zA-Z0-9_-]{35,}\b/g, (m) => m.split(':')[0] + ':****'],
|
|
12
|
+
// Anthropic API Key
|
|
13
|
+
[/\bant-[a-zA-Z0-9]{32,}\b/g, (m) => 'ant-****' + m.slice(-4)],
|
|
14
|
+
// OpenRouter API Key
|
|
15
|
+
[/\bsk-or-[a-zA-Z0-9]{32,}\b/g, (m) => 'sk-or-****' + m.slice(-4)],
|
|
16
|
+
// Google API Key
|
|
17
|
+
[/\bAIza[a-zA-Z0-9_-]{35}\b/g, (m) => 'AIza****'],
|
|
18
|
+
// Azure API Key
|
|
19
|
+
[/\b[a-f0-9]{32}:[a-zA-Z0-9]{44}\b/g, (m) => m.split(':')[0] + ':****'],
|
|
20
|
+
// 飞书/钉钉等 Bot Token
|
|
21
|
+
[/\b(bot)[-_][a-zA-Z0-9_-]{20,}\b/gi, (m) => 'bot_****'],
|
|
22
|
+
// 通用 API Key (更严格的匹配,避免误报)
|
|
23
|
+
[/\b(api_key|apikey|api-key|API_KEY|APIKEY)[\s]*[:=][\s]*[a-zA-Z0-9_-]{16,}\b/gi,
|
|
24
|
+
(m) => m.split(/[:=]/)[0] + '=****'],
|
|
25
|
+
// 密码模式
|
|
26
|
+
[/\b(password|passwd|pwd)[\s]*[:=][\s]*[^\s]{8,}\b/gi,
|
|
27
|
+
(m) => m.split(/[:=]/)[0] + '=****'],
|
|
5
28
|
];
|
|
6
29
|
export function sanitize(text) {
|
|
7
30
|
let result = text;
|
package/dist/shared/keepalive.js
CHANGED
package/dist/shared/sentry.js
CHANGED
|
@@ -9,7 +9,9 @@ import * as Sentry from '@sentry/node';
|
|
|
9
9
|
import { createLogger } from '../logger.js';
|
|
10
10
|
const log = createLogger('Sentry');
|
|
11
11
|
// 开发者的 Sentry DSN(所有 open-im 实例共享)
|
|
12
|
-
|
|
12
|
+
// 注意:此 DSN 仅用于 open-im 自身的错误追踪
|
|
13
|
+
// 用户可在环境变量中配置自己的 DSN
|
|
14
|
+
const DEFAULT_DSN = process.env.OPEN_IM_SENTRY_DSN ?? 'https://cc5ad094c1229b2a2ff23ab54b0fd807@o4508612762861568.ingest.us.sentry.io/4511583989727232';
|
|
13
15
|
let initialized = false;
|
|
14
16
|
/**
|
|
15
17
|
* 清理 PII(用户数据)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wu529778790/open-im",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.11-beta.0",
|
|
4
4
|
"description": "Your AI coding assistant, in every chat app. Multi-platform IM bridge for Claude Code, Codex, and CodeBuddy.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -54,11 +54,13 @@
|
|
|
54
54
|
"@anthropic-ai/claude-agent-sdk": "^0.3.179",
|
|
55
55
|
"@anthropic-ai/sdk": "^0.104.2",
|
|
56
56
|
"@aws-sdk/client-s3": "^3.1035.0",
|
|
57
|
-
"@
|
|
57
|
+
"@isaacs/ttlcache": "^2.1.5",
|
|
58
|
+
"@larksuiteoapi/node-sdk": "^1.67.0",
|
|
58
59
|
"@opencode-ai/sdk": "^1.17.9",
|
|
59
60
|
"@sentry/node": "^10.58.0",
|
|
60
61
|
"@types/qrcode": "^1.5.6",
|
|
61
62
|
"centrifuge": "^5.5.3",
|
|
63
|
+
"cookie": "^1.1.1",
|
|
62
64
|
"dingtalk-stream": "^2.1.4",
|
|
63
65
|
"edge-tts": "^1.0.1",
|
|
64
66
|
"https-proxy-agent": "^9.1.0",
|
|
@@ -74,15 +76,20 @@
|
|
|
74
76
|
"@types/node": "^20.0.0",
|
|
75
77
|
"@types/prompts": "^2.4.9",
|
|
76
78
|
"@types/ws": "^8.5.13",
|
|
79
|
+
"axios": "^1.18.1",
|
|
77
80
|
"dotenv": "^17.3.1",
|
|
78
81
|
"eslint": "^9.15.0",
|
|
79
82
|
"globals": "^15.12.0",
|
|
80
83
|
"tsx": "^4.0.0",
|
|
81
84
|
"typescript": "^5.0.0",
|
|
82
85
|
"typescript-eslint": "^8.58.0",
|
|
86
|
+
"vite": "^8.1.0",
|
|
83
87
|
"vitest": "^4.1.2"
|
|
84
88
|
},
|
|
85
89
|
"engines": {
|
|
86
90
|
"node": ">=20"
|
|
91
|
+
},
|
|
92
|
+
"overrides": {
|
|
93
|
+
"axios": "^1.18.1"
|
|
87
94
|
}
|
|
88
95
|
}
|