@wu529778790/open-im 1.9.4-beta.1 → 1.9.4-beta.10
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/access/access-control.js +1 -1
- package/dist/adapters/claude-sdk-adapter.js +50 -30
- package/dist/adapters/claude-sdk-adapter.test.js +50 -0
- package/dist/config/credentials.d.ts +27 -0
- package/dist/config/credentials.js +85 -0
- package/dist/config/file-io.d.ts +11 -0
- package/dist/config/file-io.js +176 -0
- package/dist/config/types.d.ts +177 -0
- package/dist/config/types.js +1 -0
- package/dist/config.d.ts +3 -185
- package/dist/config.js +9 -185
- package/dist/dingtalk/api.d.ts +21 -0
- package/dist/dingtalk/api.js +117 -0
- package/dist/dingtalk/client.d.ts +10 -23
- package/dist/dingtalk/client.js +57 -607
- package/dist/dingtalk/event-handler.d.ts +1 -0
- package/dist/dingtalk/event-handler.js +50 -67
- package/dist/dingtalk/streaming-card.d.ts +20 -0
- package/dist/dingtalk/streaming-card.js +300 -0
- package/dist/dingtalk/webhook.d.ts +12 -0
- package/dist/dingtalk/webhook.js +223 -0
- package/dist/feishu/cardkit-manager.js +24 -1
- package/dist/feishu/event-handler.d.ts +1 -0
- package/dist/feishu/event-handler.js +160 -227
- package/dist/index.js +94 -108
- package/dist/platform/create-event-context.d.ts +28 -0
- package/dist/platform/create-event-context.js +30 -0
- package/dist/platform/create-event-context.test.d.ts +1 -0
- package/dist/platform/create-event-context.test.js +89 -0
- package/dist/platform/handle-ai-request.d.ts +108 -0
- package/dist/platform/handle-ai-request.js +169 -0
- package/dist/platform/handle-ai-request.test.d.ts +1 -0
- package/dist/platform/handle-ai-request.test.js +349 -0
- package/dist/platform/handle-text-flow.d.ts +75 -0
- package/dist/platform/handle-text-flow.js +114 -0
- package/dist/platform/handle-text-flow.test.d.ts +1 -0
- package/dist/platform/handle-text-flow.test.js +237 -0
- package/dist/qq/client.js +12 -0
- package/dist/qq/event-handler.d.ts +1 -0
- package/dist/qq/event-handler.js +137 -106
- package/dist/queue/request-queue.d.ts +1 -1
- package/dist/queue/request-queue.js +14 -1
- package/dist/queue/request-queue.test.d.ts +1 -0
- package/dist/queue/request-queue.test.js +114 -0
- package/dist/session/session-manager.js +8 -4
- package/dist/session/session-manager.test.js +92 -4
- package/dist/setup.js +9 -1
- package/dist/shared/ai-task.d.ts +2 -0
- package/dist/shared/ai-task.js +12 -2
- package/dist/shared/ai-task.test.js +127 -0
- package/dist/shared/task-cleanup.d.ts +8 -0
- package/dist/shared/task-cleanup.js +30 -0
- package/dist/shared/task-cleanup.test.d.ts +1 -0
- package/dist/shared/task-cleanup.test.js +74 -0
- package/dist/telegram/event-handler.d.ts +1 -0
- package/dist/telegram/event-handler.js +162 -189
- package/dist/wework/event-handler.d.ts +2 -1
- package/dist/wework/event-handler.js +87 -95
- package/dist/workbuddy/centrifuge-client.d.ts +13 -11
- package/dist/workbuddy/centrifuge-client.js +132 -115
- package/dist/workbuddy/client.js +10 -1
- package/dist/workbuddy/event-handler.d.ts +2 -1
- package/dist/workbuddy/event-handler.js +109 -64
- package/dist/workbuddy/message-sender.d.ts +0 -5
- package/dist/workbuddy/message-sender.js +0 -17
- package/dist/workbuddy/types.d.ts +1 -1
- package/package.json +2 -5
- package/dist/qq/event-handler.test.js +0 -68
- package/dist/shared/retry.d.ts +0 -10
- package/dist/shared/retry.js +0 -26
- /package/dist/{qq/event-handler.test.d.ts → adapters/claude-sdk-adapter.test.d.ts} +0 -0
|
@@ -8,7 +8,7 @@ export class AccessControl {
|
|
|
8
8
|
}
|
|
9
9
|
isAllowed(userId) {
|
|
10
10
|
if (this.allowedUserIds.size === 0) {
|
|
11
|
-
log.warn(`Allowing user ${userId}
|
|
11
|
+
log.warn(`SECURITY: Allowing user ${userId} -- no whitelist configured. Set allowedUserIds in config or OPEN_IM_ALLOWED_USER_IDS env var to restrict access.`);
|
|
12
12
|
return true;
|
|
13
13
|
}
|
|
14
14
|
const allowed = this.allowedUserIds.has(userId);
|
|
@@ -9,8 +9,34 @@
|
|
|
9
9
|
* 认证:ANTHROPIC_API_KEY 或 CLAUDE_CODE_OAUTH_TOKEN
|
|
10
10
|
*/
|
|
11
11
|
import { unstable_v2_createSession, unstable_v2_resumeSession } from '@anthropic-ai/claude-agent-sdk';
|
|
12
|
+
import { existsSync, readFileSync } from 'fs';
|
|
13
|
+
import { homedir } from 'os';
|
|
14
|
+
import { join } from 'path';
|
|
12
15
|
import { createLogger } from '../logger.js';
|
|
13
16
|
const log = createLogger('ClaudeSDK');
|
|
17
|
+
function loadUserPluginSettings() {
|
|
18
|
+
try {
|
|
19
|
+
const settingsPath = join(homedir(), '.claude', 'settings.json');
|
|
20
|
+
if (!existsSync(settingsPath))
|
|
21
|
+
return null;
|
|
22
|
+
const content = readFileSync(settingsPath, 'utf-8');
|
|
23
|
+
const settings = JSON.parse(content);
|
|
24
|
+
const result = {};
|
|
25
|
+
if (settings.enabledPlugins)
|
|
26
|
+
result.enabledPlugins = settings.enabledPlugins;
|
|
27
|
+
if (settings.extraKnownMarketplaces)
|
|
28
|
+
result.extraKnownMarketplaces = settings.extraKnownMarketplaces;
|
|
29
|
+
if (Object.keys(result).length === 0)
|
|
30
|
+
return null;
|
|
31
|
+
log.info(`Loaded user plugin settings: plugins=[${Object.keys(result.enabledPlugins ?? {}).join(', ')}]`);
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
log.warn('Failed to read ~/.claude/settings.json for plugin config:', err);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const userPluginSettings = loadUserPluginSettings();
|
|
14
40
|
// 存储所有活跃的 SDKSession 对象,key 为 sessionId
|
|
15
41
|
// 使用 Map 而不是 Set,因为我们需要通过 sessionId 获取 session
|
|
16
42
|
const activeSessions = new Map();
|
|
@@ -22,6 +48,8 @@ const sessionLastUsed = new Map();
|
|
|
22
48
|
const runningSessions = new Set();
|
|
23
49
|
const SESSION_IDLE_TTL_MS = 30 * 60 * 1000; // 30 分钟未使用则清理
|
|
24
50
|
const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 每 5 分钟检查一次
|
|
51
|
+
const MAX_ACTIVE_SESSIONS = 100;
|
|
52
|
+
let sessionSeq = 0;
|
|
25
53
|
const cleanupInterval = setInterval(() => {
|
|
26
54
|
const now = Date.now();
|
|
27
55
|
for (const [id, lastUsed] of sessionLastUsed) {
|
|
@@ -42,33 +70,17 @@ const cleanupInterval = setInterval(() => {
|
|
|
42
70
|
}
|
|
43
71
|
}, CLEANUP_INTERVAL_MS);
|
|
44
72
|
cleanupInterval.unref(); // 不阻止进程退出
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
continue; // 跳过正在运行任务的 session
|
|
57
|
-
if (now - lastUsed > SESSION_IDLE_TTL_MS) {
|
|
58
|
-
const s = activeSessions.get(id);
|
|
59
|
-
if (s) {
|
|
60
|
-
try {
|
|
61
|
-
s.close();
|
|
62
|
-
}
|
|
63
|
-
catch { /* ignore */ }
|
|
64
|
-
activeSessions.delete(id);
|
|
65
|
-
}
|
|
66
|
-
sessionLastUsed.delete(id);
|
|
67
|
-
log.info(`Lazy cleanup: idle session ${id} (unused ${Math.round((now - lastUsed) / 60000)}min)`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
// Mutex to serialize process.chdir() calls across concurrent users
|
|
73
|
+
/**
|
|
74
|
+
* Serializes process.chdir() calls across concurrent users.
|
|
75
|
+
*
|
|
76
|
+
* process.chdir() is a process-wide global side effect — only one chdir can
|
|
77
|
+
* be active at a time. The SDK's createSession/resumeSession do not accept a
|
|
78
|
+
* `cwd` parameter, so we must chdir before calling them. This mutex ensures
|
|
79
|
+
* concurrent requests don't race on the working directory.
|
|
80
|
+
*
|
|
81
|
+
* **Limitation:** If the SDK ever supports a `cwd` option, this mutex should
|
|
82
|
+
* be removed entirely.
|
|
83
|
+
*/
|
|
72
84
|
let chdirMutex = Promise.resolve();
|
|
73
85
|
function withChdirMutex(fn) {
|
|
74
86
|
const previous = chdirMutex;
|
|
@@ -108,8 +120,10 @@ function isSessionCorruptionError(msg) {
|
|
|
108
120
|
* @returns SDKSession 对象和实际的 sessionId
|
|
109
121
|
*/
|
|
110
122
|
async function getOrCreateSession(sessionId, workDir, model, permissionMode) {
|
|
111
|
-
lazyCleanupIdleSessions();
|
|
112
123
|
const resolvedModel = model?.trim() || 'claude-opus-4-5';
|
|
124
|
+
if (activeSessions.size >= MAX_ACTIVE_SESSIONS) {
|
|
125
|
+
throw new Error(`Session pool is full (${MAX_ACTIVE_SESSIONS}). Cannot create new session.`);
|
|
126
|
+
}
|
|
113
127
|
const sessionOptions = {
|
|
114
128
|
model: resolvedModel,
|
|
115
129
|
permissionMode,
|
|
@@ -156,7 +170,7 @@ async function getOrCreateSession(sessionId, workDir, model, permissionMode) {
|
|
|
156
170
|
activeSessions.set(tempId, session);
|
|
157
171
|
sessionLastUsed.set(tempId, Date.now());
|
|
158
172
|
log.info(`Created new session (tempId: ${tempId})`);
|
|
159
|
-
return { session, sessionId: tempId };
|
|
173
|
+
return { session, sessionId: tempId, wasReused: false };
|
|
160
174
|
}
|
|
161
175
|
finally {
|
|
162
176
|
if (workDir && workDir !== originalCwd) {
|
|
@@ -260,7 +274,13 @@ export class ClaudeSDKAdapter {
|
|
|
260
274
|
}
|
|
261
275
|
// 获取实际的 sessionId(从 init 消息中)
|
|
262
276
|
if (isSystemInit(msg)) {
|
|
263
|
-
const
|
|
277
|
+
const initMsg = msg;
|
|
278
|
+
// 记录 session 加载的插件和技能
|
|
279
|
+
const pluginNames = initMsg.plugins?.map(p => p.name).join(', ') ?? 'none';
|
|
280
|
+
const skillCount = initMsg.skills?.length ?? 0;
|
|
281
|
+
const toolCount = initMsg.tools?.length ?? 0;
|
|
282
|
+
log.info(`[V2] Init: plugins=[${pluginNames}], skills=${skillCount}, tools=${toolCount}`);
|
|
283
|
+
const newSessionId = initMsg.session_id;
|
|
264
284
|
if (newSessionId && newSessionId !== actualSessionId) {
|
|
265
285
|
// 更新 sessionId 映射
|
|
266
286
|
// 清理 pending 临时 ID(actualSessionId 尚未赋值时用 pendingTempId)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
// Mock the logger before importing the adapter under test
|
|
3
|
+
vi.mock('../logger.js', () => ({
|
|
4
|
+
createLogger: () => ({
|
|
5
|
+
info: vi.fn(),
|
|
6
|
+
warn: vi.fn(),
|
|
7
|
+
error: vi.fn(),
|
|
8
|
+
debug: vi.fn(),
|
|
9
|
+
}),
|
|
10
|
+
}));
|
|
11
|
+
// Mock the Claude Agent SDK
|
|
12
|
+
vi.mock('@anthropic-ai/claude-agent-sdk', () => ({
|
|
13
|
+
unstable_v2_createSession: vi.fn(),
|
|
14
|
+
unstable_v2_resumeSession: vi.fn(),
|
|
15
|
+
}));
|
|
16
|
+
// Import after mocks are set up
|
|
17
|
+
import { ClaudeSDKAdapter } from './claude-sdk-adapter.js';
|
|
18
|
+
describe('ClaudeSDKAdapter', () => {
|
|
19
|
+
let adapter;
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
adapter = new ClaudeSDKAdapter();
|
|
22
|
+
});
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
// Clean up any active sessions/timers created during tests
|
|
25
|
+
ClaudeSDKAdapter.destroy();
|
|
26
|
+
});
|
|
27
|
+
it('implements the ToolAdapter interface', () => {
|
|
28
|
+
expect(adapter).toBeDefined();
|
|
29
|
+
expect(typeof adapter.toolId).toBe('string');
|
|
30
|
+
expect(typeof adapter.run).toBe('function');
|
|
31
|
+
});
|
|
32
|
+
it('has toolId set to claude-sdk', () => {
|
|
33
|
+
expect(adapter.toolId).toBe('claude-sdk');
|
|
34
|
+
});
|
|
35
|
+
it('has a run method that returns a RunHandle', () => {
|
|
36
|
+
const callbacks = {
|
|
37
|
+
onText: vi.fn(),
|
|
38
|
+
onComplete: vi.fn(),
|
|
39
|
+
onError: vi.fn(),
|
|
40
|
+
};
|
|
41
|
+
const handle = adapter.run('test prompt', undefined, '/tmp', callbacks);
|
|
42
|
+
expect(handle).toBeDefined();
|
|
43
|
+
expect(typeof handle.abort).toBe('function');
|
|
44
|
+
// Abort to clean up the background promise
|
|
45
|
+
handle.abort();
|
|
46
|
+
});
|
|
47
|
+
it('stop() (static destroy) does not throw', () => {
|
|
48
|
+
expect(() => ClaudeSDKAdapter.destroy()).not.toThrow();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { FileConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves a single credential value using the standard priority chain:
|
|
4
|
+
* environment variable → platform file config → legacy file config.
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolveCredential(envKey: string, fileValue?: string, legacyFileValue?: string): string | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Generic per-platform credential resolution.
|
|
9
|
+
* Returns all resolved credentials and whether the platform should be enabled.
|
|
10
|
+
*/
|
|
11
|
+
export interface ResolvedPlatform {
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
credentials: Record<string, string | undefined>;
|
|
14
|
+
}
|
|
15
|
+
export declare function resolvePlatformCredentials(envKeys: Record<string, string>, fileValues: Record<string, string | undefined>, legacyValues: Record<string, string | undefined>, requiredKeys: string[], enabledFlag?: boolean): ResolvedPlatform;
|
|
16
|
+
/**
|
|
17
|
+
* Extract WorkBuddy credentials, with legacy platforms.wechat migration support.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveWorkBuddyFileConfig(fileConfig: FileConfig): NonNullable<FileConfig['platforms']>['workbuddy'] | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Check if a CLI tool is available at the given path or on PATH.
|
|
22
|
+
*/
|
|
23
|
+
export declare function checkCliAvailable(cliPath: string, toolName: string): void;
|
|
24
|
+
/**
|
|
25
|
+
* Resolve Windows-specific CLI path for npm global installs.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveWindowsCliPath(cliName: string, configuredPath: string): string;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { accessSync, constants } from 'node:fs';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { join, isAbsolute } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Resolves a single credential value using the standard priority chain:
|
|
6
|
+
* environment variable → platform file config → legacy file config.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveCredential(envKey, fileValue, legacyFileValue) {
|
|
9
|
+
return process.env[envKey] ?? fileValue ?? legacyFileValue;
|
|
10
|
+
}
|
|
11
|
+
export function resolvePlatformCredentials(envKeys, fileValues, legacyValues, requiredKeys, enabledFlag) {
|
|
12
|
+
const credentials = {};
|
|
13
|
+
for (const [name, envKey] of Object.entries(envKeys)) {
|
|
14
|
+
credentials[name] = resolveCredential(envKey, fileValues[name], legacyValues[name]);
|
|
15
|
+
}
|
|
16
|
+
const hasRequired = requiredKeys.every((key) => credentials[key] !== undefined && credentials[key] !== '');
|
|
17
|
+
return {
|
|
18
|
+
enabled: hasRequired && enabledFlag !== false,
|
|
19
|
+
credentials,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Extract WorkBuddy credentials, with legacy platforms.wechat migration support.
|
|
24
|
+
*/
|
|
25
|
+
export function resolveWorkBuddyFileConfig(fileConfig) {
|
|
26
|
+
const direct = fileConfig.platforms?.workbuddy;
|
|
27
|
+
if (direct)
|
|
28
|
+
return direct;
|
|
29
|
+
const legacyWechat = fileConfig.platforms?.wechat;
|
|
30
|
+
if (legacyWechat?.workbuddyAccessToken && legacyWechat?.workbuddyRefreshToken) {
|
|
31
|
+
return {
|
|
32
|
+
accessToken: legacyWechat.workbuddyAccessToken,
|
|
33
|
+
refreshToken: legacyWechat.workbuddyRefreshToken,
|
|
34
|
+
userId: legacyWechat.userId,
|
|
35
|
+
baseUrl: legacyWechat.workbuddyBaseUrl,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Check if a CLI tool is available at the given path or on PATH.
|
|
42
|
+
*/
|
|
43
|
+
export function checkCliAvailable(cliPath, toolName) {
|
|
44
|
+
if (isAbsolute(cliPath) || cliPath.includes('/') || cliPath.includes('\\')) {
|
|
45
|
+
try {
|
|
46
|
+
accessSync(cliPath, constants.F_OK);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
throw new Error(`${toolName} CLI not found at: ${cliPath}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const checkCommand = process.platform === 'win32' ? 'where' : 'which';
|
|
54
|
+
try {
|
|
55
|
+
execFileSync(checkCommand, [cliPath], {
|
|
56
|
+
stdio: 'pipe',
|
|
57
|
+
windowsHide: process.platform === 'win32',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
throw new Error(`${toolName} CLI not found on PATH: ${cliPath}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolve Windows-specific CLI path for npm global installs.
|
|
67
|
+
*/
|
|
68
|
+
export function resolveWindowsCliPath(cliName, configuredPath) {
|
|
69
|
+
if (process.platform !== 'win32' || configuredPath !== cliName)
|
|
70
|
+
return configuredPath;
|
|
71
|
+
const npmPaths = [
|
|
72
|
+
join(process.env.APPDATA || '', 'npm', `${cliName}.cmd`),
|
|
73
|
+
join(process.env.LOCALAPPDATA || '', 'npm', `${cliName}.cmd`),
|
|
74
|
+
];
|
|
75
|
+
for (const p of npmPaths) {
|
|
76
|
+
try {
|
|
77
|
+
accessSync(p, constants.F_OK);
|
|
78
|
+
return p;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
/* try next */
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return configuredPath;
|
|
85
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AiCommand, FileConfig } from './types.js';
|
|
2
|
+
export declare const CONFIG_PATH: string;
|
|
3
|
+
export declare const CODEX_AUTH_PATHS: string[];
|
|
4
|
+
export declare function loadFileConfig(): FileConfig;
|
|
5
|
+
export declare function saveFileConfig(raw: FileConfig): void;
|
|
6
|
+
export declare function getClaudeConfigHome(): string;
|
|
7
|
+
export declare function loadClaudeSettingsEnv(): Record<string, string>;
|
|
8
|
+
export declare function saveClaudeSettingsEnv(env: Record<string, string>): void;
|
|
9
|
+
export declare function normalizeAiCommand(value: unknown, fallback: AiCommand): AiCommand;
|
|
10
|
+
export declare function hasCodexAuth(): boolean;
|
|
11
|
+
export declare function parseCommaSeparated(value: string): string[];
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, chmodSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { createLogger } from '../logger.js';
|
|
5
|
+
import { APP_HOME } from '../constants.js';
|
|
6
|
+
const log = createLogger('config');
|
|
7
|
+
export const CONFIG_PATH = join(APP_HOME, 'config.json');
|
|
8
|
+
export const CODEX_AUTH_PATHS = [
|
|
9
|
+
join(homedir(), '.codex', 'auth.json'),
|
|
10
|
+
join(homedir(), '.config', 'codex', 'auth.json'),
|
|
11
|
+
join(homedir(), 'AppData', 'Roaming', 'codex', 'auth.json'),
|
|
12
|
+
];
|
|
13
|
+
const OLD_ROOT_KEYS = [
|
|
14
|
+
'claudeWorkDir',
|
|
15
|
+
'claudeTimeoutMs',
|
|
16
|
+
'claudeModel',
|
|
17
|
+
];
|
|
18
|
+
const AI_COMMANDS = ['claude', 'codex', 'codebuddy'];
|
|
19
|
+
// Config cache with mtime tracking
|
|
20
|
+
let cachedConfig = null;
|
|
21
|
+
let cachedClaudeEnv = null;
|
|
22
|
+
function hasOldConfigFormat(raw) {
|
|
23
|
+
const hasOld = OLD_ROOT_KEYS.some((k) => raw[k] !== undefined && raw[k] !== null);
|
|
24
|
+
const hasNew = raw.tools && typeof raw.tools === 'object' && raw.tools.claude;
|
|
25
|
+
return !!hasOld && !hasNew;
|
|
26
|
+
}
|
|
27
|
+
function migrateToNewConfigFormat(raw) {
|
|
28
|
+
const tools = raw.tools || {};
|
|
29
|
+
const tc = tools.claude || {};
|
|
30
|
+
const tcod = tools.codex || {};
|
|
31
|
+
const tcb = tools.codebuddy || {};
|
|
32
|
+
const migrated = { ...raw };
|
|
33
|
+
migrated.tools = {
|
|
34
|
+
claude: {
|
|
35
|
+
...tc,
|
|
36
|
+
workDir: tc.workDir ?? raw.claudeWorkDir ?? process.cwd(),
|
|
37
|
+
proxy: tc.proxy,
|
|
38
|
+
},
|
|
39
|
+
codex: {
|
|
40
|
+
...tcod,
|
|
41
|
+
cliPath: tcod.cliPath ?? 'codex',
|
|
42
|
+
workDir: tcod.workDir ?? raw.claudeWorkDir ?? process.cwd(),
|
|
43
|
+
proxy: tcod.proxy,
|
|
44
|
+
},
|
|
45
|
+
codebuddy: {
|
|
46
|
+
...tcb,
|
|
47
|
+
cliPath: tcb.cliPath ?? 'codebuddy',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
for (const k of OLD_ROOT_KEYS) {
|
|
51
|
+
delete migrated[k];
|
|
52
|
+
}
|
|
53
|
+
return migrated;
|
|
54
|
+
}
|
|
55
|
+
export function loadFileConfig() {
|
|
56
|
+
try {
|
|
57
|
+
if (!existsSync(CONFIG_PATH))
|
|
58
|
+
return {};
|
|
59
|
+
const stats = statSync(CONFIG_PATH);
|
|
60
|
+
const currentMtime = stats.mtimeMs;
|
|
61
|
+
if (cachedConfig && cachedConfig.mtime === currentMtime) {
|
|
62
|
+
return cachedConfig.config;
|
|
63
|
+
}
|
|
64
|
+
const raw = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
65
|
+
if (!raw || typeof raw !== 'object')
|
|
66
|
+
return {};
|
|
67
|
+
if (hasOldConfigFormat(raw)) {
|
|
68
|
+
const migrated = migrateToNewConfigFormat(raw);
|
|
69
|
+
const dir = dirname(CONFIG_PATH);
|
|
70
|
+
if (!existsSync(dir))
|
|
71
|
+
mkdirSync(dir, { recursive: true });
|
|
72
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(migrated, null, 2), 'utf-8');
|
|
73
|
+
try {
|
|
74
|
+
chmodSync(CONFIG_PATH, 0o600);
|
|
75
|
+
}
|
|
76
|
+
catch { /* ignore */ }
|
|
77
|
+
cachedConfig = { config: migrated, mtime: currentMtime };
|
|
78
|
+
return migrated;
|
|
79
|
+
}
|
|
80
|
+
cachedConfig = { config: raw, mtime: currentMtime };
|
|
81
|
+
return raw;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export function saveFileConfig(raw) {
|
|
88
|
+
const dir = dirname(CONFIG_PATH);
|
|
89
|
+
if (!existsSync(dir))
|
|
90
|
+
mkdirSync(dir, { recursive: true });
|
|
91
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(raw, null, 2), 'utf-8');
|
|
92
|
+
cachedConfig = null;
|
|
93
|
+
}
|
|
94
|
+
export function getClaudeConfigHome() {
|
|
95
|
+
return process.env.HOME || process.env.USERPROFILE || homedir();
|
|
96
|
+
}
|
|
97
|
+
export function loadClaudeSettingsEnv() {
|
|
98
|
+
const home = getClaudeConfigHome();
|
|
99
|
+
const paths = [
|
|
100
|
+
join(home, '.claude', 'settings.json'),
|
|
101
|
+
join(home, '.claude.json'),
|
|
102
|
+
];
|
|
103
|
+
for (const p of paths) {
|
|
104
|
+
try {
|
|
105
|
+
if (existsSync(p)) {
|
|
106
|
+
const stats = statSync(p);
|
|
107
|
+
const currentMtime = stats.mtimeMs;
|
|
108
|
+
if (cachedClaudeEnv && cachedClaudeEnv.mtime === currentMtime && cachedClaudeEnv.env) {
|
|
109
|
+
return cachedClaudeEnv.env;
|
|
110
|
+
}
|
|
111
|
+
const raw = JSON.parse(readFileSync(p, 'utf-8'));
|
|
112
|
+
const env = raw?.env;
|
|
113
|
+
if (env && typeof env === 'object') {
|
|
114
|
+
const result = {};
|
|
115
|
+
for (const [k, v] of Object.entries(env)) {
|
|
116
|
+
if (v != null && typeof k === 'string') {
|
|
117
|
+
result[k] = String(v);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
cachedClaudeEnv = { env: result, mtime: currentMtime };
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
/* file not found or parse error, try next path */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
export function saveClaudeSettingsEnv(env) {
|
|
132
|
+
const home = getClaudeConfigHome();
|
|
133
|
+
const claudeSettingsPath = join(home, '.claude', 'settings.json');
|
|
134
|
+
const claudeDir = join(home, '.claude');
|
|
135
|
+
try {
|
|
136
|
+
if (!existsSync(claudeDir)) {
|
|
137
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
138
|
+
}
|
|
139
|
+
let existing = {};
|
|
140
|
+
if (existsSync(claudeSettingsPath)) {
|
|
141
|
+
try {
|
|
142
|
+
existing = JSON.parse(readFileSync(claudeSettingsPath, 'utf-8'));
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// file format error, start fresh
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
existing.env = { ...existing.env, ...env };
|
|
149
|
+
writeFileSync(claudeSettingsPath, JSON.stringify(existing, null, 2), 'utf-8');
|
|
150
|
+
cachedClaudeEnv = null;
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
log.error('Failed to save Claude settings:', error);
|
|
154
|
+
throw new Error(`Failed to save Claude settings: ${error instanceof Error ? error.message : String(error)}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export function normalizeAiCommand(value, fallback) {
|
|
158
|
+
return typeof value === 'string' && AI_COMMANDS.includes(value)
|
|
159
|
+
? value
|
|
160
|
+
: fallback;
|
|
161
|
+
}
|
|
162
|
+
export function hasCodexAuth() {
|
|
163
|
+
if (process.env.OPENAI_API_KEY)
|
|
164
|
+
return true;
|
|
165
|
+
return CODEX_AUTH_PATHS.some((p) => {
|
|
166
|
+
try {
|
|
167
|
+
return existsSync(p) && readFileSync(p, 'utf-8').trim().length > 0;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
export function parseCommaSeparated(value) {
|
|
175
|
+
return value.split(',').map((s) => s.trim()).filter(Boolean);
|
|
176
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { LogLevel } from '../logger.js';
|
|
2
|
+
export type Platform = 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wework' | 'workbuddy';
|
|
3
|
+
export type AiCommand = 'claude' | 'codex' | 'codebuddy';
|
|
4
|
+
export interface Config {
|
|
5
|
+
enabledPlatforms: Platform[];
|
|
6
|
+
telegramBotToken?: string;
|
|
7
|
+
feishuAppId?: string;
|
|
8
|
+
feishuAppSecret?: string;
|
|
9
|
+
weworkCorpId?: string;
|
|
10
|
+
weworkSecret?: string;
|
|
11
|
+
weworkWsUrl?: string;
|
|
12
|
+
dingtalkClientId?: string;
|
|
13
|
+
dingtalkClientSecret?: string;
|
|
14
|
+
dingtalkCardTemplateId?: string;
|
|
15
|
+
qqAppId?: string;
|
|
16
|
+
qqSecret?: string;
|
|
17
|
+
allowedUserIds: string[];
|
|
18
|
+
telegramAllowedUserIds: string[];
|
|
19
|
+
feishuAllowedUserIds: string[];
|
|
20
|
+
qqAllowedUserIds: string[];
|
|
21
|
+
weworkAllowedUserIds: string[];
|
|
22
|
+
dingtalkAllowedUserIds: string[];
|
|
23
|
+
workbuddyAllowedUserIds: string[];
|
|
24
|
+
aiCommand: AiCommand;
|
|
25
|
+
codexCliPath: string;
|
|
26
|
+
codebuddyCliPath: string;
|
|
27
|
+
/** Claude 访问 API 的代理(如 http://127.0.0.1:7890) */
|
|
28
|
+
claudeProxy?: string;
|
|
29
|
+
/** Codex 访问 chatgpt.com 的代理(如 http://127.0.0.1:7890) */
|
|
30
|
+
codexProxy?: string;
|
|
31
|
+
claudeWorkDir: string;
|
|
32
|
+
claudeModel?: string;
|
|
33
|
+
/** 是否跳过 AI 工具的权限确认(默认 true) */
|
|
34
|
+
skipPermissions?: boolean;
|
|
35
|
+
logDir: string;
|
|
36
|
+
logLevel: LogLevel;
|
|
37
|
+
platforms: {
|
|
38
|
+
telegram?: {
|
|
39
|
+
enabled: boolean;
|
|
40
|
+
aiCommand?: AiCommand;
|
|
41
|
+
proxy?: string;
|
|
42
|
+
allowedUserIds: string[];
|
|
43
|
+
};
|
|
44
|
+
feishu?: {
|
|
45
|
+
enabled: boolean;
|
|
46
|
+
aiCommand?: AiCommand;
|
|
47
|
+
allowedUserIds: string[];
|
|
48
|
+
};
|
|
49
|
+
qq?: {
|
|
50
|
+
enabled: boolean;
|
|
51
|
+
aiCommand?: AiCommand;
|
|
52
|
+
allowedUserIds: string[];
|
|
53
|
+
};
|
|
54
|
+
wework?: {
|
|
55
|
+
enabled: boolean;
|
|
56
|
+
aiCommand?: AiCommand;
|
|
57
|
+
allowedUserIds: string[];
|
|
58
|
+
};
|
|
59
|
+
dingtalk?: {
|
|
60
|
+
enabled: boolean;
|
|
61
|
+
aiCommand?: AiCommand;
|
|
62
|
+
allowedUserIds: string[];
|
|
63
|
+
cardTemplateId?: string;
|
|
64
|
+
};
|
|
65
|
+
workbuddy?: {
|
|
66
|
+
enabled: boolean;
|
|
67
|
+
aiCommand?: AiCommand;
|
|
68
|
+
allowedUserIds: string[];
|
|
69
|
+
accessToken?: string;
|
|
70
|
+
refreshToken?: string;
|
|
71
|
+
userId?: string;
|
|
72
|
+
baseUrl?: string;
|
|
73
|
+
guid?: string;
|
|
74
|
+
workspacePath?: string;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export interface FilePlatformTelegram {
|
|
79
|
+
enabled?: boolean;
|
|
80
|
+
botToken?: string;
|
|
81
|
+
aiCommand?: AiCommand;
|
|
82
|
+
allowedUserIds?: string[];
|
|
83
|
+
proxy?: string;
|
|
84
|
+
}
|
|
85
|
+
export interface FilePlatformFeishu {
|
|
86
|
+
enabled?: boolean;
|
|
87
|
+
appId?: string;
|
|
88
|
+
appSecret?: string;
|
|
89
|
+
aiCommand?: AiCommand;
|
|
90
|
+
allowedUserIds?: string[];
|
|
91
|
+
}
|
|
92
|
+
export interface FilePlatformQQ {
|
|
93
|
+
enabled?: boolean;
|
|
94
|
+
appId?: string;
|
|
95
|
+
secret?: string;
|
|
96
|
+
aiCommand?: AiCommand;
|
|
97
|
+
allowedUserIds?: string[];
|
|
98
|
+
}
|
|
99
|
+
export interface FilePlatformWechat {
|
|
100
|
+
enabled?: boolean;
|
|
101
|
+
aiCommand?: AiCommand;
|
|
102
|
+
userId?: string;
|
|
103
|
+
allowedUserIds?: string[];
|
|
104
|
+
workbuddyAccessToken?: string;
|
|
105
|
+
workbuddyRefreshToken?: string;
|
|
106
|
+
workbuddyBaseUrl?: string;
|
|
107
|
+
workbuddyHostId?: string;
|
|
108
|
+
}
|
|
109
|
+
export interface FilePlatformWework {
|
|
110
|
+
enabled?: boolean;
|
|
111
|
+
corpId?: string;
|
|
112
|
+
secret?: string;
|
|
113
|
+
aiCommand?: AiCommand;
|
|
114
|
+
wsUrl?: string;
|
|
115
|
+
allowedUserIds?: string[];
|
|
116
|
+
}
|
|
117
|
+
export interface FilePlatformDingtalk {
|
|
118
|
+
enabled?: boolean;
|
|
119
|
+
clientId?: string;
|
|
120
|
+
clientSecret?: string;
|
|
121
|
+
aiCommand?: AiCommand;
|
|
122
|
+
allowedUserIds?: string[];
|
|
123
|
+
cardTemplateId?: string;
|
|
124
|
+
}
|
|
125
|
+
export interface FilePlatformWorkBuddy {
|
|
126
|
+
enabled?: boolean;
|
|
127
|
+
aiCommand?: AiCommand;
|
|
128
|
+
allowedUserIds?: string[];
|
|
129
|
+
accessToken?: string;
|
|
130
|
+
refreshToken?: string;
|
|
131
|
+
userId?: string;
|
|
132
|
+
baseUrl?: string;
|
|
133
|
+
guid?: string;
|
|
134
|
+
workspacePath?: string;
|
|
135
|
+
}
|
|
136
|
+
export interface FileToolClaude {
|
|
137
|
+
cliPath?: string;
|
|
138
|
+
workDir?: string;
|
|
139
|
+
skipPermissions?: boolean;
|
|
140
|
+
/** HTTP/HTTPS 代理,用于访问 Claude API(如 http://127.0.0.1:7890) */
|
|
141
|
+
proxy?: string;
|
|
142
|
+
/** Claude API 配置(优先级:环境变量 > tools.claude.env > ~/.claude/settings.json) */
|
|
143
|
+
env?: Record<string, string>;
|
|
144
|
+
}
|
|
145
|
+
export interface FileToolCodex {
|
|
146
|
+
cliPath?: string;
|
|
147
|
+
workDir?: string;
|
|
148
|
+
/** HTTP/HTTPS 代理,用于访问 chatgpt.com(如 http://127.0.0.1:7890) */
|
|
149
|
+
proxy?: string;
|
|
150
|
+
}
|
|
151
|
+
export interface FileToolCodeBuddy {
|
|
152
|
+
cliPath?: string;
|
|
153
|
+
}
|
|
154
|
+
export interface FileConfig {
|
|
155
|
+
telegramBotToken?: string;
|
|
156
|
+
feishuAppId?: string;
|
|
157
|
+
feishuAppSecret?: string;
|
|
158
|
+
allowedUserIds?: string[];
|
|
159
|
+
platforms?: {
|
|
160
|
+
telegram?: FilePlatformTelegram;
|
|
161
|
+
feishu?: FilePlatformFeishu;
|
|
162
|
+
qq?: FilePlatformQQ;
|
|
163
|
+
wechat?: FilePlatformWechat;
|
|
164
|
+
wework?: FilePlatformWework;
|
|
165
|
+
dingtalk?: FilePlatformDingtalk;
|
|
166
|
+
workbuddy?: FilePlatformWorkBuddy;
|
|
167
|
+
};
|
|
168
|
+
env?: Record<string, string>;
|
|
169
|
+
aiCommand?: string;
|
|
170
|
+
tools?: {
|
|
171
|
+
claude?: FileToolClaude;
|
|
172
|
+
codex?: FileToolCodex;
|
|
173
|
+
codebuddy?: FileToolCodeBuddy;
|
|
174
|
+
};
|
|
175
|
+
logDir?: string;
|
|
176
|
+
logLevel?: LogLevel;
|
|
177
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|