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,124 @@
1
+ // ================================================================
2
+ // TelegramInboundAdapter — Telegram 平台媒体下载适配器
3
+ // ================================================================
4
+ // 实现 InboundMediaAdapter 接口,负责从 Telegram Bot API 下载消息附件 buffer
5
+ // 不负责:存储、MIME sniff、扩展名处理(由 MediaStore 处理)
6
+ //
7
+ // Telegram Bot API 文件下载流程:
8
+ // 1. 调用 /getFile 传入 file_id → 获取 file_path
9
+ // 2. 通过 https://api.telegram.org/file/bot<token>/<file_path> 下载
10
+ // ================================================================
11
+
12
+ import type { InboundMediaAdapter, DownloadedMedia, MediaResourceType } from './types';
13
+
14
+ export interface TelegramInboundAdapterOptions {
15
+ token: string;
16
+ proxy?: string;
17
+ /** 可选:复用父类的代理感知 fetch,避免重复初始化 dispatcher */
18
+ fetchFn?: (url: string, init?: RequestInit) => Promise<Response>;
19
+ }
20
+
21
+ export class TelegramInboundAdapter implements InboundMediaAdapter {
22
+ readonly platform = 'telegram';
23
+ private token: string;
24
+ private apiUrl: string;
25
+ private fileUrl: string;
26
+ private proxy?: string;
27
+ private dispatcher?: any; // undici Dispatcher
28
+ private fetchFn?: (url: string, init?: RequestInit) => Promise<Response>;
29
+
30
+ constructor(options: TelegramInboundAdapterOptions) {
31
+ this.token = options.token;
32
+ this.proxy = options.proxy;
33
+ this.fetchFn = options.fetchFn;
34
+ this.apiUrl = `https://api.telegram.org/bot${this.token}`;
35
+ this.fileUrl = `https://api.telegram.org/file/bot${this.token}/`;
36
+
37
+ // 仅在没有外部 fetchFn 时,自己初始化代理 dispatcher(undici 风格)
38
+ if (!this.fetchFn && this.proxy) {
39
+ try {
40
+ const ProxyAgent = (globalThis as any).ProxyAgent;
41
+ if (ProxyAgent) {
42
+ this.dispatcher = new ProxyAgent(this.proxy);
43
+ console.log(`[TelegramInbound] 已配置代理 dispatcher: ${this.proxy}`);
44
+ } else {
45
+ console.log(`[TelegramInbound] ⚠️ 代理已配置但 ProxyAgent 不可用,将尝试直接连接`);
46
+ }
47
+ } catch (e: any) {
48
+ console.log(`[TelegramInbound] ⚠️ 代理 dispatcher 初始化失败: ${e.message}`);
49
+ }
50
+ }
51
+ }
52
+
53
+ async downloadResource(
54
+ messageId: string,
55
+ resourceKey: string,
56
+ type: MediaResourceType,
57
+ fileName?: string
58
+ ): Promise<DownloadedMedia | null> {
59
+ try {
60
+ // Step 1: 调用 getFile 获取 file_path
61
+ const fileResp = await this._fetch(`${this.apiUrl}/getFile`, {
62
+ method: 'POST',
63
+ headers: { 'Content-Type': 'application/json' },
64
+ body: JSON.stringify({ file_id: resourceKey }),
65
+ });
66
+
67
+ if (!fileResp.ok) {
68
+ console.error(`[TelegramInbound] getFile 失败: HTTP ${fileResp.status} (file_id=${resourceKey.slice(-10)})`);
69
+ return null;
70
+ }
71
+
72
+ const fileData = await fileResp.json();
73
+ if (!fileData.ok || !fileData.result?.file_path) {
74
+ console.error(`[TelegramInbound] getFile 返回无效: ${JSON.stringify(fileData).slice(0, 200)}`);
75
+ return null;
76
+ }
77
+
78
+ const filePath = fileData.result.file_path;
79
+ const fileSize = fileData.result.file_size;
80
+
81
+ if (fileSize && fileSize > 20 * 1024 * 1024) {
82
+ console.log(`[TelegramInbound] 文件过大 (${fileSize} bytes),跳过下载`);
83
+ return null;
84
+ }
85
+
86
+ // Step 2: 下载文件内容
87
+ const downloadUrl = this.fileUrl + filePath;
88
+ const contentResp = await this._fetch(downloadUrl);
89
+
90
+ if (!contentResp.ok) {
91
+ console.error(`[TelegramInbound] 下载文件失败: HTTP ${contentResp.status} (path=${filePath})`);
92
+ return null;
93
+ }
94
+
95
+ const buffer = Buffer.from(await contentResp.arrayBuffer());
96
+ const contentType = contentResp.headers.get('content-type')?.split(';')[0].trim().toLowerCase() || undefined;
97
+
98
+ // 如果 Telegram 没有提供文件名,尝试从 file_path 提取
99
+ const finalFileName = fileName || filePath.split('/').pop() || 'unknown';
100
+
101
+ return {
102
+ buffer,
103
+ fileName: finalFileName,
104
+ contentType,
105
+ sourceKey: resourceKey,
106
+ };
107
+ } catch (e: any) {
108
+ console.error(`[TelegramInbound] 下载资源异常: ${e.message}`);
109
+ return null;
110
+ }
111
+ }
112
+
113
+ private async _fetch(url: string, init?: RequestInit): Promise<Response> {
114
+ // 优先使用外部注入的 fetchFn(复用父类的代理感知 fetch)
115
+ if (this.fetchFn) {
116
+ return this.fetchFn(url, init);
117
+ }
118
+ // 否则使用自己的 dispatcher
119
+ if (this.dispatcher) {
120
+ return fetch(url, { ...init, dispatcher: this.dispatcher } as any);
121
+ }
122
+ return fetch(url, init);
123
+ }
124
+ }
@@ -0,0 +1,76 @@
1
+ // ================================================================
2
+ // Inbound Media — 适配器 + 抽象层类型定义
3
+ // ================================================================
4
+ // 设计思路(参考 OpenClaw):
5
+ // 1. IM 平台只管从自己平台下载原始 buffer(InboundMediaAdapter)
6
+ // 2. MediaStore 负责存储、MIME sniff、生命周期管理
7
+ // 3. InboundMediaResolver 负责串联下载→存储→生成 Agent 提示
8
+ // ================================================================
9
+
10
+ /** 媒体资源类型(对应飞书 API 的 type 参数) */
11
+ export type MediaResourceType = 'image' | 'file' | 'media';
12
+
13
+ /** 下载后的原始媒体元数据 */
14
+ export interface DownloadedMedia {
15
+ /** 文件内容 buffer */
16
+ buffer: Buffer;
17
+ /** 平台提供的原始文件名(如有) */
18
+ fileName?: string;
19
+ /** 平台提供的 MIME 类型(如有,可能不准确) */
20
+ contentType?: string;
21
+ /** 平台资源标识(image_key / file_key 等,调试用) */
22
+ sourceKey?: string;
23
+ }
24
+
25
+ /** 统一媒体条目 — MediaStore 返回的标准格式 */
26
+ export interface MediaEntry {
27
+ /** 本地存储路径 */
28
+ localPath: string;
29
+ /** 嗅探/推断出的 MIME 类型 */
30
+ mimeType: string;
31
+ /** 原始文件名(保留扩展名信息) */
32
+ fileName: string;
33
+ /** 媒体分类:image | document | audio | video | archive | spreadsheet | presentation | text | other */
34
+ category: MediaCategory;
35
+ /** 文件大小(bytes) */
36
+ sizeBytes: number;
37
+ /** 平台来源标识 */
38
+ source?: string;
39
+ }
40
+
41
+ /** 媒体分类(用于差异化 Agent 提示) */
42
+ export type MediaCategory =
43
+ | 'image'
44
+ | 'document' // PDF, DOC, DOCX, TXT, MD 等
45
+ | 'audio'
46
+ | 'video'
47
+ | 'archive' // ZIP, TAR, RAR, 7Z 等
48
+ | 'spreadsheet' // XLS, XLSX, CSV 等
49
+ | 'presentation' // PPT, PPTX 等
50
+ | 'text' // 纯文本文件
51
+ | 'other';
52
+
53
+ /**
54
+ * InboundMediaAdapter — IM 平台必须实现的媒体下载接口
55
+ *
56
+ * 职责:从各自 IM 平台下载消息附件的原始 buffer
57
+ * 不负责:存储、MIME sniff、扩展名处理
58
+ */
59
+ export interface InboundMediaAdapter {
60
+ /**
61
+ * 从 IM 平台下载消息附件
62
+ * @param messageId 消息 ID
63
+ * @param resourceKey 平台资源 key(image_key / file_key 等)
64
+ * @param type 资源类型
65
+ * @param fileName 原始文件名(如有,用于扩展名推断)
66
+ */
67
+ downloadResource(
68
+ messageId: string,
69
+ resourceKey: string,
70
+ type: MediaResourceType,
71
+ fileName?: string
72
+ ): Promise<DownloadedMedia | null>;
73
+
74
+ /** 平台标识('feishu' / 'telegram' / ...) */
75
+ readonly platform: string;
76
+ }
@@ -0,0 +1,123 @@
1
+ // ================================================================
2
+ // Prompt Builder — 统一的 Agent 系统提示词构建层
3
+ // ================================================================
4
+ // 职责:
5
+ // 1. 加载 Soul(用户自定义指令)
6
+ // 2. 构建 IM 能力说明
7
+ // 3. 组合成完整 system prompt
8
+ //
9
+ // 设计原则:
10
+ // - 单一入口:所有 Agent 路径都调用 buildSystemPrompt()
11
+ // - 与 Agent 类型无关:Claude / Codex / 未来任何 Agent 都用同一套
12
+ // - 与 IM 类型无关:通过 IMCapabilities 接口抽象,飞书/微信/Telegram 都行
13
+ // - 无重复:loadSoul、fallback caps、构建逻辑只有一份
14
+ // ================================================================
15
+
16
+ import type { IMCapabilities } from './types';
17
+ import { buildCapabilityPrompt } from './capabilities';
18
+ import { getSoulDir } from './utils/paths';
19
+
20
+ // ================================================================
21
+ // 默认终端能力(无 IM 模块时的 fallback)
22
+ // 唯一数据源 — 所有模块都用这个
23
+ // ================================================================
24
+ export const DEFAULT_TERMINAL_CAPS: IMCapabilities = {
25
+ text: true,
26
+ codeBlock: true,
27
+ cardMessage: false,
28
+ fileSend: false,
29
+ imageSend: false,
30
+ buttonAction: false,
31
+ maxTextLength: 50000,
32
+ };
33
+
34
+ // ================================================================
35
+ // Soul 加载
36
+ // ================================================================
37
+ // 从 ~/Desktop/imtoagent/soul/{botName}/ 按顺序加载
38
+ // 加载顺序:rules → identity → profile → workspace → skills
39
+ // ================================================================
40
+ export function loadSoul(botName: string): string {
41
+ const soulDir = getSoulDir(botName);
42
+ const soulOrder = ['rules.md', 'identity.md', 'profile.md', 'workspace.md', 'skills.md'];
43
+ const parts: string[] = [];
44
+ try {
45
+ const fs = require('fs');
46
+ if (!fs.existsSync(soulDir)) return '';
47
+ for (const file of soulOrder) {
48
+ const fp = soulDir + '/' + file;
49
+ if (fs.existsSync(fp)) {
50
+ const content = fs.readFileSync(fp, 'utf-8').trim();
51
+ if (content) parts.push(content);
52
+ }
53
+ }
54
+ } catch {}
55
+ return parts.join('\n\n');
56
+ }
57
+
58
+ // ================================================================
59
+ // 上下文接口
60
+ // ================================================================
61
+ export interface PromptBuilderContext {
62
+ /** IM 模块实例,用于动态获取当前能力 */
63
+ imModule?: { getCapabilities(): IMCapabilities } | null;
64
+ /** 当 imModule 不可用时,手动指定的能力 */
65
+ caps?: IMCapabilities | null;
66
+ /** Bot 名称,用于加载 Soul */
67
+ botName: string;
68
+ /** Agent 特有的额外系统提示(如工具使用指南、工作目录约束等) */
69
+ agentInstructions?: string;
70
+ }
71
+
72
+ // ================================================================
73
+ // 主入口:构建完整 system prompt
74
+ // ================================================================
75
+ // 组合顺序(从上到下,优先级递减):
76
+ // 1. Agent 特有指令(最高优先级,Agent 最关心)
77
+ // 2. IM 能力说明(告诉 Agent 输出格式约束)
78
+ // 3. Soul / 用户自定义指令(长期人格和规则)
79
+ // ================================================================
80
+ export function buildSystemPrompt(ctx: PromptBuilderContext): string {
81
+ const sections: string[] = [];
82
+
83
+ // 1. Agent 特有指令
84
+ if (ctx.agentInstructions) {
85
+ sections.push(ctx.agentInstructions.trim());
86
+ }
87
+
88
+ // 2. IM 能力
89
+ const caps = ctx.imModule?.getCapabilities() ?? ctx.caps ?? DEFAULT_TERMINAL_CAPS;
90
+ const capSection = buildCapabilityPrompt(caps);
91
+ sections.push('# 当前对接 IM 能力\n\n' + capSection);
92
+
93
+ // 3. 网关运行日志(Agent 可主动查询)
94
+ sections.push(`# 网关运行日志
95
+
96
+ 网关运行日志: ~/.imtoagent/logs/imtoagent.log
97
+
98
+ 你可以通过查看日志来了解网关状态、排查问题、感知重启事件:
99
+ - \`tail -n 30 ~/.imtoagent/logs/imtoagent.log\` — 最近 30 行
100
+ - \`grep -i "restart\|reload\|shutdown\|SIGTERM" ~/.imtoagent/logs/imtoagent.log | tail -n 10\` — 重启/关闭记录
101
+ - \`grep -i "error\|fail\|crash" ~/.imtoagent/logs/imtoagent.log | tail -n 10\` — 错误记录
102
+ - \`grep -i "online\|connected\|disconnected" ~/.imtoagent/logs/imtoagent.log | tail -n 10\` — Bot 连接状态
103
+
104
+ 注意:你启动后第一条消息的对话记忆可能已丢失(如果网关重启过),请先检查日志了解上下文。`);
105
+
106
+ // 4. Soul
107
+ const soul = loadSoul(ctx.botName);
108
+ if (soul) {
109
+ sections.push('# 用户自定义指令 (IMtoAgent Soul)\n\n' + soul);
110
+ }
111
+
112
+ return sections.join('\n\n---\n\n');
113
+ }
114
+
115
+ // ================================================================
116
+ // 便捷函数:直接获取能力(消除各处 inline fallback)
117
+ // ================================================================
118
+ export function resolveCapabilities(
119
+ imModule?: { getCapabilities(): IMCapabilities } | null,
120
+ fallback?: IMCapabilities | null
121
+ ): IMCapabilities {
122
+ return imModule?.getCapabilities() ?? fallback ?? DEFAULT_TERMINAL_CAPS;
123
+ }