@wu529778790/open-im 1.11.10-beta.3 → 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.
@@ -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;
@@ -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
- const cookies = {};
62
- const parts = header.split(";");
63
- for (const part of parts) {
64
- const [rawKey, ...rest] = part.split("=");
65
- const key = rawKey.trim();
66
- if (!key)
67
- continue;
68
- const value = rest.join("=").trim();
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 cookies;
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 parts = [
100
- `openim_session=${encodeURIComponent(sessionId)}`,
101
- "Path=/",
102
- "HttpOnly",
103
- "SameSite=Lax",
104
- `Max-Age=${maxAgeSec}`,
105
- ];
106
- // 不设置 Secure,方便本地 http 使用;如果放在 https 反代后,可以在代理层加 Secure
107
- return parts.join("; ");
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);
@@ -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
- const cookie = buildSessionCookie(sessionId, sessionTtlMs);
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
- /* ignore */
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
- /* ignore */
33
+ catch (err) {
34
+ // 日志轮转失败不影响主流程
35
+ console.error('Failed to rotate log files:', err);
36
36
  }
37
37
  }
38
- function rotateOldJsonl() {
38
+ /**
39
+ * 压缩日志文件
40
+ */
41
+ function compressFile(filePath) {
39
42
  try {
40
- const files = readdirSync(logDir)
41
- .filter((f) => f.endsWith('.jsonl'))
42
- .map((f) => ({ name: f, time: statSync(join(logDir, f)).mtimeMs }))
43
- .sort((a, b) => b.time - a.time);
44
- for (let i = MAX_LOG_FILES; i < files.length; i++) {
45
- unlinkSync(join(logDir, files[i].name));
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
- /* ignore */
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 dedupCache = new Map(); // msgId → timestamp
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
- const now = Date.now();
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, now);
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
- [/\b(sk|pk|bot)[-_][a-zA-Z0-9_-]{8,}/gi, (m) => (m.match(/^[a-zA-Z]+/)?.[0] || m.slice(0, 2)) + '_****'],
3
- [/\b(AIza|AKIA)[a-zA-Z0-9]{12,}/g, (m) => m.slice(0, 4) + '****'],
4
- [/\b[a-zA-Z0-9]{40,}\b/g, (m) => m.slice(0, 6) + '****'],
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;
@@ -51,7 +51,9 @@ async function ping(target, workDir) {
51
51
  try {
52
52
  handle.abort();
53
53
  }
54
- catch { }
54
+ catch (err) {
55
+ // 中止失败,忽略错误
56
+ }
55
57
  settle(false, `timeout after ${PING_TIMEOUT_MS}ms`);
56
58
  }, PING_TIMEOUT_MS);
57
59
  });
@@ -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
- const DEFAULT_DSN = 'https://cc5ad094c1229b2a2ff23ab54b0fd807@o4508612762861568.ingest.us.sentry.io/4511583989727232';
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.10-beta.3",
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
- "@larksuiteoapi/node-sdk": "^1.59.0",
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
  }