@ascegu/teamily 1.0.31 → 1.0.33
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/README.md +14 -14
- package/package.json +1 -1
- package/src/monitor.ts +6 -3
- package/src/upload.ts +65 -33
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Integrates [Teamily](https://teamily.ai/) with OpenClaw as a self-hosted team me
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
openclaw plugins install @
|
|
8
|
+
openclaw plugins install @teamily/teamily
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
Or update an existing install:
|
|
@@ -26,10 +26,10 @@ openclaw channel configure teamily
|
|
|
26
26
|
|
|
27
27
|
### Server Settings
|
|
28
28
|
|
|
29
|
-
| Field | Description | Default
|
|
30
|
-
| -------- | --------------------- |
|
|
31
|
-
| `apiURL` | Teamily REST API URL | `https://imserver
|
|
32
|
-
| `wsURL` | Teamily WebSocket URL | `wss://imserver
|
|
29
|
+
| Field | Description | Default |
|
|
30
|
+
| -------- | --------------------- | --------------------------------------- |
|
|
31
|
+
| `apiURL` | Teamily REST API URL | `https://imserver.teamily.ai/im_api` |
|
|
32
|
+
| `wsURL` | Teamily WebSocket URL | `wss://imserver.teamily.ai/msg_gateway` |
|
|
33
33
|
|
|
34
34
|
### Bot Account Settings
|
|
35
35
|
|
|
@@ -56,8 +56,8 @@ channels:
|
|
|
56
56
|
teamily:
|
|
57
57
|
enabled: true
|
|
58
58
|
server:
|
|
59
|
-
apiURL: https://imserver
|
|
60
|
-
wsURL: wss://imserver
|
|
59
|
+
apiURL: https://imserver.teamily.ai/im_api
|
|
60
|
+
wsURL: wss://imserver.teamily.ai/msg_gateway
|
|
61
61
|
accounts:
|
|
62
62
|
default:
|
|
63
63
|
userID: "bot-user-id"
|
|
@@ -89,12 +89,12 @@ openclaw message send teamily:user:userID --media /path/to/image.jpg
|
|
|
89
89
|
|
|
90
90
|
Supported media types are auto-detected by file extension:
|
|
91
91
|
|
|
92
|
-
| Extension
|
|
93
|
-
|
|
|
94
|
-
| `.jpg`, `.png`, `.gif`, etc.
|
|
95
|
-
| `.mp4`, `.mov`, `.webm`
|
|
96
|
-
| `.mp3`, `.m4a`, `.wav`
|
|
97
|
-
| `.pdf`, `.doc`, `.
|
|
92
|
+
| Extension | Type |
|
|
93
|
+
| ----------------------------------------------------- | ----- |
|
|
94
|
+
| `.jpg`, `.png`, `.gif`, etc. | Image |
|
|
95
|
+
| `.mp4`, `.mov`, `.webm` | Video |
|
|
96
|
+
| `.mp3`, `.m4a`, `.wav` | Audio |
|
|
97
|
+
| `.pdf`, `.doc`, `.zip`, `.json`, `.txt`, `.csv`, etc. | File |
|
|
98
98
|
|
|
99
99
|
## Group Chat Behavior
|
|
100
100
|
|
|
@@ -130,9 +130,9 @@ src/
|
|
|
130
130
|
config-schema.ts Zod config schema (server, accounts, DM security)
|
|
131
131
|
accounts.ts Account resolution and listing
|
|
132
132
|
normalize.ts Target ID normalization (user:ID, group:ID)
|
|
133
|
+
upload.ts Media category detection, MIME types, MP4 metadata parsing, video snapshot extraction
|
|
133
134
|
probe.ts Health check via REST API
|
|
134
135
|
runtime.ts Plugin runtime store
|
|
135
|
-
send.ts REST API message/media send (fallback path)
|
|
136
136
|
```
|
|
137
137
|
|
|
138
138
|
## License
|
package/package.json
CHANGED
package/src/monitor.ts
CHANGED
|
@@ -308,7 +308,10 @@ export class TeamilyMonitor {
|
|
|
308
308
|
|
|
309
309
|
// Try to extract a real first-frame snapshot via ffmpeg; fall back to
|
|
310
310
|
// a minimal 1x1 JPEG placeholder if ffmpeg is unavailable.
|
|
311
|
-
const
|
|
311
|
+
const snapshot = await extractVideoSnapshot(buffer);
|
|
312
|
+
const snapshotBuf = snapshot?.buffer ?? Buffer.from(MINIMAL_JPEG);
|
|
313
|
+
const snapWidth = snapshot?.width || meta.width || 1;
|
|
314
|
+
const snapHeight = snapshot?.height || meta.height || 1;
|
|
312
315
|
const snapshotFile = new File([new Uint8Array(snapshotBuf)], "snapshot.jpg", {
|
|
313
316
|
type: "image/jpeg",
|
|
314
317
|
});
|
|
@@ -325,8 +328,8 @@ export class TeamilyMonitor {
|
|
|
325
328
|
snapshotUUID: crypto.randomUUID(),
|
|
326
329
|
snapshotSize: snapshotBuf.length,
|
|
327
330
|
snapshotUrl: "",
|
|
328
|
-
snapshotWidth:
|
|
329
|
-
snapshotHeight:
|
|
331
|
+
snapshotWidth: snapWidth,
|
|
332
|
+
snapshotHeight: snapHeight,
|
|
330
333
|
videoFile,
|
|
331
334
|
snapshotFile,
|
|
332
335
|
});
|
package/src/upload.ts
CHANGED
|
@@ -125,39 +125,48 @@ export function guessContentType(filePath: string): string {
|
|
|
125
125
|
* Returns null if ffmpeg is unavailable or extraction fails.
|
|
126
126
|
* The thumbnail is scaled down to fit within maxDim (default 320px).
|
|
127
127
|
*/
|
|
128
|
-
export
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const { execFile } = await import("node:child_process");
|
|
134
|
-
const { promisify } = await import("node:util");
|
|
135
|
-
const { writeFile, readFile, unlink, mkdtemp } = await import("node:fs/promises");
|
|
136
|
-
const { tmpdir } = await import("node:os");
|
|
137
|
-
const execFileAsync = promisify(execFile);
|
|
128
|
+
export interface SnapshotResult {
|
|
129
|
+
buffer: Buffer;
|
|
130
|
+
width: number;
|
|
131
|
+
height: number;
|
|
132
|
+
}
|
|
138
133
|
|
|
139
|
-
|
|
134
|
+
export async function extractVideoSnapshot(videoBuf: Buffer): Promise<SnapshotResult | null> {
|
|
135
|
+
const { execFile } = await import("node:child_process");
|
|
136
|
+
const { promisify } = await import("node:util");
|
|
137
|
+
const { writeFile, readFile, unlink, mkdtemp } = await import("node:fs/promises");
|
|
138
|
+
const { tmpdir } = await import("node:os");
|
|
139
|
+
const execFileAsync = promisify(execFile);
|
|
140
|
+
|
|
141
|
+
let dir: string | undefined;
|
|
142
|
+
try {
|
|
143
|
+
dir = await mkdtemp(path.join(tmpdir(), "oc-snap-"));
|
|
140
144
|
const inPath = path.join(dir, "input.mp4");
|
|
141
145
|
const outPath = path.join(dir, "snapshot.jpg");
|
|
142
146
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return await readFile(outPath);
|
|
153
|
-
} finally {
|
|
154
|
-
await unlink(inPath).catch(() => {});
|
|
155
|
-
await unlink(outPath).catch(() => {});
|
|
156
|
-
const { rmdir } = await import("node:fs/promises");
|
|
157
|
-
await rmdir(dir).catch(() => {});
|
|
147
|
+
await writeFile(inPath, videoBuf);
|
|
148
|
+
await execFileAsync("ffmpeg", ["-y", "-i", inPath, "-frames:v", "1", outPath], {
|
|
149
|
+
timeout: 15_000,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const snap = await readFile(outPath);
|
|
153
|
+
if (snap.length === 0) {
|
|
154
|
+
console.warn("[teamily] ffmpeg produced empty snapshot");
|
|
155
|
+
return null;
|
|
158
156
|
}
|
|
159
|
-
|
|
157
|
+
const dims = parseJpegDimensions(snap);
|
|
158
|
+
console.info(
|
|
159
|
+
`[teamily] extracted video snapshot: ${snap.length} bytes, ${dims.width}x${dims.height}`,
|
|
160
|
+
);
|
|
161
|
+
return { buffer: snap, width: dims.width, height: dims.height };
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.warn("[teamily] ffmpeg snapshot extraction failed:", err);
|
|
160
164
|
return null;
|
|
165
|
+
} finally {
|
|
166
|
+
if (dir) {
|
|
167
|
+
const { rm } = await import("node:fs/promises");
|
|
168
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
169
|
+
}
|
|
161
170
|
}
|
|
162
171
|
}
|
|
163
172
|
|
|
@@ -236,12 +245,7 @@ interface BoxInfo {
|
|
|
236
245
|
}
|
|
237
246
|
|
|
238
247
|
/** Find a box by type within [start, end) range. */
|
|
239
|
-
function findBox(
|
|
240
|
-
buf: Buffer,
|
|
241
|
-
start: number,
|
|
242
|
-
end: number,
|
|
243
|
-
type: string,
|
|
244
|
-
): BoxInfo | null {
|
|
248
|
+
function findBox(buf: Buffer, start: number, end: number, type: string): BoxInfo | null {
|
|
245
249
|
let offset = start;
|
|
246
250
|
while (offset + 8 <= end) {
|
|
247
251
|
let size = buf.readUInt32BE(offset);
|
|
@@ -263,3 +267,31 @@ function findBox(
|
|
|
263
267
|
}
|
|
264
268
|
return null;
|
|
265
269
|
}
|
|
270
|
+
|
|
271
|
+
/** Parse width/height from a JPEG buffer by scanning for SOF markers. */
|
|
272
|
+
function parseJpegDimensions(buf: Buffer): { width: number; height: number } {
|
|
273
|
+
let i = 0;
|
|
274
|
+
while (i + 1 < buf.length) {
|
|
275
|
+
if (buf[i] !== 0xff) {
|
|
276
|
+
i++;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const marker = buf[i + 1];
|
|
280
|
+
// SOF0–SOF3 (0xC0–0xC3): baseline, progressive, etc.
|
|
281
|
+
if (marker >= 0xc0 && marker <= 0xc3 && i + 9 < buf.length) {
|
|
282
|
+
const height = buf.readUInt16BE(i + 5);
|
|
283
|
+
const width = buf.readUInt16BE(i + 7);
|
|
284
|
+
return { width, height };
|
|
285
|
+
}
|
|
286
|
+
// Skip marker segment (length follows the 2-byte marker)
|
|
287
|
+
if (marker === 0xd8 || marker === 0xd9) {
|
|
288
|
+
i += 2; // SOI/EOI have no payload
|
|
289
|
+
} else if (i + 3 < buf.length) {
|
|
290
|
+
const len = buf.readUInt16BE(i + 2);
|
|
291
|
+
i += 2 + len;
|
|
292
|
+
} else {
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return { width: 0, height: 0 };
|
|
297
|
+
}
|