@dobby.ai/dobby 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.
- package/.env.example +9 -0
- package/AGENTS.md +267 -0
- package/README.md +382 -0
- package/ROADMAP.md +34 -0
- package/config/cron.example.json +9 -0
- package/config/gateway.example.json +128 -0
- package/config/models.custom.example.json +27 -0
- package/dist/src/agent/event-forwarder.js +341 -0
- package/dist/src/agent/tests/event-forwarder.test.js +113 -0
- package/dist/src/cli/commands/config.js +243 -0
- package/dist/src/cli/commands/configure.js +61 -0
- package/dist/src/cli/commands/cron.js +288 -0
- package/dist/src/cli/commands/doctor.js +189 -0
- package/dist/src/cli/commands/extension.js +151 -0
- package/dist/src/cli/commands/init.js +286 -0
- package/dist/src/cli/commands/start.js +177 -0
- package/dist/src/cli/commands/topology.js +254 -0
- package/dist/src/cli/index.js +8 -0
- package/dist/src/cli/program.js +386 -0
- package/dist/src/cli/shared/config-io.js +223 -0
- package/dist/src/cli/shared/config-mutators.js +345 -0
- package/dist/src/cli/shared/config-path.js +207 -0
- package/dist/src/cli/shared/config-schema.js +159 -0
- package/dist/src/cli/shared/config-types.js +1 -0
- package/dist/src/cli/shared/configure-sections.js +429 -0
- package/dist/src/cli/shared/discord-config.js +12 -0
- package/dist/src/cli/shared/init-catalog.js +115 -0
- package/dist/src/cli/shared/init-models-file.js +65 -0
- package/dist/src/cli/shared/presets.js +86 -0
- package/dist/src/cli/shared/runtime.js +29 -0
- package/dist/src/cli/shared/schema-prompts.js +325 -0
- package/dist/src/cli/tests/config-command.test.js +42 -0
- package/dist/src/cli/tests/config-io.test.js +64 -0
- package/dist/src/cli/tests/config-mutators.test.js +47 -0
- package/dist/src/cli/tests/config-path.test.js +21 -0
- package/dist/src/cli/tests/discord-config.test.js +23 -0
- package/dist/src/cli/tests/doctor.test.js +107 -0
- package/dist/src/cli/tests/init-catalog.test.js +87 -0
- package/dist/src/cli/tests/presets.test.js +41 -0
- package/dist/src/cli/tests/program-options.test.js +92 -0
- package/dist/src/cli/tests/routing-config.test.js +199 -0
- package/dist/src/cli/tests/routing-legacy.test.js +191 -0
- package/dist/src/core/control-command.js +12 -0
- package/dist/src/core/dedup-store.js +92 -0
- package/dist/src/core/gateway.js +432 -0
- package/dist/src/core/routing.js +306 -0
- package/dist/src/core/runtime-registry.js +119 -0
- package/dist/src/core/tests/control-command.test.js +17 -0
- package/dist/src/core/tests/gateway-update-strategy.test.js +167 -0
- package/dist/src/core/tests/runtime-registry.test.js +116 -0
- package/dist/src/core/tests/typing-controller.test.js +103 -0
- package/dist/src/core/types.js +1 -0
- package/dist/src/core/typing-controller.js +88 -0
- package/dist/src/cron/config.js +114 -0
- package/dist/src/cron/schedule.js +49 -0
- package/dist/src/cron/service.js +196 -0
- package/dist/src/cron/store.js +142 -0
- package/dist/src/cron/types.js +1 -0
- package/dist/src/extension/loader.js +97 -0
- package/dist/src/extension/manager.js +269 -0
- package/dist/src/extension/manifest.js +21 -0
- package/dist/src/extension/registry.js +137 -0
- package/dist/src/main.js +6 -0
- package/dist/src/sandbox/executor.js +1 -0
- package/dist/src/sandbox/host-executor.js +111 -0
- package/docs/BOXLITE_SANDBOX_FEASIBILITY.md +175 -0
- package/docs/CRON_SCHEDULER_DESIGN.md +374 -0
- package/docs/DOCKER_SANDBOX_vs_BOXLITE.md +77 -0
- package/docs/EXTENSION_SYSTEM_ARCHITECTURE.md +119 -0
- package/docs/MVP.md +135 -0
- package/docs/RUNBOOK.md +242 -0
- package/docs/TEAMWORK_HANDOFF_DESIGN.md +440 -0
- package/package.json +43 -0
- package/plugins/connector-discord/dobby.manifest.json +18 -0
- package/plugins/connector-discord/index.js +1 -0
- package/plugins/connector-discord/package-lock.json +360 -0
- package/plugins/connector-discord/package.json +38 -0
- package/plugins/connector-discord/src/connector.ts +350 -0
- package/plugins/connector-discord/src/contribution.ts +21 -0
- package/plugins/connector-discord/src/mapper.ts +102 -0
- package/plugins/connector-discord/tsconfig.json +19 -0
- package/plugins/connector-feishu/dobby.manifest.json +18 -0
- package/plugins/connector-feishu/index.js +1 -0
- package/plugins/connector-feishu/package-lock.json +618 -0
- package/plugins/connector-feishu/package.json +38 -0
- package/plugins/connector-feishu/src/connector.ts +343 -0
- package/plugins/connector-feishu/src/contribution.ts +26 -0
- package/plugins/connector-feishu/src/mapper.ts +401 -0
- package/plugins/connector-feishu/tsconfig.json +19 -0
- package/plugins/plugin-sdk/index.d.ts +261 -0
- package/plugins/plugin-sdk/index.js +1 -0
- package/plugins/plugin-sdk/package-lock.json +12 -0
- package/plugins/plugin-sdk/package.json +22 -0
- package/plugins/provider-claude/dobby.manifest.json +17 -0
- package/plugins/provider-claude/index.js +1 -0
- package/plugins/provider-claude/package-lock.json +3398 -0
- package/plugins/provider-claude/package.json +39 -0
- package/plugins/provider-claude/src/contribution.ts +1018 -0
- package/plugins/provider-claude/tsconfig.json +19 -0
- package/plugins/provider-claude-cli/dobby.manifest.json +17 -0
- package/plugins/provider-claude-cli/index.js +1 -0
- package/plugins/provider-claude-cli/package-lock.json +2898 -0
- package/plugins/provider-claude-cli/package.json +38 -0
- package/plugins/provider-claude-cli/src/contribution.ts +1673 -0
- package/plugins/provider-claude-cli/tsconfig.json +19 -0
- package/plugins/provider-pi/dobby.manifest.json +17 -0
- package/plugins/provider-pi/index.js +1 -0
- package/plugins/provider-pi/package-lock.json +3877 -0
- package/plugins/provider-pi/package.json +40 -0
- package/plugins/provider-pi/src/contribution.ts +476 -0
- package/plugins/provider-pi/tsconfig.json +19 -0
- package/plugins/sandbox-core/boxlite.js +1 -0
- package/plugins/sandbox-core/dobby.manifest.json +17 -0
- package/plugins/sandbox-core/docker.js +1 -0
- package/plugins/sandbox-core/package-lock.json +136 -0
- package/plugins/sandbox-core/package.json +39 -0
- package/plugins/sandbox-core/src/boxlite-context.ts +2 -0
- package/plugins/sandbox-core/src/boxlite-contribution.ts +53 -0
- package/plugins/sandbox-core/src/boxlite-executor.ts +911 -0
- package/plugins/sandbox-core/src/docker-contribution.ts +43 -0
- package/plugins/sandbox-core/src/docker-executor.ts +217 -0
- package/plugins/sandbox-core/tsconfig.json +19 -0
- package/scripts/local-extensions.mjs +168 -0
- package/src/agent/event-forwarder.ts +414 -0
- package/src/cli/commands/config.ts +328 -0
- package/src/cli/commands/configure.ts +92 -0
- package/src/cli/commands/cron.ts +410 -0
- package/src/cli/commands/doctor.ts +230 -0
- package/src/cli/commands/extension.ts +205 -0
- package/src/cli/commands/init.ts +396 -0
- package/src/cli/commands/start.ts +223 -0
- package/src/cli/commands/topology.ts +383 -0
- package/src/cli/index.ts +9 -0
- package/src/cli/program.ts +465 -0
- package/src/cli/shared/config-io.ts +277 -0
- package/src/cli/shared/config-mutators.ts +440 -0
- package/src/cli/shared/config-schema.ts +228 -0
- package/src/cli/shared/config-types.ts +121 -0
- package/src/cli/shared/configure-sections.ts +551 -0
- package/src/cli/shared/discord-config.ts +14 -0
- package/src/cli/shared/init-catalog.ts +189 -0
- package/src/cli/shared/init-models-file.ts +77 -0
- package/src/cli/shared/runtime.ts +33 -0
- package/src/cli/shared/schema-prompts.ts +414 -0
- package/src/cli/tests/config-command.test.ts +56 -0
- package/src/cli/tests/config-io.test.ts +92 -0
- package/src/cli/tests/config-mutators.test.ts +59 -0
- package/src/cli/tests/doctor.test.ts +120 -0
- package/src/cli/tests/init-catalog.test.ts +96 -0
- package/src/cli/tests/program-options.test.ts +113 -0
- package/src/cli/tests/routing-config.test.ts +209 -0
- package/src/core/control-command.ts +12 -0
- package/src/core/dedup-store.ts +103 -0
- package/src/core/gateway.ts +607 -0
- package/src/core/routing.ts +379 -0
- package/src/core/runtime-registry.ts +141 -0
- package/src/core/tests/control-command.test.ts +20 -0
- package/src/core/tests/runtime-registry.test.ts +140 -0
- package/src/core/tests/typing-controller.test.ts +129 -0
- package/src/core/types.ts +318 -0
- package/src/core/typing-controller.ts +119 -0
- package/src/cron/config.ts +154 -0
- package/src/cron/schedule.ts +61 -0
- package/src/cron/service.ts +249 -0
- package/src/cron/store.ts +155 -0
- package/src/cron/types.ts +60 -0
- package/src/extension/loader.ts +145 -0
- package/src/extension/manager.ts +355 -0
- package/src/extension/manifest.ts +26 -0
- package/src/extension/registry.ts +229 -0
- package/src/main.ts +8 -0
- package/src/sandbox/executor.ts +44 -0
- package/src/sandbox/host-executor.ts +118 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { mkdir, readFile, rename } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { Client } from "@larksuiteoapi/node-sdk";
|
|
4
|
+
import type { GatewayLogger, InboundAttachment, InboundEnvelope } from "@dobby.ai/plugin-sdk";
|
|
5
|
+
|
|
6
|
+
interface FeishuMention {
|
|
7
|
+
key: string;
|
|
8
|
+
id?: {
|
|
9
|
+
union_id?: string;
|
|
10
|
+
user_id?: string;
|
|
11
|
+
open_id?: string;
|
|
12
|
+
};
|
|
13
|
+
name: string;
|
|
14
|
+
tenant_key?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface FeishuMessageEvent {
|
|
18
|
+
app_id?: string;
|
|
19
|
+
sender: {
|
|
20
|
+
sender_id?: {
|
|
21
|
+
union_id?: string;
|
|
22
|
+
user_id?: string;
|
|
23
|
+
open_id?: string;
|
|
24
|
+
};
|
|
25
|
+
sender_type: string;
|
|
26
|
+
tenant_key?: string;
|
|
27
|
+
};
|
|
28
|
+
message: {
|
|
29
|
+
message_id: string;
|
|
30
|
+
root_id?: string;
|
|
31
|
+
parent_id?: string;
|
|
32
|
+
create_time: string;
|
|
33
|
+
update_time?: string;
|
|
34
|
+
chat_id: string;
|
|
35
|
+
thread_id?: string;
|
|
36
|
+
chat_type: string;
|
|
37
|
+
message_type: string;
|
|
38
|
+
content: string;
|
|
39
|
+
mentions?: FeishuMention[];
|
|
40
|
+
user_agent?: string;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface MapFeishuMessageOptions {
|
|
45
|
+
event: FeishuMessageEvent;
|
|
46
|
+
connectorId: string;
|
|
47
|
+
attachmentsRoot: string;
|
|
48
|
+
client: Client;
|
|
49
|
+
logger: GatewayLogger;
|
|
50
|
+
downloadAttachments: boolean;
|
|
51
|
+
botOpenId?: string;
|
|
52
|
+
botName?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface AttachmentDescriptor {
|
|
56
|
+
fileKey: string;
|
|
57
|
+
fileName: string;
|
|
58
|
+
mimeType?: string;
|
|
59
|
+
resourceType: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function safeParseJson(value: string): unknown {
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(value) as unknown;
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sanitizeFileName(value: string): string {
|
|
71
|
+
return value.replaceAll(/[^a-zA-Z0-9._-]/g, "_");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function headerValue(headers: unknown, name: string): string | undefined {
|
|
75
|
+
if (!headers || typeof headers !== "object") {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const record = headers as Record<string, unknown>;
|
|
80
|
+
const target = record[name] ?? record[name.toLowerCase()] ?? record[name.toUpperCase()];
|
|
81
|
+
if (typeof target === "string" && target.trim().length > 0) {
|
|
82
|
+
return target.trim();
|
|
83
|
+
}
|
|
84
|
+
if (Array.isArray(target)) {
|
|
85
|
+
const first = target.find((value) => typeof value === "string" && value.trim().length > 0);
|
|
86
|
+
return typeof first === "string" ? first.trim() : undefined;
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizeMimeType(value?: string): string | undefined {
|
|
92
|
+
if (!value) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const [mimeType] = value.split(";", 1);
|
|
97
|
+
return mimeType?.trim().toLowerCase() || undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function sniffImageMimeType(buffer: Buffer): string | undefined {
|
|
101
|
+
if (buffer.length >= 8 && buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) {
|
|
102
|
+
return "image/png";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
|
|
106
|
+
return "image/jpeg";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (buffer.length >= 6) {
|
|
110
|
+
const header = buffer.subarray(0, 6).toString("ascii");
|
|
111
|
+
if (header === "GIF87a" || header === "GIF89a") {
|
|
112
|
+
return "image/gif";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (
|
|
117
|
+
buffer.length >= 12
|
|
118
|
+
&& buffer.subarray(0, 4).toString("ascii") === "RIFF"
|
|
119
|
+
&& buffer.subarray(8, 12).toString("ascii") === "WEBP"
|
|
120
|
+
) {
|
|
121
|
+
return "image/webp";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (buffer.length >= 2 && buffer[0] === 0x42 && buffer[1] === 0x4d) {
|
|
125
|
+
return "image/bmp";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function extensionForMimeType(mimeType?: string): string | undefined {
|
|
132
|
+
switch (mimeType) {
|
|
133
|
+
case "image/png":
|
|
134
|
+
return "png";
|
|
135
|
+
case "image/jpeg":
|
|
136
|
+
return "jpg";
|
|
137
|
+
case "image/gif":
|
|
138
|
+
return "gif";
|
|
139
|
+
case "image/webp":
|
|
140
|
+
return "webp";
|
|
141
|
+
case "image/bmp":
|
|
142
|
+
return "bmp";
|
|
143
|
+
default:
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function normalizeWhitespace(value: string): string {
|
|
149
|
+
return value.replaceAll(/\s+/g, " ").trim();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function normalizeComparableName(value: string): string {
|
|
153
|
+
return normalizeWhitespace(value).replace(/^@+/, "").toLowerCase();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function collectPostText(node: unknown, output: string[]): void {
|
|
157
|
+
if (typeof node === "string") {
|
|
158
|
+
const trimmed = node.trim();
|
|
159
|
+
if (trimmed.length > 0) {
|
|
160
|
+
output.push(trimmed);
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (Array.isArray(node)) {
|
|
166
|
+
for (const item of node) {
|
|
167
|
+
collectPostText(item, output);
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!node || typeof node !== "object") {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const record = node as Record<string, unknown>;
|
|
177
|
+
if (record.tag === "text" && typeof record.text === "string") {
|
|
178
|
+
const trimmed = record.text.trim();
|
|
179
|
+
if (trimmed.length > 0) {
|
|
180
|
+
output.push(trimmed);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for (const value of Object.values(record)) {
|
|
185
|
+
collectPostText(value, output);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function isBotMention(mention: FeishuMention, botOpenId?: string, botName?: string): boolean {
|
|
190
|
+
if (botOpenId) {
|
|
191
|
+
return mention.id?.open_id === botOpenId || mention.id?.user_id === botOpenId || mention.id?.union_id === botOpenId;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (botName) {
|
|
195
|
+
return normalizeComparableName(mention.name) === normalizeComparableName(botName);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function replaceMentionKeys(text: string, mentions: FeishuMention[]): string {
|
|
202
|
+
let next = text;
|
|
203
|
+
for (const mention of mentions) {
|
|
204
|
+
const replacement = mention.name.length > 0 ? `@${mention.name}` : "@user";
|
|
205
|
+
next = next.split(mention.key).join(replacement);
|
|
206
|
+
}
|
|
207
|
+
return next;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function stripBotMentions(text: string, mentions: FeishuMention[], botOpenId?: string, botName?: string): string {
|
|
211
|
+
let next = text;
|
|
212
|
+
for (const mention of mentions) {
|
|
213
|
+
if (!isBotMention(mention, botOpenId, botName)) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
next = next.split(mention.key).join("");
|
|
217
|
+
if (mention.name.length > 0) {
|
|
218
|
+
next = next.split(`@${mention.name}`).join("");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return normalizeWhitespace(next);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function extractMessageText(event: FeishuMessageEvent, botOpenId?: string, botName?: string): string {
|
|
225
|
+
const { message } = event;
|
|
226
|
+
const mentions = message.mentions ?? [];
|
|
227
|
+
const parsed = safeParseJson(message.content);
|
|
228
|
+
let text = "";
|
|
229
|
+
|
|
230
|
+
switch (message.message_type) {
|
|
231
|
+
case "text":
|
|
232
|
+
text = typeof (parsed as { text?: unknown } | null)?.text === "string"
|
|
233
|
+
? (parsed as { text: string }).text
|
|
234
|
+
: message.content;
|
|
235
|
+
break;
|
|
236
|
+
case "post": {
|
|
237
|
+
const parts: string[] = [];
|
|
238
|
+
collectPostText(parsed, parts);
|
|
239
|
+
text = parts.join("\n");
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
case "file":
|
|
243
|
+
text = typeof (parsed as { file_name?: unknown } | null)?.file_name === "string"
|
|
244
|
+
? `[file] ${(parsed as { file_name: string }).file_name}`
|
|
245
|
+
: "(file)";
|
|
246
|
+
break;
|
|
247
|
+
case "image":
|
|
248
|
+
text = "(image)";
|
|
249
|
+
break;
|
|
250
|
+
case "audio":
|
|
251
|
+
text = "(audio)";
|
|
252
|
+
break;
|
|
253
|
+
case "media":
|
|
254
|
+
text = typeof (parsed as { file_name?: unknown } | null)?.file_name === "string"
|
|
255
|
+
? `[media] ${(parsed as { file_name: string }).file_name}`
|
|
256
|
+
: "(media)";
|
|
257
|
+
break;
|
|
258
|
+
default:
|
|
259
|
+
text = `(${message.message_type})`;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const withReadableMentions = replaceMentionKeys(text, mentions);
|
|
264
|
+
return stripBotMentions(withReadableMentions, mentions, botOpenId, botName);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function attachmentDescriptor(event: FeishuMessageEvent): AttachmentDescriptor | null {
|
|
268
|
+
const parsed = safeParseJson(event.message.content) as Record<string, unknown> | null;
|
|
269
|
+
if (!parsed) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (event.message.message_type === "image" && typeof parsed.image_key === "string") {
|
|
274
|
+
return {
|
|
275
|
+
fileKey: parsed.image_key,
|
|
276
|
+
fileName: `image-${parsed.image_key}`,
|
|
277
|
+
resourceType: "image",
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (event.message.message_type === "file" && typeof parsed.file_key === "string") {
|
|
282
|
+
return {
|
|
283
|
+
fileKey: parsed.file_key,
|
|
284
|
+
fileName: typeof parsed.file_name === "string" ? parsed.file_name : `file-${parsed.file_key}`,
|
|
285
|
+
resourceType: "file",
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function downloadAttachment(
|
|
293
|
+
client: Client,
|
|
294
|
+
event: FeishuMessageEvent,
|
|
295
|
+
descriptor: AttachmentDescriptor,
|
|
296
|
+
attachmentDir: string,
|
|
297
|
+
): Promise<InboundAttachment> {
|
|
298
|
+
await mkdir(attachmentDir, { recursive: true });
|
|
299
|
+
|
|
300
|
+
let targetPath = join(attachmentDir, sanitizeFileName(descriptor.fileName));
|
|
301
|
+
const resource = await client.im.v1.messageResource.get({
|
|
302
|
+
params: {
|
|
303
|
+
type: descriptor.resourceType,
|
|
304
|
+
},
|
|
305
|
+
path: {
|
|
306
|
+
message_id: event.message.message_id,
|
|
307
|
+
file_key: descriptor.fileKey,
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
await resource.writeFile(targetPath);
|
|
312
|
+
let resolvedMimeType = normalizeMimeType(descriptor.mimeType) ?? normalizeMimeType(headerValue(resource.headers, "content-type"));
|
|
313
|
+
let resolvedFileName = descriptor.fileName;
|
|
314
|
+
|
|
315
|
+
if (descriptor.resourceType === "image") {
|
|
316
|
+
const fileBuffer = await readFile(targetPath);
|
|
317
|
+
resolvedMimeType = sniffImageMimeType(fileBuffer) ?? resolvedMimeType;
|
|
318
|
+
|
|
319
|
+
const extension = extensionForMimeType(resolvedMimeType);
|
|
320
|
+
if (extension) {
|
|
321
|
+
resolvedFileName = `${descriptor.fileName}.${extension}`;
|
|
322
|
+
const finalPath = join(attachmentDir, sanitizeFileName(resolvedFileName));
|
|
323
|
+
if (finalPath !== targetPath) {
|
|
324
|
+
await rename(targetPath, finalPath);
|
|
325
|
+
targetPath = finalPath;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
id: descriptor.fileKey,
|
|
332
|
+
fileName: resolvedFileName,
|
|
333
|
+
...(resolvedMimeType ? { mimeType: resolvedMimeType } : {}),
|
|
334
|
+
localPath: targetPath,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export async function mapFeishuMessageEvent(options: MapFeishuMessageOptions): Promise<InboundEnvelope | null> {
|
|
339
|
+
const { event, connectorId, attachmentsRoot, client, logger, downloadAttachments, botOpenId, botName } = options;
|
|
340
|
+
if (event.sender.sender_type !== "user") {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const sourceId = event.message.chat_id;
|
|
345
|
+
const attachments: InboundAttachment[] = [];
|
|
346
|
+
if (downloadAttachments) {
|
|
347
|
+
const descriptor = attachmentDescriptor(event);
|
|
348
|
+
if (descriptor) {
|
|
349
|
+
try {
|
|
350
|
+
const attachmentDir = join(attachmentsRoot, sourceId, event.message.message_id);
|
|
351
|
+
attachments.push(await downloadAttachment(client, event, descriptor, attachmentDir));
|
|
352
|
+
} catch (error) {
|
|
353
|
+
logger.warn(
|
|
354
|
+
{
|
|
355
|
+
err: error,
|
|
356
|
+
connectorId,
|
|
357
|
+
messageId: event.message.message_id,
|
|
358
|
+
fileKey: descriptor.fileKey,
|
|
359
|
+
resourceType: descriptor.resourceType,
|
|
360
|
+
},
|
|
361
|
+
"Failed to download Feishu attachment; keeping metadata only",
|
|
362
|
+
);
|
|
363
|
+
attachments.push({
|
|
364
|
+
id: descriptor.fileKey,
|
|
365
|
+
fileName: descriptor.fileName,
|
|
366
|
+
...(descriptor.mimeType ? { mimeType: descriptor.mimeType } : {}),
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const senderId = event.sender.sender_id?.open_id
|
|
373
|
+
?? event.sender.sender_id?.user_id
|
|
374
|
+
?? event.sender.sender_id?.union_id
|
|
375
|
+
?? "unknown";
|
|
376
|
+
|
|
377
|
+
const mentionedBot = event.message.chat_type === "p2p"
|
|
378
|
+
|| (event.message.mentions ?? []).some((mention) => isBotMention(mention, botOpenId, botName))
|
|
379
|
+
|| (!botOpenId && !botName && (event.message.mentions?.length ?? 0) > 0);
|
|
380
|
+
|
|
381
|
+
const timestampMs = Number.parseInt(event.message.create_time, 10);
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
connectorId,
|
|
385
|
+
platform: "feishu",
|
|
386
|
+
accountId: event.app_id ?? connectorId,
|
|
387
|
+
source: {
|
|
388
|
+
type: "chat",
|
|
389
|
+
id: sourceId,
|
|
390
|
+
},
|
|
391
|
+
chatId: event.message.chat_id,
|
|
392
|
+
messageId: event.message.message_id,
|
|
393
|
+
userId: senderId,
|
|
394
|
+
text: extractMessageText(event, botOpenId, botName),
|
|
395
|
+
attachments,
|
|
396
|
+
timestampMs: Number.isFinite(timestampMs) ? timestampMs : Date.now(),
|
|
397
|
+
raw: event,
|
|
398
|
+
isDirectMessage: event.message.chat_type === "p2p",
|
|
399
|
+
mentionedBot,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"types": ["node"],
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"noUncheckedIndexedAccess": true,
|
|
14
|
+
"exactOptionalPropertyTypes": true,
|
|
15
|
+
"resolveJsonModule": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*.ts"],
|
|
18
|
+
"exclude": ["dist", "node_modules"]
|
|
19
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import type { ImageContent } from "@mariozechner/pi-ai";
|
|
2
|
+
import type { Logger } from "pino";
|
|
3
|
+
|
|
4
|
+
export type Platform = string;
|
|
5
|
+
export type ToolProfile = "full" | "readonly";
|
|
6
|
+
export type MentionPolicy = "required" | "optional";
|
|
7
|
+
export type BindingSourceType = "channel" | "chat";
|
|
8
|
+
|
|
9
|
+
export interface InboundAttachment {
|
|
10
|
+
id: string;
|
|
11
|
+
fileName?: string;
|
|
12
|
+
mimeType?: string;
|
|
13
|
+
size?: number;
|
|
14
|
+
localPath?: string;
|
|
15
|
+
remoteUrl?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface BindingSource {
|
|
19
|
+
type: BindingSourceType;
|
|
20
|
+
id: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface InboundEnvelope {
|
|
24
|
+
connectorId: string;
|
|
25
|
+
platform: Platform;
|
|
26
|
+
accountId: string;
|
|
27
|
+
guildId?: string;
|
|
28
|
+
source: BindingSource;
|
|
29
|
+
chatId: string;
|
|
30
|
+
threadId?: string;
|
|
31
|
+
messageId: string;
|
|
32
|
+
userId: string;
|
|
33
|
+
userName?: string;
|
|
34
|
+
text: string;
|
|
35
|
+
attachments: InboundAttachment[];
|
|
36
|
+
timestampMs: number;
|
|
37
|
+
raw: unknown;
|
|
38
|
+
isDirectMessage: boolean;
|
|
39
|
+
mentionedBot: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface OutboundAttachment {
|
|
43
|
+
localPath: string;
|
|
44
|
+
title?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface OutboundEnvelope {
|
|
48
|
+
platform: Platform;
|
|
49
|
+
accountId: string;
|
|
50
|
+
chatId: string;
|
|
51
|
+
threadId?: string;
|
|
52
|
+
replyToMessageId?: string;
|
|
53
|
+
mode: "create" | "update";
|
|
54
|
+
targetMessageId?: string;
|
|
55
|
+
text: string;
|
|
56
|
+
attachments?: OutboundAttachment[];
|
|
57
|
+
metadata?: Record<string, string>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ConnectorSendResult {
|
|
61
|
+
messageId?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ConnectorTypingEnvelope {
|
|
65
|
+
platform: Platform;
|
|
66
|
+
accountId: string;
|
|
67
|
+
chatId: string;
|
|
68
|
+
threadId?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ConnectorContext {
|
|
72
|
+
emitInbound: (msg: InboundEnvelope) => Promise<void>;
|
|
73
|
+
emitControl: (event: {
|
|
74
|
+
type: "stop";
|
|
75
|
+
connectorId: string;
|
|
76
|
+
platform: Platform;
|
|
77
|
+
accountId: string;
|
|
78
|
+
chatId: string;
|
|
79
|
+
threadId?: string;
|
|
80
|
+
}) => Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type ConnectorUpdateStrategy = "edit" | "final_only" | "append";
|
|
84
|
+
|
|
85
|
+
export interface ConnectorCapabilities {
|
|
86
|
+
updateStrategy: ConnectorUpdateStrategy;
|
|
87
|
+
supportedSources: BindingSourceType[];
|
|
88
|
+
supportsThread: boolean;
|
|
89
|
+
supportsTyping: boolean;
|
|
90
|
+
supportsFileUpload: boolean;
|
|
91
|
+
maxTextLength?: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface ConnectorPlugin {
|
|
95
|
+
readonly id: string;
|
|
96
|
+
readonly platform: Platform;
|
|
97
|
+
readonly name: string;
|
|
98
|
+
readonly capabilities: ConnectorCapabilities;
|
|
99
|
+
start(ctx: ConnectorContext): Promise<void>;
|
|
100
|
+
send(message: OutboundEnvelope): Promise<ConnectorSendResult>;
|
|
101
|
+
sendTyping?(message: ConnectorTypingEnvelope): Promise<void>;
|
|
102
|
+
stop(): Promise<void>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface RouteProfile {
|
|
106
|
+
projectRoot: string;
|
|
107
|
+
tools: ToolProfile;
|
|
108
|
+
mentions: MentionPolicy;
|
|
109
|
+
provider: string;
|
|
110
|
+
sandbox: string;
|
|
111
|
+
systemPromptFile?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface RouteResolution {
|
|
115
|
+
routeId: string;
|
|
116
|
+
profile: RouteProfile;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type GatewayAgentEvent =
|
|
120
|
+
| { type: "message_delta"; delta: string }
|
|
121
|
+
| { type: "message_complete"; text: string }
|
|
122
|
+
| { type: "tool_start"; toolName: string }
|
|
123
|
+
| { type: "tool_end"; toolName: string; isError: boolean; output: string }
|
|
124
|
+
| { type: "status"; message: string };
|
|
125
|
+
|
|
126
|
+
export interface GatewayAgentRuntime {
|
|
127
|
+
prompt(text: string, options?: { images?: ImageContent[] }): Promise<void>;
|
|
128
|
+
subscribe(listener: (event: GatewayAgentEvent) => void): () => void;
|
|
129
|
+
abort(): Promise<void>;
|
|
130
|
+
dispose(): void;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface ExecOptions {
|
|
134
|
+
timeoutSeconds?: number;
|
|
135
|
+
signal?: AbortSignal;
|
|
136
|
+
env?: NodeJS.ProcessEnv;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface ExecResult {
|
|
140
|
+
stdout: string;
|
|
141
|
+
stderr: string;
|
|
142
|
+
code: number;
|
|
143
|
+
killed: boolean;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface SpawnOptions {
|
|
147
|
+
command: string;
|
|
148
|
+
args: string[];
|
|
149
|
+
cwd?: string;
|
|
150
|
+
env?: NodeJS.ProcessEnv;
|
|
151
|
+
signal?: AbortSignal;
|
|
152
|
+
tty?: boolean;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface SpawnedProcess {
|
|
156
|
+
stdin: NodeJS.WritableStream;
|
|
157
|
+
stdout: NodeJS.ReadableStream;
|
|
158
|
+
stderr: NodeJS.ReadableStream;
|
|
159
|
+
readonly killed: boolean;
|
|
160
|
+
readonly exitCode: number | null;
|
|
161
|
+
kill(signal?: NodeJS.Signals): boolean;
|
|
162
|
+
on(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): void;
|
|
163
|
+
on(event: "error", listener: (error: Error) => void): void;
|
|
164
|
+
once(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): void;
|
|
165
|
+
once(event: "error", listener: (error: Error) => void): void;
|
|
166
|
+
off(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): void;
|
|
167
|
+
off(event: "error", listener: (error: Error) => void): void;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface Executor {
|
|
171
|
+
exec(command: string, cwd: string, options?: ExecOptions): Promise<ExecResult>;
|
|
172
|
+
spawn(options: SpawnOptions): SpawnedProcess;
|
|
173
|
+
close(): Promise<void>;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface DataConfig {
|
|
177
|
+
rootDir: string;
|
|
178
|
+
sessionsDir: string;
|
|
179
|
+
attachmentsDir: string;
|
|
180
|
+
logsDir: string;
|
|
181
|
+
stateDir: string;
|
|
182
|
+
dedupTtlMs: number;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export type GatewayLogger = Logger;
|
|
186
|
+
|
|
187
|
+
export interface ExtensionHostContext {
|
|
188
|
+
logger: GatewayLogger;
|
|
189
|
+
configBaseDir: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface ProviderRuntimeCreateOptions {
|
|
193
|
+
conversationKey: string;
|
|
194
|
+
route: RouteResolution;
|
|
195
|
+
inbound: InboundEnvelope;
|
|
196
|
+
executor: Executor;
|
|
197
|
+
sessionPolicy?: "shared-session" | "ephemeral";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface ProviderSessionArchiveOptions {
|
|
201
|
+
conversationKey: string;
|
|
202
|
+
inbound: InboundEnvelope;
|
|
203
|
+
sessionPolicy?: "shared-session" | "ephemeral";
|
|
204
|
+
archivedAtMs?: number;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface ProviderSessionArchiveResult {
|
|
208
|
+
archived: boolean;
|
|
209
|
+
archivePath?: string;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface ProviderInstance {
|
|
213
|
+
id: string;
|
|
214
|
+
createRuntime(options: ProviderRuntimeCreateOptions): Promise<GatewayAgentRuntime>;
|
|
215
|
+
archiveSession?(options: ProviderSessionArchiveOptions): Promise<ProviderSessionArchiveResult>;
|
|
216
|
+
close?: () => Promise<void>;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface ProviderInstanceCreateOptions {
|
|
220
|
+
instanceId: string;
|
|
221
|
+
config: Record<string, unknown>;
|
|
222
|
+
host: ExtensionHostContext;
|
|
223
|
+
data: DataConfig;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export interface ProviderContributionModule {
|
|
227
|
+
kind: "provider";
|
|
228
|
+
configSchema?: Record<string, unknown>;
|
|
229
|
+
createInstance(options: ProviderInstanceCreateOptions): Promise<ProviderInstance> | ProviderInstance;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface ConnectorInstanceCreateOptions {
|
|
233
|
+
instanceId: string;
|
|
234
|
+
config: Record<string, unknown>;
|
|
235
|
+
host: ExtensionHostContext;
|
|
236
|
+
attachmentsRoot: string;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface ConnectorContributionModule {
|
|
240
|
+
kind: "connector";
|
|
241
|
+
configSchema?: Record<string, unknown>;
|
|
242
|
+
createInstance(options: ConnectorInstanceCreateOptions): Promise<ConnectorPlugin> | ConnectorPlugin;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export interface SandboxInstance {
|
|
246
|
+
id: string;
|
|
247
|
+
executor: Executor;
|
|
248
|
+
close?: () => Promise<void>;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface SandboxInstanceCreateOptions {
|
|
252
|
+
instanceId: string;
|
|
253
|
+
config: Record<string, unknown>;
|
|
254
|
+
host: ExtensionHostContext;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export interface SandboxContributionModule {
|
|
258
|
+
kind: "sandbox";
|
|
259
|
+
configSchema?: Record<string, unknown>;
|
|
260
|
+
createInstance(options: SandboxInstanceCreateOptions): Promise<SandboxInstance> | SandboxInstance;
|
|
261
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|