twindex-openclaw-plugin 0.6.1 → 0.6.2
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/package.json +1 -1
- package/src/client.ts +24 -1
- package/src/index.ts +52 -6
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -173,7 +173,7 @@ interface TryOnResult {
|
|
|
173
173
|
product: { title: string; price: string; buy_url: string };
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
/** Upload a selfie for personalized merch try-on. */
|
|
176
|
+
/** Upload a selfie for personalized merch try-on (from URL). */
|
|
177
177
|
export async function uploadPhoto(
|
|
178
178
|
apiKey: string,
|
|
179
179
|
photoUrl: string,
|
|
@@ -185,6 +185,29 @@ export async function uploadPhoto(
|
|
|
185
185
|
});
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
/** Upload a selfie from raw file bytes (multipart). */
|
|
189
|
+
export async function uploadPhotoFile(
|
|
190
|
+
apiKey: string,
|
|
191
|
+
fileData: Uint8Array,
|
|
192
|
+
filename: string,
|
|
193
|
+
): Promise<void> {
|
|
194
|
+
const formData = new FormData();
|
|
195
|
+
formData.append("photo", new Blob([fileData]), filename);
|
|
196
|
+
|
|
197
|
+
const url = `${BASE_URL}/api/v1/me/photo/upload`;
|
|
198
|
+
const res = await fetch(url, {
|
|
199
|
+
method: "POST",
|
|
200
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
201
|
+
body: formData,
|
|
202
|
+
signal: AbortSignal.timeout(30_000),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (!res.ok) {
|
|
206
|
+
const body = await res.text().catch(() => "");
|
|
207
|
+
throw new Error(`Twindex API ${res.status}: ${body}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
188
211
|
/** Generate an image of the user wearing merch. */
|
|
189
212
|
export async function tryOn(
|
|
190
213
|
apiKey: string,
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from "fs";
|
|
1
|
+
import { readFileSync, writeFileSync, readdirSync, statSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import * as twindex from "./client.js";
|
|
@@ -457,19 +457,18 @@ export default function register(api: any) {
|
|
|
457
457
|
api.registerTool({
|
|
458
458
|
name: "twindex_upload_photo",
|
|
459
459
|
description:
|
|
460
|
-
"Upload the user's
|
|
460
|
+
"Upload the user's selfie for personalized merch try-on. When the user sends a photo in chat, call this tool with NO parameters — it automatically finds the most recent photo from the conversation. One photo per user — uploading replaces the previous one.",
|
|
461
461
|
parameters: {
|
|
462
462
|
type: "object",
|
|
463
463
|
properties: {
|
|
464
464
|
photo_url: {
|
|
465
465
|
type: "string",
|
|
466
466
|
description:
|
|
467
|
-
"URL of the user's photo
|
|
467
|
+
"Optional. URL of the user's photo. If omitted, automatically uses the most recent photo sent in chat.",
|
|
468
468
|
},
|
|
469
469
|
},
|
|
470
|
-
required: ["photo_url"],
|
|
471
470
|
},
|
|
472
|
-
async execute(_id: string, params: { photo_url
|
|
471
|
+
async execute(_id: string, params: { photo_url?: string }) {
|
|
473
472
|
const apiKey = cfg().apiKey;
|
|
474
473
|
if (!apiKey) {
|
|
475
474
|
return {
|
|
@@ -480,7 +479,54 @@ export default function register(api: any) {
|
|
|
480
479
|
}
|
|
481
480
|
|
|
482
481
|
try {
|
|
483
|
-
|
|
482
|
+
if (params.photo_url) {
|
|
483
|
+
await twindex.uploadPhoto(apiKey, params.photo_url);
|
|
484
|
+
} else {
|
|
485
|
+
// Find the most recent inbound photo from OpenClaw media
|
|
486
|
+
const inboundDir = join(homedir(), ".openclaw", "media", "inbound");
|
|
487
|
+
let files: string[];
|
|
488
|
+
try {
|
|
489
|
+
files = readdirSync(inboundDir).filter((f) =>
|
|
490
|
+
/\.(jpg|jpeg|png)$/i.test(f),
|
|
491
|
+
);
|
|
492
|
+
} catch {
|
|
493
|
+
return {
|
|
494
|
+
content: [
|
|
495
|
+
{
|
|
496
|
+
type: "text",
|
|
497
|
+
text: "No photos found. Please send a selfie in chat first.",
|
|
498
|
+
},
|
|
499
|
+
],
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (files.length === 0) {
|
|
504
|
+
return {
|
|
505
|
+
content: [
|
|
506
|
+
{
|
|
507
|
+
type: "text",
|
|
508
|
+
text: "No photos found in chat. Please send a selfie first.",
|
|
509
|
+
},
|
|
510
|
+
],
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Sort by modification time, newest first
|
|
515
|
+
files.sort((a, b) => {
|
|
516
|
+
const aStat = statSync(join(inboundDir, a));
|
|
517
|
+
const bStat = statSync(join(inboundDir, b));
|
|
518
|
+
return bStat.mtimeMs - aStat.mtimeMs;
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
const latestFile = join(inboundDir, files[0]);
|
|
522
|
+
const fileData = readFileSync(latestFile);
|
|
523
|
+
await twindex.uploadPhotoFile(
|
|
524
|
+
apiKey,
|
|
525
|
+
new Uint8Array(fileData),
|
|
526
|
+
files[0],
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
484
530
|
return {
|
|
485
531
|
content: [
|
|
486
532
|
{
|