@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 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 @ascegu/teamily
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-test.teamily.ai/im_api` |
32
- | `wsURL` | Teamily WebSocket URL | `wss://imserver-test.teamily.ai/msg_gateway` |
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-test.teamily.ai/im_api
60
- wsURL: wss://imserver-test.teamily.ai/msg_gateway
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 | Type |
93
- | ------------------------------- | ----- |
94
- | `.jpg`, `.png`, `.gif`, etc. | Image |
95
- | `.mp4`, `.mov`, `.webm` | Video |
96
- | `.mp3`, `.m4a`, `.wav` | Audio |
97
- | `.pdf`, `.doc`, `.docx`, `.zip` | File |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ascegu/teamily",
3
- "version": "1.0.31",
3
+ "version": "1.0.33",
4
4
  "description": "OpenClaw Teamily channel plugin - Team instant messaging server integration",
5
5
  "keywords": [
6
6
  "channel",
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 snapshotBuf = (await extractVideoSnapshot(buffer)) ?? Buffer.from(MINIMAL_JPEG);
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: meta.width || 1,
329
- snapshotHeight: meta.height || 1,
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 async function extractVideoSnapshot(
129
- videoBuf: Buffer,
130
- maxDim = 320,
131
- ): Promise<Buffer | null> {
132
- try {
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
- const dir = await mkdtemp(path.join(tmpdir(), "oc-snap-"));
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
- try {
144
- await writeFile(inPath, videoBuf);
145
- await execFileAsync("ffmpeg", [
146
- "-i", inPath,
147
- "-vframes", "1",
148
- "-vf", `scale=min(${maxDim}\\,iw):-1`,
149
- "-q:v", "6",
150
- "-y", outPath,
151
- ], { timeout: 10_000 });
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
- } catch {
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
+ }