twindex-openclaw-plugin 0.6.0 → 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 +88 -7
- package/src/poll-service.ts +21 -5
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";
|
|
@@ -36,6 +36,32 @@ export default function register(api: any) {
|
|
|
36
36
|
const pollService = createNotificationService(api);
|
|
37
37
|
api.registerService?.(pollService);
|
|
38
38
|
|
|
39
|
+
// ── Auto-detect chatTarget from Telegram channel config ─────────
|
|
40
|
+
function inferChatTarget(): string | undefined {
|
|
41
|
+
const config = cfg();
|
|
42
|
+
if (config.chatTarget) return config.chatTarget;
|
|
43
|
+
|
|
44
|
+
// Infer from Telegram allowFrom list (first allowed chat ID)
|
|
45
|
+
const telegramAllow = api.config?.channels?.telegram?.allowFrom;
|
|
46
|
+
if (Array.isArray(telegramAllow) && telegramAllow.length > 0) {
|
|
47
|
+
return String(telegramAllow[0]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Infer from Slack channel config
|
|
51
|
+
const slackChannel = api.config?.channels?.slack?.channel;
|
|
52
|
+
if (slackChannel) return slackChannel;
|
|
53
|
+
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function inferChatChannel(): string {
|
|
58
|
+
const config = cfg();
|
|
59
|
+
if (config.chatChannel) return config.chatChannel;
|
|
60
|
+
if (api.config?.channels?.telegram?.allowFrom?.length > 0) return "telegram";
|
|
61
|
+
if (api.config?.channels?.slack?.channel) return "slack";
|
|
62
|
+
return "telegram";
|
|
63
|
+
}
|
|
64
|
+
|
|
39
65
|
// ── Auto-bootstrap: register + subscribe ────────────────────────
|
|
40
66
|
|
|
41
67
|
(async () => {
|
|
@@ -60,7 +86,16 @@ export default function register(api: any) {
|
|
|
60
86
|
}
|
|
61
87
|
}
|
|
62
88
|
|
|
63
|
-
|
|
89
|
+
// Auto-detect chatTarget from channel config if not explicitly set
|
|
90
|
+
const updates: Record<string, any> = { apiKey };
|
|
91
|
+
const chatTarget = inferChatTarget();
|
|
92
|
+
if (chatTarget) {
|
|
93
|
+
updates.chatTarget = chatTarget;
|
|
94
|
+
updates.chatChannel = inferChatChannel();
|
|
95
|
+
api.logger?.info?.(`Twindex: auto-detected chatTarget=${chatTarget}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
persistConfig(updates);
|
|
64
99
|
pollService.start();
|
|
65
100
|
api.logger?.info?.(
|
|
66
101
|
"Twindex: auto-bootstrap complete. Delivery via polling.",
|
|
@@ -422,19 +457,18 @@ export default function register(api: any) {
|
|
|
422
457
|
api.registerTool({
|
|
423
458
|
name: "twindex_upload_photo",
|
|
424
459
|
description:
|
|
425
|
-
"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.",
|
|
426
461
|
parameters: {
|
|
427
462
|
type: "object",
|
|
428
463
|
properties: {
|
|
429
464
|
photo_url: {
|
|
430
465
|
type: "string",
|
|
431
466
|
description:
|
|
432
|
-
"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.",
|
|
433
468
|
},
|
|
434
469
|
},
|
|
435
|
-
required: ["photo_url"],
|
|
436
470
|
},
|
|
437
|
-
async execute(_id: string, params: { photo_url
|
|
471
|
+
async execute(_id: string, params: { photo_url?: string }) {
|
|
438
472
|
const apiKey = cfg().apiKey;
|
|
439
473
|
if (!apiKey) {
|
|
440
474
|
return {
|
|
@@ -445,7 +479,54 @@ export default function register(api: any) {
|
|
|
445
479
|
}
|
|
446
480
|
|
|
447
481
|
try {
|
|
448
|
-
|
|
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
|
+
|
|
449
530
|
return {
|
|
450
531
|
content: [
|
|
451
532
|
{
|
package/src/poll-service.ts
CHANGED
|
@@ -42,10 +42,19 @@ export function createNotificationService(api: any) {
|
|
|
42
42
|
|
|
43
43
|
function getChatTarget(): string | undefined {
|
|
44
44
|
const raw = api.config?.plugins?.entries?.twindex?.config?.chatTarget;
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
if (raw) {
|
|
46
|
+
// Strip channel prefix if agent included it (e.g. "telegram:123" → "123")
|
|
47
|
+
const colonIdx = raw.indexOf(":");
|
|
48
|
+
return colonIdx >= 0 ? raw.slice(colonIdx + 1) : raw;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Fallback: infer from Telegram allowFrom
|
|
52
|
+
const telegramAllow = api.config?.channels?.telegram?.allowFrom;
|
|
53
|
+
if (Array.isArray(telegramAllow) && telegramAllow.length > 0) {
|
|
54
|
+
return String(telegramAllow[0]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return undefined;
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
function getChatChannel(): string {
|
|
@@ -53,7 +62,14 @@ export function createNotificationService(api: any) {
|
|
|
53
62
|
// Infer channel from prefix if present (e.g. "telegram:123" → "telegram")
|
|
54
63
|
const colonIdx = raw.indexOf(":");
|
|
55
64
|
if (colonIdx > 0) return raw.slice(0, colonIdx);
|
|
56
|
-
|
|
65
|
+
|
|
66
|
+
const explicit = api.config?.plugins?.entries?.twindex?.config?.chatChannel;
|
|
67
|
+
if (explicit) return explicit;
|
|
68
|
+
|
|
69
|
+
// Fallback: infer from which channel is configured
|
|
70
|
+
if (api.config?.channels?.telegram?.allowFrom?.length > 0) return "telegram";
|
|
71
|
+
if (api.config?.channels?.slack?.channel) return "slack";
|
|
72
|
+
return "telegram";
|
|
57
73
|
}
|
|
58
74
|
|
|
59
75
|
const logger = api.logger;
|