imtoagent 0.2.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.
Files changed (47) hide show
  1. package/README.md +234 -0
  2. package/bin/imtoagent +453 -0
  3. package/index.ts +1129 -0
  4. package/modules/agent/claude-adapter.ts +258 -0
  5. package/modules/agent/claude.ts +160 -0
  6. package/modules/agent/codex-adapter.ts +232 -0
  7. package/modules/agent/codex-exec-server.ts +513 -0
  8. package/modules/agent/codex.ts +275 -0
  9. package/modules/agent/opencode-adapter.ts +308 -0
  10. package/modules/agent/opencode.ts +247 -0
  11. package/modules/bot-context.ts +26 -0
  12. package/modules/capabilities.ts +189 -0
  13. package/modules/cli/setup.ts +424 -0
  14. package/modules/core/config.ts +275 -0
  15. package/modules/core/error.ts +124 -0
  16. package/modules/core/index.ts +39 -0
  17. package/modules/core/runtime.ts +282 -0
  18. package/modules/core/session.ts +256 -0
  19. package/modules/core/stats.ts +92 -0
  20. package/modules/core/types.ts +250 -0
  21. package/modules/im/feishu.ts +731 -0
  22. package/modules/im/telegram.ts +639 -0
  23. package/modules/im/wechat.ts +1094 -0
  24. package/modules/im/wecom.ts +603 -0
  25. package/modules/media/feishu-inbound-adapter.ts +108 -0
  26. package/modules/media/index.ts +27 -0
  27. package/modules/media/media-store.ts +273 -0
  28. package/modules/media/resolver.ts +178 -0
  29. package/modules/media/telegram-inbound-adapter.ts +124 -0
  30. package/modules/media/types.ts +76 -0
  31. package/modules/prompt-builder.ts +123 -0
  32. package/modules/proxy/anthropic-proxy.ts +1083 -0
  33. package/modules/proxy/codex-proxy.ts +657 -0
  34. package/modules/rate-limiter.ts +58 -0
  35. package/modules/types.ts +144 -0
  36. package/modules/utils/backend-check.ts +121 -0
  37. package/modules/utils/paths.ts +218 -0
  38. package/package.json +53 -0
  39. package/scripts/postinstall.ts +70 -0
  40. package/templates/config.template.json +57 -0
  41. package/templates/opencode.template.json +28 -0
  42. package/templates/providers.template.json +19 -0
  43. package/templates/soul.template/identity.md +6 -0
  44. package/templates/soul.template/profile.md +11 -0
  45. package/templates/soul.template/rules.md +7 -0
  46. package/templates/soul.template/skills.md +3 -0
  47. package/templates/soul.template/workspace.md +4 -0
