@neomei/opencode-qiwei 0.1.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 (55) hide show
  1. package/README.md +38 -0
  2. package/bin/opencode-qiwei +96 -0
  3. package/dist/core/config.d.ts +62 -0
  4. package/dist/core/config.d.ts.map +1 -0
  5. package/dist/core/config.js +47 -0
  6. package/dist/core/config.js.map +1 -0
  7. package/dist/core/group-policy.d.ts +12 -0
  8. package/dist/core/group-policy.d.ts.map +1 -0
  9. package/dist/core/group-policy.js +26 -0
  10. package/dist/core/group-policy.js.map +1 -0
  11. package/dist/core/logger.d.ts +8 -0
  12. package/dist/core/logger.d.ts.map +1 -0
  13. package/dist/core/logger.js +20 -0
  14. package/dist/core/logger.js.map +1 -0
  15. package/dist/core/media-handler.d.ts +22 -0
  16. package/dist/core/media-handler.d.ts.map +1 -0
  17. package/dist/core/media-handler.js +51 -0
  18. package/dist/core/media-handler.js.map +1 -0
  19. package/dist/core/message-handler.d.ts +38 -0
  20. package/dist/core/message-handler.d.ts.map +1 -0
  21. package/dist/core/message-handler.js +257 -0
  22. package/dist/core/message-handler.js.map +1 -0
  23. package/dist/core/session-manager.d.ts +33 -0
  24. package/dist/core/session-manager.d.ts.map +1 -0
  25. package/dist/core/session-manager.js +75 -0
  26. package/dist/core/session-manager.js.map +1 -0
  27. package/dist/core/template-card-manager.d.ts +32 -0
  28. package/dist/core/template-card-manager.d.ts.map +1 -0
  29. package/dist/core/template-card-manager.js +46 -0
  30. package/dist/core/template-card-manager.js.map +1 -0
  31. package/dist/core/types.d.ts +79 -0
  32. package/dist/core/types.d.ts.map +1 -0
  33. package/dist/core/types.js +2 -0
  34. package/dist/core/types.js.map +1 -0
  35. package/dist/index.d.ts +23 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +22 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/opencode/client.d.ts +27 -0
  40. package/dist/opencode/client.d.ts.map +1 -0
  41. package/dist/opencode/client.js +65 -0
  42. package/dist/opencode/client.js.map +1 -0
  43. package/dist/opencode/event-handler.d.ts +25 -0
  44. package/dist/opencode/event-handler.d.ts.map +1 -0
  45. package/dist/opencode/event-handler.js +167 -0
  46. package/dist/opencode/event-handler.js.map +1 -0
  47. package/dist/plugin.d.ts +14 -0
  48. package/dist/plugin.d.ts.map +1 -0
  49. package/dist/plugin.js +58 -0
  50. package/dist/plugin.js.map +1 -0
  51. package/dist/standalone.d.ts +8 -0
  52. package/dist/standalone.d.ts.map +1 -0
  53. package/dist/standalone.js +84 -0
  54. package/dist/standalone.js.map +1 -0
  55. package/package.json +42 -0
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # opencode-qiwei
2
+
3
+ OpenCode 企业微信桥接 — 让 OpenCode 接入企业微信
4
+
5
+ 基于企业微信智能机器人 WebSocket 长连接通道,提供:
6
+ - 消息收发(文本、图片、语音、文件)
7
+ - 流式回复(Markdown 渲染)
8
+ - 模板卡片交互(按钮、投票、选择)
9
+ - 自动权限批准
10
+
11
+ ## 安装
12
+
13
+ ```bash
14
+ npm install -g @neomei/opencode-qiwei
15
+ ```
16
+
17
+ ## 配置
18
+
19
+ ```json
20
+ // ~/.config/opencode/qiwei.json
21
+ {
22
+ "botId": "your-bot-id",
23
+ "corpSecret": "your-secret",
24
+ "agentId": 1000001,
25
+ "token": "your-token",
26
+ "encodingAESKey": "your-43-char-aes-key",
27
+ "opencodeUrl": "http://localhost:19876",
28
+ "streaming": true,
29
+ "showProcess": "tools",
30
+ "autoApprove": true
31
+ }
32
+ ```
33
+
34
+ ## 使用
35
+
36
+ ```bash
37
+ opencode-qiwei start
38
+ ```
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * opencode-qiwei CLI — 企业微信桥接命令行工具
4
+ */
5
+
6
+ import { startStandalone } from '../dist/standalone.js';
7
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { homedir } from 'os';
10
+ import { createInterface } from 'readline';
11
+
12
+ const QIWEI_CONFIG = join(homedir(), '.config', 'opencode', 'qiwei.json');
13
+
14
+ async function runSetup() {
15
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
16
+ const ask = (q) => new Promise(r => rl.question(q, r));
17
+
18
+ console.log('\n📋 企业微信智能机器人配置向导\n');
19
+ console.log('请在企业微信管理后台获取以下信息:');
20
+ console.log(' 应用管理 → 智能机器人 → 接收消息\n');
21
+
22
+ const botId = await ask('机器人 ID (botId): ');
23
+ const secret = await ask('机器人 Secret: ');
24
+
25
+ rl.close();
26
+
27
+ const config = {
28
+ botId: botId.trim(),
29
+ secret: secret.trim(),
30
+ corpId: 'placeholder',
31
+ corpSecret: 'placeholder',
32
+ agentId: 1000001,
33
+ token: 'placeholder-token',
34
+ encodingAESKey: 'placeholder-aes-key-43-characters-here',
35
+ opencodeUrl: 'http://localhost:19876',
36
+ streaming: true,
37
+ requireMention: true,
38
+ groupPolicy: 'open',
39
+ showProcess: 'tools',
40
+ autoApprove: true,
41
+ };
42
+
43
+ mkdirSync(join(homedir(), '.config', 'opencode'), { recursive: true });
44
+ writeFileSync(QIWEI_CONFIG, JSON.stringify(config, null, 2));
45
+ console.log(`\n✅ 配置已保存到 ${QIWEI_CONFIG}`);
46
+ console.log('现在可以运行 opencode-qiwei start 启动桥接\n');
47
+ }
48
+
49
+ async function runDoctor() {
50
+ console.log('\n🔍 opencode-qiwei 连接预检\n');
51
+
52
+ if (!existsSync(QIWEI_CONFIG)) {
53
+ console.log('❌ 配置文件不存在');
54
+ console.log(' 运行 opencode-qiwei setup 创建配置\n');
55
+ process.exit(1);
56
+ }
57
+
58
+ const config = JSON.parse(readFileSync(QIWEI_CONFIG, 'utf-8'));
59
+
60
+ console.log(`✅ 配置文件: ${QIWEI_CONFIG}`);
61
+ console.log(` botId: ${config.botId}`);
62
+ console.log(` OpenCode: ${config.opencodeUrl}`);
63
+
64
+ try {
65
+ const res = await fetch(config.opencodeUrl);
66
+ console.log(`✅ OpenCode 可达 (${res.status})`);
67
+ } catch {
68
+ console.log('❌ OpenCode 不可达');
69
+ }
70
+
71
+ try {
72
+ await import('@wecom/aibot-node-sdk');
73
+ console.log('✅ 企业微信 SDK 已加载');
74
+ } catch (e) {
75
+ console.log(`❌ SDK 加载失败: ${e.message}`);
76
+ }
77
+
78
+ console.log('');
79
+ }
80
+
81
+ async function main() {
82
+ const cmd = process.argv[2] || 'start';
83
+
84
+ try {
85
+ if (cmd === 'setup') return await runSetup();
86
+ if (cmd === 'doctor') return await runDoctor();
87
+ if (cmd === 'status') { console.log('opencode-qiwei v0.1.0'); return; }
88
+ if (cmd === 'start') return await startStandalone();
89
+ console.log('用法: opencode-qiwei <start|setup|doctor|status>');
90
+ } catch (err) {
91
+ console.error('错误:', err.message);
92
+ process.exit(1);
93
+ }
94
+ }
95
+
96
+ main();
@@ -0,0 +1,62 @@
1
+ import { z } from 'zod';
2
+ import type { WecomConfig } from './types.js';
3
+ declare const WecomConfigSchema: z.ZodObject<{
4
+ botId: z.ZodString;
5
+ secret: z.ZodString;
6
+ corpId: z.ZodDefault<z.ZodOptional<z.ZodString>>;
7
+ corpSecret: z.ZodDefault<z.ZodOptional<z.ZodString>>;
8
+ agentId: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
9
+ token: z.ZodDefault<z.ZodOptional<z.ZodString>>;
10
+ encodingAESKey: z.ZodDefault<z.ZodOptional<z.ZodString>>;
11
+ opencodeUrl: z.ZodDefault<z.ZodString>;
12
+ streaming: z.ZodDefault<z.ZodBoolean>;
13
+ requireMention: z.ZodDefault<z.ZodBoolean>;
14
+ groupPolicy: z.ZodDefault<z.ZodEnum<["open", "allowlist", "disabled"]>>;
15
+ allowlist: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
16
+ showProcess: z.ZodDefault<z.ZodEnum<["none", "tools", "thinking", "full"]>>;
17
+ autoApprove: z.ZodDefault<z.ZodBoolean>;
18
+ scene: z.ZodOptional<z.ZodNumber>;
19
+ plugVersion: z.ZodOptional<z.ZodString>;
20
+ }, "strip", z.ZodTypeAny, {
21
+ botId: string;
22
+ secret: string;
23
+ corpId: string;
24
+ corpSecret: string;
25
+ agentId: number;
26
+ token: string;
27
+ encodingAESKey: string;
28
+ opencodeUrl: string;
29
+ streaming: boolean;
30
+ requireMention: boolean;
31
+ groupPolicy: "open" | "allowlist" | "disabled";
32
+ showProcess: "none" | "tools" | "thinking" | "full";
33
+ autoApprove: boolean;
34
+ allowlist?: string[] | undefined;
35
+ scene?: number | undefined;
36
+ plugVersion?: string | undefined;
37
+ }, {
38
+ botId: string;
39
+ secret: string;
40
+ allowlist?: string[] | undefined;
41
+ corpId?: string | undefined;
42
+ corpSecret?: string | undefined;
43
+ agentId?: number | undefined;
44
+ token?: string | undefined;
45
+ encodingAESKey?: string | undefined;
46
+ opencodeUrl?: string | undefined;
47
+ streaming?: boolean | undefined;
48
+ requireMention?: boolean | undefined;
49
+ groupPolicy?: "open" | "allowlist" | "disabled" | undefined;
50
+ showProcess?: "none" | "tools" | "thinking" | "full" | undefined;
51
+ autoApprove?: boolean | undefined;
52
+ scene?: number | undefined;
53
+ plugVersion?: string | undefined;
54
+ }>;
55
+ export declare class ConfigManager {
56
+ private configPath;
57
+ constructor(configPath?: string);
58
+ load(): WecomConfig;
59
+ save(config: WecomConfig): void;
60
+ }
61
+ export { WecomConfigSchema };
62
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,QAAA,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiBrB,CAAC;AAIH,qBAAa,aAAa;IACxB,OAAO,CAAC,UAAU,CAAS;gBAEf,UAAU,CAAC,EAAE,MAAM;IAI/B,IAAI,IAAI,WAAW;IAUnB,IAAI,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;CAKhC;AAED,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
@@ -0,0 +1,47 @@
1
+ import { z } from 'zod';
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
3
+ import { join, dirname } from 'path';
4
+ import { homedir } from 'os';
5
+ const WecomConfigSchema = z.object({
6
+ botId: z.string().min(1),
7
+ secret: z.string().min(1),
8
+ corpId: z.string().optional().default(''),
9
+ corpSecret: z.string().optional().default(''),
10
+ agentId: z.number().int().positive().optional().default(1000001),
11
+ token: z.string().optional().default(''),
12
+ encodingAESKey: z.string().optional().default(''),
13
+ opencodeUrl: z.string().url().default('http://localhost:19876'),
14
+ streaming: z.boolean().default(true),
15
+ requireMention: z.boolean().default(true),
16
+ groupPolicy: z.enum(['open', 'allowlist', 'disabled']).default('allowlist'),
17
+ allowlist: z.array(z.string()).optional(),
18
+ showProcess: z.enum(['none', 'tools', 'thinking', 'full']).default('tools'),
19
+ autoApprove: z.boolean().default(false),
20
+ scene: z.number().int().optional(),
21
+ plugVersion: z.string().optional(),
22
+ });
23
+ const DEFAULT_CONFIG_PATH = join(homedir(), '.config', 'opencode', 'qiwei.json');
24
+ export class ConfigManager {
25
+ configPath;
26
+ constructor(configPath) {
27
+ this.configPath = configPath || DEFAULT_CONFIG_PATH;
28
+ }
29
+ load() {
30
+ const dir = dirname(this.configPath);
31
+ if (!existsSync(dir))
32
+ mkdirSync(dir, { recursive: true });
33
+ if (!existsSync(this.configPath)) {
34
+ throw new Error(`配置文件不存在: ${this.configPath}\n请先配置企业微信凭证`);
35
+ }
36
+ const raw = JSON.parse(readFileSync(this.configPath, 'utf-8'));
37
+ return WecomConfigSchema.parse(raw);
38
+ }
39
+ save(config) {
40
+ const dir = dirname(this.configPath);
41
+ if (!existsSync(dir))
42
+ mkdirSync(dir, { recursive: true });
43
+ writeFileSync(this.configPath, JSON.stringify(config, null, 2));
44
+ }
45
+ }
46
+ export { WecomConfigSchema };
47
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * 群聊策略处理
3
+ */
4
+ export declare class GroupPolicy {
5
+ /**
6
+ * 检查是否需要 @ 机器人才回复
7
+ * 企微文本消息中,@ 机器人会以 @机器人名 的形式出现在内容开头
8
+ * 群聊消息中如果不 @ 机器人,则不回复
9
+ */
10
+ static shouldReply(text: string, chatType: 'single' | 'group', requireMention: boolean, botName?: string): boolean;
11
+ }
12
+ //# sourceMappingURL=group-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"group-policy.d.ts","sourceRoot":"","sources":["../../src/core/group-policy.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,qBAAa,WAAW;IACtB;;;;OAIG;IACH,MAAM,CAAC,WAAW,CAChB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,QAAQ,GAAG,OAAO,EAC5B,cAAc,EAAE,OAAO,EACvB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO;CAeX"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * 群聊策略处理
3
+ */
4
+ export class GroupPolicy {
5
+ /**
6
+ * 检查是否需要 @ 机器人才回复
7
+ * 企微文本消息中,@ 机器人会以 @机器人名 的形式出现在内容开头
8
+ * 群聊消息中如果不 @ 机器人,则不回复
9
+ */
10
+ static shouldReply(text, chatType, requireMention, botName) {
11
+ // 单聊永远回复
12
+ if (chatType === 'single')
13
+ return true;
14
+ // 群聊:不需要 @ 则直接回复
15
+ if (!requireMention)
16
+ return true;
17
+ // 群聊需要 @ — 检查是否 @ 了机器人
18
+ if (botName && text.includes(`@${botName}`))
19
+ return true;
20
+ // 通用 @ 检测
21
+ if (text.startsWith('@') || text.includes('@'))
22
+ return true;
23
+ return false;
24
+ }
25
+ }
26
+ //# sourceMappingURL=group-policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"group-policy.js","sourceRoot":"","sources":["../../src/core/group-policy.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,OAAO,WAAW;IACtB;;;;OAIG;IACH,MAAM,CAAC,WAAW,CAChB,IAAY,EACZ,QAA4B,EAC5B,cAAuB,EACvB,OAAgB;QAEhB,SAAS;QACT,IAAI,QAAQ,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QAEvC,iBAAiB;QACjB,IAAI,CAAC,cAAc;YAAE,OAAO,IAAI,CAAC;QAEjC,uBAAuB;QACvB,IAAI,OAAO,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,OAAO,EAAE,CAAC;YAAE,OAAO,IAAI,CAAC;QAEzD,UAAU;QACV,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;QAE5D,OAAO,KAAK,CAAC;IACf,CAAC;CACF"}
@@ -0,0 +1,8 @@
1
+ /** 轻量日志工具 */
2
+ export declare function createLogger(module: string): {
3
+ info(msg: string, data?: any): void;
4
+ warn(msg: string, data?: any): void;
5
+ error(msg: string, data?: any): void;
6
+ debug(msg: string, data?: any): void;
7
+ };
8
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/core/logger.ts"],"names":[],"mappings":"AAAA,aAAa;AACb,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM;cAG7B,MAAM,SAAS,GAAG;cAGlB,MAAM,SAAS,GAAG;eAGjB,MAAM,SAAS,GAAG;eAGlB,MAAM,SAAS,GAAG;EAIhC"}
@@ -0,0 +1,20 @@
1
+ /** 轻量日志工具 */
2
+ export function createLogger(module) {
3
+ const prefix = `[${module}]`;
4
+ return {
5
+ info(msg, data) {
6
+ console.log(`${prefix} ${msg}`, data ? JSON.stringify(data).slice(0, 200) : '');
7
+ },
8
+ warn(msg, data) {
9
+ console.warn(`${prefix} ⚠️ ${msg}`, data ? JSON.stringify(data).slice(0, 200) : '');
10
+ },
11
+ error(msg, data) {
12
+ console.error(`${prefix} ❌ ${msg}`, data ? JSON.stringify(data).slice(0, 200) : '');
13
+ },
14
+ debug(msg, data) {
15
+ if (process.env.DEBUG)
16
+ console.log(`${prefix} 🔍 ${msg}`, data ? JSON.stringify(data).slice(0, 200) : '');
17
+ },
18
+ };
19
+ }
20
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/core/logger.ts"],"names":[],"mappings":"AAAA,aAAa;AACb,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,MAAM,MAAM,GAAG,IAAI,MAAM,GAAG,CAAC;IAC7B,OAAO;QACL,IAAI,CAAC,GAAW,EAAE,IAAU;YAC1B,OAAO,CAAC,GAAG,CAAC,GAAG,MAAM,IAAI,GAAG,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAClF,CAAC;QACD,IAAI,CAAC,GAAW,EAAE,IAAU;YAC1B,OAAO,CAAC,IAAI,CAAC,GAAG,MAAM,QAAQ,GAAG,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACvF,CAAC;QACD,KAAK,CAAC,GAAW,EAAE,IAAU;YAC3B,OAAO,CAAC,KAAK,CAAC,GAAG,MAAM,MAAM,GAAG,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACtF,CAAC;QACD,KAAK,CAAC,GAAW,EAAE,IAAU;YAC3B,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK;gBAAE,OAAO,CAAC,GAAG,CAAC,GAAG,MAAM,OAAO,GAAG,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC5G,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * 企微媒体文件处理
3
+ * 下载并解密企微发送的图片/文件/语音/视频
4
+ */
5
+ import type { WSClient } from '@wecom/aibot-node-sdk';
6
+ export interface DownloadedMedia {
7
+ buffer: Buffer;
8
+ filename: string;
9
+ mimeType: string;
10
+ }
11
+ export declare class MediaHandler {
12
+ /**
13
+ * 从企微消息中下载并解密文件
14
+ * @param url - 加密的文件 URL(5分钟有效)
15
+ * @param aesKey - Base64 编码的 AES 解密密钥
16
+ */
17
+ static download(wsClient: WSClient, url: string, aesKey?: string): Promise<DownloadedMedia | null>;
18
+ /** 上传文件获取 media_id(用于主动发送) */
19
+ static upload(wsClient: WSClient, fileBuffer: Buffer, type: 'image' | 'file' | 'voice' | 'video', filename: string): Promise<string | null>;
20
+ private static guessMimeType;
21
+ }
22
+ //# sourceMappingURL=media-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-handler.d.ts","sourceRoot":"","sources":["../../src/core/media-handler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAKtD,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,YAAY;IACvB;;;;OAIG;WACU,QAAQ,CACnB,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAclC,8BAA8B;WACjB,MAAM,CACjB,QAAQ,EAAE,QAAQ,EAClB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,EAC1C,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAUzB,OAAO,CAAC,MAAM,CAAC,aAAa;CAY7B"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 企微媒体文件处理
3
+ * 下载并解密企微发送的图片/文件/语音/视频
4
+ */
5
+ import { createLogger } from './logger.js';
6
+ const log = createLogger('MediaHandler');
7
+ export class MediaHandler {
8
+ /**
9
+ * 从企微消息中下载并解密文件
10
+ * @param url - 加密的文件 URL(5分钟有效)
11
+ * @param aesKey - Base64 编码的 AES 解密密钥
12
+ */
13
+ static async download(wsClient, url, aesKey) {
14
+ try {
15
+ const result = await wsClient.downloadFile(url, aesKey);
16
+ return {
17
+ buffer: result.buffer,
18
+ filename: result.filename || 'download',
19
+ mimeType: MediaHandler.guessMimeType(result.filename || ''),
20
+ };
21
+ }
22
+ catch (err) {
23
+ log.error('文件下载失败', { err });
24
+ return null;
25
+ }
26
+ }
27
+ /** 上传文件获取 media_id(用于主动发送) */
28
+ static async upload(wsClient, fileBuffer, type, filename) {
29
+ try {
30
+ const result = await wsClient.uploadMedia(fileBuffer, { type, filename });
31
+ return result.media_id;
32
+ }
33
+ catch (err) {
34
+ log.error('文件上传失败', { err });
35
+ return null;
36
+ }
37
+ }
38
+ static guessMimeType(filename) {
39
+ const ext = filename.split('.').pop()?.toLowerCase() || '';
40
+ const map = {
41
+ jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png',
42
+ gif: 'image/gif', webp: 'image/webp', bmp: 'image/bmp',
43
+ mp4: 'video/mp4', mov: 'video/quicktime',
44
+ mp3: 'audio/mpeg', wav: 'audio/wav', amr: 'audio/amr',
45
+ pdf: 'application/pdf', doc: 'application/msword',
46
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
47
+ };
48
+ return map[ext] || 'application/octet-stream';
49
+ }
50
+ }
51
+ //# sourceMappingURL=media-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-handler.js","sourceRoot":"","sources":["../../src/core/media-handler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,GAAG,GAAG,YAAY,CAAC,cAAc,CAAC,CAAC;AAQzC,MAAM,OAAO,YAAY;IACvB;;;;OAIG;IACH,MAAM,CAAC,KAAK,CAAC,QAAQ,CACnB,QAAkB,EAClB,GAAW,EACX,MAAe;QAEf,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;YACxD,OAAO;gBACL,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,QAAQ,EAAE,MAAM,CAAC,QAAQ,IAAI,UAAU;gBACvC,QAAQ,EAAE,YAAY,CAAC,aAAa,CAAC,MAAM,CAAC,QAAQ,IAAI,EAAE,CAAC;aAC5D,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;YAC7B,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,8BAA8B;IAC9B,MAAM,CAAC,KAAK,CAAC,MAAM,CACjB,QAAkB,EAClB,UAAkB,EAClB,IAA0C,EAC1C,QAAgB;QAEhB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,WAAW,CAAC,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;YAC1E,OAAO,MAAM,CAAC,QAAQ,CAAC;QACzB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;YAC7B,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAEO,MAAM,CAAC,aAAa,CAAC,QAAgB;QAC3C,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;QAC3D,MAAM,GAAG,GAA2B;YAClC,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,YAAY,EAAE,GAAG,EAAE,WAAW;YACvD,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,YAAY,EAAE,GAAG,EAAE,WAAW;YACtD,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,iBAAiB;YACxC,GAAG,EAAE,YAAY,EAAE,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,WAAW;YACrD,GAAG,EAAE,iBAAiB,EAAE,GAAG,EAAE,oBAAoB;YACjD,IAAI,EAAE,yEAAyE;SAChF,CAAC;QACF,OAAO,GAAG,CAAC,GAAG,CAAC,IAAI,0BAA0B,CAAC;IAChD,CAAC;CACF"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * 消息处理器 — 企微消息 ↔ OpenCode 流式回复
3
+ */
4
+ import type { WsFrame, TextMessage, ImageMessage, MixedMessage, WSClient } from '@wecom/aibot-node-sdk';
5
+ import type { WecomConfig } from './types.js';
6
+ import { SessionManager } from './session-manager.js';
7
+ import { OpenCodeClient } from '../opencode/client.js';
8
+ export declare class MessageHandler {
9
+ private config;
10
+ private sessionManager;
11
+ private wsClient;
12
+ private opencode;
13
+ /** 挂起的流式回调:sessionId → { frame, streamId, lastContent } */
14
+ private streams;
15
+ constructor(config: WecomConfig, sessionManager: SessionManager, wsClient: WSClient, opencode: OpenCodeClient);
16
+ /** 处理文本消息 */
17
+ handleMessage(frame: WsFrame<TextMessage>): Promise<void>;
18
+ /**
19
+ * 尝试将用户消息解析为挂起交互的回复。
20
+ * 返回 true 表示已处理。
21
+ */
22
+ private handleInteractionReply;
23
+ /** 通知用户有挂起的交互(由 EventHandler 调用) */
24
+ notifyPending(chatId: string, message: string): Promise<void>;
25
+ /** 流式推送文本增量到企微 */
26
+ pushStreamDelta(sessionId: string, delta: string): Promise<void>;
27
+ /** 流式结束 */
28
+ finishStream(sessionId: string): Promise<void>;
29
+ /** 处理图片消息 */
30
+ handleImageMessage(frame: WsFrame<ImageMessage>): Promise<void>;
31
+ /** 处理图文混排消息 */
32
+ handleMixedMessage(frame: WsFrame<MixedMessage>): Promise<void>;
33
+ /** 进入会话事件 */
34
+ handleEnterChat(frame: WsFrame<any>): Promise<void>;
35
+ /** 模板卡片事件 */
36
+ handleCardEvent(frame: WsFrame<any>): Promise<void>;
37
+ }
38
+ //# sourceMappingURL=message-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-handler.d.ts","sourceRoot":"","sources":["../../src/core/message-handler.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACxG,OAAO,KAAK,EAAE,WAAW,EAAsB,MAAM,YAAY,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAKvD,qBAAa,cAAc;IAKvB,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,cAAc;IACtB,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,QAAQ;IAPlB,2DAA2D;IAC3D,OAAO,CAAC,OAAO,CAAqF;gBAG1F,MAAM,EAAE,WAAW,EACnB,cAAc,EAAE,cAAc,EAC9B,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,cAAc;IAGlC,aAAa;IACP,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,WAAW,CAAC;IAgE/C;;;OAGG;YACW,sBAAsB;IAoGpC,oCAAoC;IAC9B,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBnE,kBAAkB;IACZ,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAkBtD,WAAW;IACL,YAAY,CAAC,SAAS,EAAE,MAAM;IAiBpC,aAAa;IACP,kBAAkB,CAAC,KAAK,EAAE,OAAO,CAAC,YAAY,CAAC;IASrD,eAAe;IACT,kBAAkB,CAAC,KAAK,EAAE,OAAO,CAAC,YAAY,CAAC;IAOrD,aAAa;IACP,eAAe,CAAC,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC;IAWzC,aAAa;IACP,eAAe,CAAC,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC;CAI1C"}