@@ -0,0 +1,58 @@
1
+ // 滑动窗口请求限流器
2
+ // per-chatId 滑动窗口,防止短时间内发送过多消息
3
+
4
+ interface WindowEntry {
5
+ timestamps: number[];
6
+ }
7
+
8
+ interface RateLimitConfig {
9
+ maxRequests: number;
10
+ windowMs: number;
11
+ }
12
+
13
+ const windows = new Map<string, WindowEntry>();
14
+
15
+ const DEFAULT_CONFIG: RateLimitConfig = {
16
+ maxRequests: 30,
17
+ windowMs: 60_000,
18
+ };
19
+
20
+ let currentConfig: RateLimitConfig = { ...DEFAULT_CONFIG };
21
+
22
+ export function setRateLimitConfig(cfg: RateLimitConfig) {
23
+ currentConfig = cfg;
24
+ }
25
+
26
+ export function checkRateLimit(chatId: string): { allowed: boolean; retryAfter?: number; remaining?: number } {
27
+ const now = Date.now();
28
+ let entry = windows.get(chatId);
29
+
30
+ if (!entry) {
31
+ entry = { timestamps: [] };
32
+ windows.set(chatId, entry);
33
+ }
34
+
35
+ // 清理窗口外的时间戳
36
+ entry.timestamps = entry.timestamps.filter(ts => now - ts < currentConfig.windowMs);
37
+
38
+ if (entry.timestamps.length >= currentConfig.maxRequests) {
39
+ const oldestInWindow = entry.timestamps[0];
40
+ const retryAfter = Math.ceil((oldestInWindow + currentConfig.windowMs - now) / 1000);
41
+ return { allowed: false, retryAfter, remaining: 0 };
42
+ }
43
+
44
+ entry.timestamps.push(now);
45
+
46
+ // 定期清理过期条目(低概率触发,不影响性能)
47
+ if (Math.random() < 0.01) {
48
+ for (const [id, e] of windows.entries()) {
49
+ e.timestamps = e.timestamps.filter(ts => now - ts < currentConfig.windowMs);
50
+ if (e.timestamps.length === 0) windows.delete(id);
51
+ }
52
+ }
53
+
54
+ return {
55
+ allowed: true,
56
+ remaining: currentConfig.maxRequests - entry.timestamps.length,
57
+ };
58
+ }
@@ -0,0 +1,144 @@
1
+ // IMtoAgent 模块接口定义
2
+ // Agent 模块和 IM 模块之间的标准合同
3
+
4
+ // ================================================================
5
+ // Session 数据结构
6
+ // ================================================================
7
+
8
+ /** 调用统计 */
9
+ export interface CallStats {
10
+ calls: number;
11
+ totalTurns: number;
12
+ totalInputTokens: number;
13
+ totalOutputTokens: number;
14
+ totalCostUSD: number;
15
+ totalDurationMs: number;
16
+ }
17
+
18
+ /** 会话数据(替代 any) */
19
+ export interface SessionData {
20
+ userId: string;
21
+ sdkSessionId?: string;
22
+ codexThreadId?: string;
23
+ ocSessionId?: string;
24
+ cwd?: string;
25
+ permissionMode?: 'bypassPermissions' | 'default';
26
+ codexMode?: string;
27
+ startFresh?: boolean;
28
+ stats: CallStats;
29
+ recentMessages: string[];
30
+ lastUsed: number;
31
+ }
32
+
33
+ // ================================================================
34
+ // Agent 模块接口
35
+ // ================================================================
36
+
37
+ /** Bot 提供给 Agent 模块的上下文 */
38
+ export interface AgentContext {
39
+ readonly name: string;
40
+ readonly backend: 'claude' | 'codex' | 'opencode';
41
+ readonly activeModel: string;
42
+ readonly modelAliases: Record<string, string>;
43
+ readonly defaultCwd: string;
44
+ /** IM 模块实例 */
45
+ readonly imModule?: IMModule;
46
+
47
+ // 发送回复给 IM
48
+ reply(chatId: string, text: string): Promise<void>;
49
+
50
+ // 推送进度消息
51
+ sendProgress(chatId: string, text: string): Promise<void>;
52
+
53
+ // 工具日志
54
+ addToolLog(chatId: string, info: { name: string; summary: string }): void;
55
+ flushToolLog(chatId: string): Promise<void>;
56
+
57
+ // 格式化发送(parseToBlocks → sendBlocks/reply 统一入口)
58
+ sendFormattedReply(chatId: string, response: string): Promise<void>;
59
+
60
+ // 统计累加(统一 token/cost 统计)
61
+ accumulateStats(session: SessionData, usage: { inputTokens: number; outputTokens: number; costUSD?: number; durationMs?: number; numTurns?: number }): void;
62
+
63
+ // 会话持久化
64
+ persistSession(chatId: string, session: SessionData): void;
65
+ loadSession(chatId: string): SessionData | null;
66
+ deleteSession(chatId: string): void;
67
+
68
+ // 模型配置持久化
69
+ saveModelConfig(config: { activeModel: string; modelAliases: Record<string, string> }): void;
70
+ loadModelConfig(): { activeModel: string; modelAliases: Record<string, string> };
71
+ }
72
+
73
+ /** Agent 模块必须实现的接口 */
74
+ export interface AgentModule {
75
+ /** 收到用户消息,处理并回复 */
76
+ handleMessage(chatId: string, text: string, session: SessionData): Promise<void>;
77
+ }
78
+
79
+ // ================================================================
80
+ // IM 模块接口
81
+ // ================================================================
82
+
83
+ /** IM 模块提供给 Bot 的消息回调 */
84
+ export type MessageHandler = (chatId: string, text: string, userId: string, attachments?: import('./core/types').MessageAttachment[]) => Promise<void>;
85
+
86
+ /** IM 模块声明的输出能力 */
87
+ export interface IMCapabilities {
88
+ text: boolean;
89
+ codeBlock: boolean;
90
+ cardMessage: boolean;
91
+ fileSend: boolean;
92
+ imageSend: boolean;
93
+ audioSend: boolean;
94
+ buttonAction: boolean;
95
+ maxTextLength: number;
96
+ }
97
+
98
+ /** Agent 可调用的输出工具定义 */
99
+ export interface OutputToolDef {
100
+ name: string;
101
+ description: string;
102
+ parameters: Record<string, { type: string; description: string; required?: boolean }>;
103
+ }
104
+
105
+ /** IM 模块必须实现的接口 */
106
+ export interface IMModule {
107
+ /** 发送文本回复 */
108
+ reply(chatId: string, text: string, maxLen?: number): Promise<void>;
109
+
110
+ /** 推送进度/工具日志 */
111
+ sendProgress(chatId: string, text: string): Promise<void>;
112
+
113
+ /** 获取 IM 输出能力 */
114
+ getCapabilities(): IMCapabilities;
115
+
116
+ /** 发送富文本块(代码块、卡片、图片等) */
117
+ sendBlocks(chatId: string, blocks: any[]): Promise<void>;
118
+
119
+ /** 发送图片 */
120
+ sendImage(chatId: string, imageKey: string, alt?: string): Promise<void>;
121
+
122
+ /** 发送文件 */
123
+ sendFile(chatId: string, fileKey: string, fileName: string): Promise<void>;
124
+
125
+ /** 启动消息监听 */
126
+ start(handler: MessageHandler): void;
127
+
128
+ /** 停止 */
129
+ stop(): void;
130
+ }
131
+
132
+ // ================================================================
133
+ // Bot 配置
134
+ // ================================================================
135
+
136
+ export interface BotConfig {
137
+ name: string;
138
+ backend: 'claude' | 'codex' | 'opencode';
139
+ appId: string;
140
+ appSecret: string;
141
+ cwd?: string;
142
+ /** IM 平台类型(默认 feishu) */
143
+ im?: 'feishu' | 'telegram' | 'wecom' | 'wechat';
144
+ }
@@ -0,0 +1,121 @@
1
+ // ================================================================
2
+ // backend-check.ts — 检测后端 Agent 是否已安装
3
+ // ================================================================
4
+
5
+ import { execSync } from 'child_process';
6
+
7
+ export interface BackendInfo {
8
+ type: 'claude' | 'codex' | 'opencode';
9
+ label: string;
10
+ installed: boolean;
11
+ version: string | null;
12
+ installHint: string;
13
+ }
14
+
15
+ const BACKEND_DEFS: Omit<BackendInfo, 'installed' | 'version'>[] = [
16
+ { type: 'claude', label: 'Claude Code', installHint: 'npm install -g @anthropic-ai/claude-agent-sdk' },
17
+ { type: 'codex', label: 'Codex', installHint: 'npm install -g @openai/codex' },
18
+ { type: 'opencode', label: 'OpenCode', installHint: 'npm install -g opencode' },
19
+ ];
20
+
21
+ function checkOne(b: Omit<BackendInfo, 'installed' | 'version'>): BackendInfo {
22
+ try {
23
+ let version: string | null = null;
24
+ if (b.type === 'claude') {
25
+ version = execSync('claude --version', { encoding: 'utf-8', timeout: 5000 }).trim();
26
+ } else if (b.type === 'codex') {
27
+ version = execSync('codex --version', { encoding: 'utf-8', timeout: 5000 }).trim();
28
+ } else {
29
+ version = execSync('opencode version', { encoding: 'utf-8', timeout: 5000 }).trim();
30
+ }
31
+ return { ...b, installed: true, version };
32
+ } catch {
33
+ return { ...b, installed: false, version: null };
34
+ }
35
+ }
36
+
37
+ export function checkAllBackends(): BackendInfo[] {
38
+ return BACKEND_DEFS.map(checkOne);
39
+ }
40
+
41
+ export function checkBackend(type: 'claude' | 'codex' | 'opencode'): BackendInfo {
42
+ const b = BACKEND_DEFS.find(x => x.type === type);
43
+ if (!b) return { type, label: type, installed: false, version: null, installHint: '' };
44
+ return checkOne(b);
45
+ }
46
+
47
+ export function formatBackendStatus(backends: BackendInfo[]): string {
48
+ return backends.map(b => {
49
+ const icon = b.installed ? '✅' : '❌';
50
+ const ver = b.version ? ` v${b.version}` : '';
51
+ const hint = b.installed ? '' : ` → ${b.installHint}`;
52
+ return ` ${icon} ${b.label}${ver}${hint}`;
53
+ }).join('\n');
54
+ }
55
+
56
+ // ================================================================
57
+ // 自动安装后端 CLI
58
+ // ================================================================
59
+
60
+ /**
61
+ * 自动安装缺失的后端 CLI
62
+ * 使用 Bun.spawn 流式输出 npm install -g 的进度
63
+ */
64
+ export async function installBackend(
65
+ type: 'claude' | 'codex' | 'opencode',
66
+ ): Promise<boolean> {
67
+ const b = BACKEND_DEFS.find((x) => x.type === type);
68
+ if (!b) {
69
+ console.error(`❌ 未知后端类型: ${type}`);
70
+ return false;
71
+ }
72
+
73
+ console.log(`\n📦 正在安装 ${b.label}...`);
74
+ console.log(` 命令: ${b.installHint}\n`);
75
+
76
+ try {
77
+ const child = Bun.spawn(b.installHint.split(' '), {
78
+ stdout: 'pipe',
79
+ stderr: 'pipe',
80
+ stdin: 'inherit',
81
+ });
82
+
83
+ const decoder = new TextDecoder();
84
+
85
+ // 实时输出 stdout
86
+ const stdoutReader = child.stdout.getReader();
87
+ while (true) {
88
+ const { done, value } = await stdoutReader.read();
89
+ if (done) break;
90
+ process.stdout.write(decoder.decode(value, { stream: true }));
91
+ }
92
+
93
+ // 实时输出 stderr
94
+ const stderrReader = child.stderr.getReader();
95
+ while (true) {
96
+ const { done, value } = await stderrReader.read();
97
+ if (done) break;
98
+ process.stderr.write(decoder.decode(value, { stream: true }));
99
+ }
100
+
101
+ const exitCode = await child.exited;
102
+
103
+ if (exitCode !== 0) {
104
+ console.error(`\n❌ ${b.label} 安装失败 (退出码: ${exitCode})`);
105
+ return false;
106
+ }
107
+
108
+ // 安装完成后验证
109
+ const info = checkOne(b);
110
+ if (info.installed) {
111
+ console.log(`\n✅ ${b.label} 安装成功! 版本: ${info.version}`);
112
+ return true;
113
+ } else {
114
+ console.error(`\n❌ ${b.label} 安装后仍未检测到,请手动运行: ${b.installHint}`);
115
+ return false;
116
+ }
117
+ } catch (e: any) {
118
+ console.error(`\n❌ 安装 ${b.label} 时出错: ${e.message || e}`);
119
+ return false;
120
+ }
121
+ }
@@ -0,0 +1,218 @@
1
+ // ================================================================
2
+ // 路径解析模块 — 首次部署自动初始化的核心地基
3
+ // ================================================================
4
+ // 职责:
5
+ // 1. 统一解析数据目录(~/.imtoagent/ 或开发时的 cwd)
6
+ // 2. 统一解析 npm 包安装目录(读取模板用)
7
+ // 3. 兼容旧开发模式(bun run index.ts 在旧项目目录)
8
+ // 4. 首次部署自动初始化数据目录 + 配置文件
9
+ // ================================================================
10
+
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+
14
+ // ===== 缓存 =====
15
+ let _dataDir: string | null = null;
16
+ let _pkgDir: string | null = null;
17
+
18
+ // ===== 数据目录解析 =====
19
+ /**
20
+ * 返回 imtoagent 的用户数据目录(读写)。
21
+ *
22
+ * 优先级:
23
+ * 1. IMTOAGENT_HOME 环境变量 → 如果有 config.json 则直接用
24
+ * 2. ~/.imtoagent/ 存在且有 config.json → 用
25
+ * 3. cwd 有 config.json → 开发模式(从 cwd 初始化 ~/.imtoagent)
26
+ * 4. 从包目录模板初始化 ~/.imtoagent(npm 全局安装首次运行)
27
+ * 5. 包目录 fallback(极端情况)
28
+ *
29
+ * 关键改进:IMTOAGENT_HOME 不再是"强制覆盖"——如果目录为空,
30
+ * 会继续尝试从其他来源找到配置文件,并自动初始化。
31
+ */
32
+ export function getDataDir(): string {
33
+ if (_dataDir) return _dataDir;
34
+
35
+ const home = process.env.HOME || process.env.USERPROFILE?.replace(/\\/g, '/') || '';
36
+ const dotDir = path.join(home, '.imtoagent');
37
+ const envHome = process.env.IMTOAGENT_HOME || '';
38
+
39
+ // ====== 第 1 步:查找已有的配置文件 ======
40
+ const candidates: { dir: string; label: string }[] = [];
41
+
42
+ // IMTOAGENT_HOME(优先检查,但不强制)
43
+ if (envHome && fs.existsSync(path.join(envHome, 'config.json'))) {
44
+ candidates.push({ dir: envHome, label: 'IMTOAGENT_HOME' });
45
+ }
46
+
47
+ // ~/.imtoagent/
48
+ if (fs.existsSync(path.join(dotDir, 'config.json'))) {
49
+ candidates.push({ dir: dotDir, label: '~/.imtoagent' });
50
+ }
51
+
52
+ // cwd(开发模式)
53
+ const cwd = process.cwd();
54
+ if (fs.existsSync(path.join(cwd, 'config.json'))) {
55
+ candidates.push({ dir: cwd, label: 'cwd 开发模式' });
56
+ }
57
+
58
+ if (candidates.length > 0) {
59
+ // 有现成配置,直接用最优先的那个
60
+ const chosen = candidates[0];
61
+ _dataDir = chosen.dir;
62
+ console.log(`[Paths] 数据目录: ${_dataDir} (${chosen.label})`);
63
+ return _dataDir;
64
+ }
65
+
66
+ // ====== 第 2 步:没有配置文件 → 自动初始化 ======
67
+ _dataDir = initDataDir(dotDir, envHome);
68
+ return _dataDir;
69
+ }
70
+
71
+ /**
72
+ * 首次部署:自动创建 ~/.imtoagent/ 并初始化配置文件。
73
+ *
74
+ * 配置文件来源(按优先级):
75
+ * 1. IMTOAGENT_HOME 下的 config.json(目录存在但没文件)
76
+ * 2. cwd 下的 config.json / providers.json(手动部署/git clone)
77
+ * 3. 包安装目录的 templates/(npm 全局安装)
78
+ */
79
+ function initDataDir(dotDir: string, envHome: string): string {
80
+ const target = dotDir; // 统一使用 ~/.imtoagent/
81
+
82
+ // 确定配置文件来源
83
+ let sourceDir: string | null = null;
84
+ let sourceLabel = '';
85
+
86
+ if (envHome && fs.existsSync(path.join(envHome, 'config.json'))) {
87
+ sourceDir = envHome;
88
+ sourceLabel = 'IMTOAGENT_HOME';
89
+ } else if (fs.existsSync(path.join(process.cwd(), 'config.json'))) {
90
+ sourceDir = process.cwd();
91
+ sourceLabel = 'cwd';
92
+ } else {
93
+ const pkgDir = getPkgDir();
94
+ if (fs.existsSync(path.join(pkgDir, 'templates', 'config.template.json'))) {
95
+ sourceDir = path.join(pkgDir, 'templates');
96
+ sourceLabel = '包模板';
97
+ }
98
+ }
99
+
100
+ // 创建目录结构
101
+ fs.mkdirSync(target, { recursive: true });
102
+ fs.mkdirSync(path.join(target, 'logs'), { recursive: true });
103
+ fs.mkdirSync(path.join(target, 'sessions'), { recursive: true });
104
+
105
+ if (sourceDir && sourceLabel === 'cwd') {
106
+ // 开发模式:直接拷贝项目目录下的配置
107
+ copyIfExists(sourceDir, target, 'config.json');
108
+ copyIfExists(sourceDir, target, 'providers.json');
109
+ copyIfExists(sourceDir, target, 'opencode.json');
110
+ } else if (sourceDir && sourceLabel === '包模板') {
111
+ // npm 安装:从模板拷贝(去掉 .template 后缀)
112
+ copyTemplateIfExists(sourceDir, target, 'config.template.json', 'config.json');
113
+ copyTemplateIfExists(sourceDir, target, 'providers.template.json', 'providers.json');
114
+ copyTemplateIfExists(sourceDir, target, 'opencode.template.json', 'opencode.json');
115
+ // 拷贝 soul 模板
116
+ const soulSrc = path.join(sourceDir, 'soul.template');
117
+ if (fs.existsSync(soulSrc)) {
118
+ copyDirSync(soulSrc, path.join(target, 'soul'));
119
+ }
120
+ }
121
+
122
+ console.log(`[Paths] ✨ 首次初始化数据目录: ${target} (来源: ${sourceLabel || '默认模板'})`);
123
+ console.log(`[Paths] 请编辑 ${path.join(target, 'config.json')} 配置你的凭证`);
124
+
125
+ return target;
126
+ }
127
+
128
+ function copyIfExists(src: string, dst: string, filename: string) {
129
+ const srcPath = path.join(src, filename);
130
+ const dstPath = path.join(dst, filename);
131
+ if (fs.existsSync(srcPath) && !fs.existsSync(dstPath)) {
132
+ fs.copyFileSync(srcPath, dstPath);
133
+ }
134
+ }
135
+
136
+ function copyTemplateIfExists(src: string, dst: string, templateName: string, outName: string) {
137
+ const srcPath = path.join(src, templateName);
138
+ const dstPath = path.join(dst, outName);
139
+ if (fs.existsSync(srcPath) && !fs.existsSync(dstPath)) {
140
+ fs.copyFileSync(srcPath, dstPath);
141
+ }
142
+ }
143
+
144
+ function copyDirSync(src: string, dst: string) {
145
+ if (!fs.existsSync(dst)) fs.mkdirSync(dst, { recursive: true });
146
+ for (const entry of fs.readdirSync(src)) {
147
+ const srcPath = path.join(src, entry);
148
+ const dstPath = path.join(dst, entry);
149
+ if (fs.statSync(srcPath).isDirectory()) {
150
+ copyDirSync(srcPath, dstPath);
151
+ } else {
152
+ fs.copyFileSync(srcPath, dstPath);
153
+ }
154
+ }
155
+ }
156
+
157
+ /**
158
+ * 返回 npm 包安装目录(只读,模板读取用)。
159
+ *
160
+ * import.meta.dirname 在 Bun/Node ESM 中返回当前文件所在目录。
161
+ * 对于全局安装的包,这就是 /usr/local/lib/node_modules/imtoagent/。
162
+ */
163
+ export function getPkgDir(): string {
164
+ if (_pkgDir) return _pkgDir;
165
+
166
+ // import.meta.dirname 是当前文件(paths.ts)所在目录 → modules/utils/
167
+ // 向上两级就是包根目录
168
+ _pkgDir = path.resolve(import.meta.dirname, '../..');
169
+ return _pkgDir;
170
+ }
171
+
172
+ // ===== 便捷路径函数 =====
173
+
174
+ export function getConfigPath(): string {
175
+ return path.join(getDataDir(), 'config.json');
176
+ }
177
+
178
+ export function getProvidersPath(): string {
179
+ return path.join(getDataDir(), 'providers.json');
180
+ }
181
+
182
+ export function getOpencodeConfigPath(): string {
183
+ return path.join(getDataDir(), 'opencode.json');
184
+ }
185
+
186
+ export function getSessionsDir(): string {
187
+ return path.join(getDataDir(), 'sessions');
188
+ }
189
+
190
+ export function getLogsDir(): string {
191
+ return path.join(getDataDir(), 'logs');
192
+ }
193
+
194
+ export function getSoulDir(botName: string): string {
195
+ return path.join(getDataDir(), 'soul', botName);
196
+ }
197
+
198
+ export function getRestoreMarkerPath(): string {
199
+ return path.join(getSessionsDir(), '.restore');
200
+ }
201
+
202
+ export function getTemplatePath(relativePath: string): string {
203
+ const tplPath = path.join(getPkgDir(), 'templates', relativePath);
204
+ if (!fs.existsSync(tplPath)) console.warn(`⚠️ 模板不存在: ${tplPath}`);
205
+ return tplPath;
206
+ }
207
+
208
+ export function getTemplateSoulPath(filename: string): string {
209
+ const tplPath = path.join(getPkgDir(), 'templates', 'soul.template', filename);
210
+ if (!fs.existsSync(tplPath)) console.warn(`⚠️ 灵魂模板不存在: ${tplPath}`);
211
+ return tplPath;
212
+ }
213
+
214
+ // ===== 重置缓存(测试用) =====
215
+ export function resetPathCache(): void {
216
+ _dataDir = null;
217
+ _pkgDir = null;
218
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "imtoagent",
3
+ "version": "0.2.0",
4
+ "description": "IM ↔ Agent 统一网关 — 飞书/Telegram/微信/企业微信对接 Claude Code/Codex/OpenCode",
5
+ "type": "module",
6
+ "bin": {
7
+ "imtoagent": "./bin/imtoagent"
8
+ },
9
+ "files": [
10
+ "index.ts",
11
+ "modules/",
12
+ "bin/",
13
+ "templates/",
14
+ "scripts/",
15
+ "README.md"
16
+ ],
17
+ "scripts": {
18
+ "postinstall": "bun run scripts/postinstall.ts",
19
+ "start": "bun run index.ts"
20
+ },
21
+ "dependencies": {
22
+ "@anthropic-ai/claude-agent-sdk": "^0.2.119",
23
+ "@larksuiteoapi/node-sdk": "^1.62.0",
24
+ "@wecom/aibot-node-sdk": "^1.0.7",
25
+ "qrcode": "^1.5.4"
26
+ },
27
+ "engines": {
28
+ "bun": ">=1.0.0"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/YOUR_USERNAME/imtoagent.git"
33
+ },
34
+ "keywords": [
35
+ "im",
36
+ "gateway",
37
+ "claude-code",
38
+ "codex",
39
+ "opencode",
40
+ "feishu",
41
+ "telegram",
42
+ "wechat",
43
+ "wecom",
44
+ "ai-agent",
45
+ "bot-framework"
46
+ ],
47
+ "author": "Keyi",
48
+ "license": "MIT",
49
+ "bugs": {
50
+ "url": "https://github.com/YOUR_USERNAME/imtoagent/issues"
51
+ },
52
+ "homepage": "https://github.com/YOUR_USERNAME/imtoagent#readme"
53
+ }
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env bun
2
+ // ================================================================
3
+ // postinstall.ts — npm 安装后引导脚本
4
+ // ================================================================
5
+ // package.json 中 "scripts": { "postinstall": "bun run scripts/postinstall.ts" }
6
+ // 安装后自动运行,检测是否需要初始化配置
7
+ // 如果是全新安装且终端交互,自动引导进入 setup
8
+ // ================================================================
9
+
10
+ import * as fs from 'fs';
11
+ import * as path from 'path';
12
+ import { execSync } from 'child_process';
13
+
14
+ const HOME = process.env.HOME || process.env.USERPROFILE || '';
15
+ const DATA_DIR = path.join(HOME, '.imtoagent');
16
+
17
+ try {
18
+ const configExists = fs.existsSync(path.join(DATA_DIR, 'config.json'));
19
+
20
+ if (configExists) {
21
+ console.log(`
22
+ ✅ imtoagent 升级成功!
23
+ 数据目录: ${DATA_DIR}
24
+ 配置文件保持不变,无需重新配置。
25
+ 运行 "imtoagent start" 启动网关。
26
+ `);
27
+ } else {
28
+ console.log(`
29
+ ╔══════════════════════════════════════════════════════════╗
30
+ ║ ║
31
+ ║ 🎉 imtoagent 安装成功! ║
32
+ ║ ║
33
+ ║ 首次使用需要配置 IM 凭证和模型供应商 ║
34
+ ║ ║
35
+ ╚══════════════════════════════════════════════════════════╝
36
+ `);
37
+
38
+ // 检测是否为交互式终端,是则自动引导进入 setup
39
+ if (process.stdin.isTTY) {
40
+ const readline = await import('readline');
41
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
42
+ const answer = await new Promise<string>(resolve => {
43
+ rl.question('是否立即运行配置向导?[Y/n]: ', resolve);
44
+ });
45
+ rl.close();
46
+
47
+ const yes = answer.trim().toLowerCase();
48
+ if (yes === '' || yes === 'y' || yes === 'yes') {
49
+ console.log('\n🚀 启动配置向导...\n');
50
+ // 调用 setup 向导
51
+ const pkgDir = path.resolve(import.meta.dirname, '..');
52
+ execSync('bun run bin/imtoagent setup', {
53
+ cwd: pkgDir,
54
+ stdio: 'inherit',
55
+ env: { ...process.env },
56
+ });
57
+ } else {
58
+ console.log('\n稍后运行 "imtoagent setup" 即可开始配置。');
59
+ }
60
+ } else {
61
+ console.log(' 运行 "imtoagent setup" 开始配置');
62
+ console.log(' 然后运行 "imtoagent start" 启动网关\n');
63
+ }
64
+ }
65
+ } catch (e: any) {
66
+ // 静默失败,不影响安装
67
+ if (e.message && !e.message.includes('readline')) {
68
+ console.error(`[postinstall] 提示失败: ${e.message}`);
69
+ }
70
+ }