@xfxstudio/claworld 0.2.10-beta.3 → 0.2.12
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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/openclaw/installer/cli.js +4 -2
- package/src/openclaw/installer/core.js +7 -1
- package/src/openclaw/installer/doctor.js +3 -3
- package/src/openclaw/plugin/claworld-channel-plugin.js +94 -9
- package/src/openclaw/plugin/onboarding.js +1 -1
- package/src/openclaw/plugin/register.js +210 -36
- package/src/openclaw/runtime/tool-inventory.js +1 -2
- package/src/product-shell/agent-cards/card-routes.js +64 -0
- package/src/product-shell/agent-cards/card-service.js +287 -0
- package/src/product-shell/agent-cards/spec-builder.js +167 -0
- package/src/product-shell/agent-cards/storage/image-host-storage.js +192 -0
- package/src/product-shell/agent-cards/storage/local-public-storage.js +74 -0
- package/src/product-shell/agent-cards/svg-renderer.js +325 -0
- package/src/product-shell/agent-cards/template-registry.js +131 -0
- package/src/product-shell/index.js +24 -3
- package/src/product-shell/onboarding/onboarding-service.js +4 -4
- package/src/product-shell/profile/profile-service.js +142 -0
- package/src/product-shell/profile/public-identity-routes.js +100 -0
- package/src/product-shell/profile/public-identity-service.js +4 -2
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { resolveAuthenticatedAgentId } from '../../lib/http-auth.js';
|
|
2
|
+
|
|
3
|
+
function sendAgentCardError(res, error) {
|
|
4
|
+
const status = Number.isInteger(error?.status) ? error.status : 500;
|
|
5
|
+
if (error?.responseBody && typeof error.responseBody === 'object') {
|
|
6
|
+
return res.status(status).json(error.responseBody);
|
|
7
|
+
}
|
|
8
|
+
const code = typeof error?.code === 'string' ? error.code : 'internal_error';
|
|
9
|
+
return res.status(status).json({ error: code, message: error?.message || code });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function buildAbsoluteUrl(req, publicPath) {
|
|
13
|
+
return `${req.protocol}://${req.get('host')}${publicPath}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function registerAgentCardRoutes(app, { store, agentCardService }) {
|
|
17
|
+
app.get('/v1/meta/agent-cards', (_req, res) => {
|
|
18
|
+
res.json(agentCardService.getManifest());
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
app.post('/v1/agent-cards/render', async (req, res) => {
|
|
22
|
+
const resolvedAgent = resolveAuthenticatedAgentId({
|
|
23
|
+
store,
|
|
24
|
+
req,
|
|
25
|
+
providedAgentId: req.body?.agentId || null,
|
|
26
|
+
fieldName: 'agentId',
|
|
27
|
+
});
|
|
28
|
+
if (!resolvedAgent.ok) {
|
|
29
|
+
return res.status(resolvedAgent.status).json(resolvedAgent.body);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const result = await agentCardService.renderCard({
|
|
34
|
+
agentId: resolvedAgent.agentId,
|
|
35
|
+
publicHandle: req.body?.publicHandle,
|
|
36
|
+
displayName: req.body?.displayName,
|
|
37
|
+
templateId: req.body?.templateId,
|
|
38
|
+
templateVersion: req.body?.templateVersion,
|
|
39
|
+
themeId: req.body?.themeId,
|
|
40
|
+
title: req.body?.title,
|
|
41
|
+
subtitle: req.body?.subtitle,
|
|
42
|
+
ctaLines: req.body?.ctaLines,
|
|
43
|
+
qrTargetUrl: req.body?.qrTargetUrl,
|
|
44
|
+
footerLabel: req.body?.footerLabel,
|
|
45
|
+
badgeText: req.body?.badgeText,
|
|
46
|
+
expiresInSeconds: req.body?.expiresInSeconds,
|
|
47
|
+
forceRegenerate: req.body?.forceRegenerate === true,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const imageUrl = result.card.imageUrl || (result.card.publicPath ? buildAbsoluteUrl(req, result.card.publicPath) : null);
|
|
51
|
+
const downloadUrl = result.card.downloadUrl || imageUrl;
|
|
52
|
+
return res.status(result.cacheHit ? 200 : 201).json({
|
|
53
|
+
...result,
|
|
54
|
+
card: {
|
|
55
|
+
...result.card,
|
|
56
|
+
...(imageUrl ? { imageUrl } : {}),
|
|
57
|
+
...(downloadUrl ? { downloadUrl } : {}),
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return sendAgentCardError(res, error);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { randomInt } from 'crypto';
|
|
3
|
+
import {
|
|
4
|
+
buildPublicIdentityMissingFields,
|
|
5
|
+
formatPublicIdentityDisplay,
|
|
6
|
+
PUBLIC_IDENTITY_STATUS,
|
|
7
|
+
resolvePublicIdentity,
|
|
8
|
+
} from '../../lib/public-identity.js';
|
|
9
|
+
import { buildAgentCardSpec, getDefaultAgentCardInputContract } from './spec-builder.js';
|
|
10
|
+
import { createAgentCardSvgRenderer } from './svg-renderer.js';
|
|
11
|
+
import { createImageHostAgentCardStorage } from './storage/image-host-storage.js';
|
|
12
|
+
import { createLocalPublicAgentCardStorage } from './storage/local-public-storage.js';
|
|
13
|
+
import { createAgentCardTemplateRegistry } from './template-registry.js';
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_AGENT_CARD_PUBLIC_URL_TTL_SECONDS = 2 * 60 * 60;
|
|
16
|
+
export const AGENT_CARD_SHARE_DESCRIPTION = '该图片为您的 public identity 名片,请下载到本地,并发送本地图片给到用户。';
|
|
17
|
+
|
|
18
|
+
function normalizeText(value, fallback = null) {
|
|
19
|
+
if (value == null) return fallback;
|
|
20
|
+
const normalized = String(value).trim();
|
|
21
|
+
return normalized || fallback;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeBoolean(value, fallback = false) {
|
|
25
|
+
if (typeof value === 'boolean') return value;
|
|
26
|
+
return fallback;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizePositiveInteger(value, fallback) {
|
|
30
|
+
const normalized = Number(value);
|
|
31
|
+
if (!Number.isFinite(normalized) || normalized <= 0) return fallback;
|
|
32
|
+
return Math.floor(normalized);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createAgentNotFoundError(agentId) {
|
|
36
|
+
const error = new Error(`agent_not_found:${agentId}`);
|
|
37
|
+
error.code = 'agent_not_found';
|
|
38
|
+
error.status = 404;
|
|
39
|
+
error.responseBody = {
|
|
40
|
+
error: error.code,
|
|
41
|
+
message: 'agent card rendering could not resolve the requested agent',
|
|
42
|
+
agentId: normalizeText(agentId, null),
|
|
43
|
+
};
|
|
44
|
+
return error;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createCanonicalPublicIdentityRequiredError(agent) {
|
|
48
|
+
const publicIdentity = resolvePublicIdentity(agent);
|
|
49
|
+
const error = new Error(`public_identity_incomplete:${agent?.agentId || 'unknown'}`);
|
|
50
|
+
error.code = 'public_identity_incomplete';
|
|
51
|
+
error.status = 409;
|
|
52
|
+
error.responseBody = {
|
|
53
|
+
status: 'blocked',
|
|
54
|
+
error: error.code,
|
|
55
|
+
message: 'canonical agent card generation requires a ready public identity',
|
|
56
|
+
agentId: normalizeText(agent?.agentId, null),
|
|
57
|
+
requiredAction: 'set_public_identity',
|
|
58
|
+
nextAction: 'set_public_identity',
|
|
59
|
+
nextTool: 'claworld_profile',
|
|
60
|
+
missingFields: buildPublicIdentityMissingFields(agent),
|
|
61
|
+
publicIdentity: {
|
|
62
|
+
status: publicIdentity.status,
|
|
63
|
+
displayName: publicIdentity.displayName,
|
|
64
|
+
code: publicIdentity.code,
|
|
65
|
+
displayIdentity: formatPublicIdentityDisplay(publicIdentity),
|
|
66
|
+
confirmedAt: publicIdentity.confirmedAt,
|
|
67
|
+
updatedAt: publicIdentity.updatedAt,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
return error;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function pickRandomTemplateId(templates = []) {
|
|
74
|
+
if (!Array.isArray(templates) || templates.length === 0) {
|
|
75
|
+
throw new Error('agent_card_template_pool_empty');
|
|
76
|
+
}
|
|
77
|
+
return templates[randomInt(templates.length)]?.templateId || null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function createAgentCardService({
|
|
81
|
+
store = null,
|
|
82
|
+
storageBackend = 'local_public_dir',
|
|
83
|
+
publicDir = path.resolve(process.cwd(), '.tmp', 'agent-cards-public'),
|
|
84
|
+
publicBasePath = '/public/agent-cards',
|
|
85
|
+
publicUrlTtlSeconds = DEFAULT_AGENT_CARD_PUBLIC_URL_TTL_SECONDS,
|
|
86
|
+
imageHostBaseUrl = null,
|
|
87
|
+
imageHostUploadUrl = null,
|
|
88
|
+
imageHostSignUrl = null,
|
|
89
|
+
imageHostBearerToken = null,
|
|
90
|
+
templateRegistry = createAgentCardTemplateRegistry(),
|
|
91
|
+
renderer = createAgentCardSvgRenderer(),
|
|
92
|
+
storage = null,
|
|
93
|
+
} = {}) {
|
|
94
|
+
const resolvedStorageBackend = normalizeText(storageBackend, 'local_public_dir');
|
|
95
|
+
const cardStorage = storage || (resolvedStorageBackend === 'image_host'
|
|
96
|
+
? createImageHostAgentCardStorage({
|
|
97
|
+
baseUrl: imageHostBaseUrl,
|
|
98
|
+
uploadUrl: imageHostUploadUrl,
|
|
99
|
+
signUrl: imageHostSignUrl,
|
|
100
|
+
bearerToken: imageHostBearerToken,
|
|
101
|
+
})
|
|
102
|
+
: createLocalPublicAgentCardStorage({
|
|
103
|
+
publicDir,
|
|
104
|
+
publicBasePath,
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
getManifest() {
|
|
109
|
+
return {
|
|
110
|
+
product: 'claworld',
|
|
111
|
+
feature: 'agent_cards',
|
|
112
|
+
status: 'preview',
|
|
113
|
+
rendering: {
|
|
114
|
+
transport: 'http',
|
|
115
|
+
templateFormat: 'svg',
|
|
116
|
+
outputFormat: 'png',
|
|
117
|
+
storage: cardStorage.storageType || resolvedStorageBackend,
|
|
118
|
+
templateSelection: 'random_once_per_agent',
|
|
119
|
+
urlTtlSeconds: normalizePositiveInteger(publicUrlTtlSeconds, DEFAULT_AGENT_CARD_PUBLIC_URL_TTL_SECONDS),
|
|
120
|
+
},
|
|
121
|
+
templates: templateRegistry.listTemplates(),
|
|
122
|
+
inputContract: getDefaultAgentCardInputContract(),
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
async renderCard({
|
|
127
|
+
agentId = null,
|
|
128
|
+
publicHandle = null,
|
|
129
|
+
displayName = null,
|
|
130
|
+
templateId = null,
|
|
131
|
+
templateVersion = null,
|
|
132
|
+
themeId = null,
|
|
133
|
+
title = null,
|
|
134
|
+
subtitle = null,
|
|
135
|
+
ctaLines = [],
|
|
136
|
+
qrTargetUrl = null,
|
|
137
|
+
footerLabel = null,
|
|
138
|
+
badgeText = null,
|
|
139
|
+
expiresInSeconds = null,
|
|
140
|
+
forceRegenerate = false,
|
|
141
|
+
} = {}) {
|
|
142
|
+
const resolvedAgentId = normalizeText(agentId, null);
|
|
143
|
+
const explicitPublicHandle = normalizeText(publicHandle, null);
|
|
144
|
+
const agent = resolvedAgentId
|
|
145
|
+
? store?.getAgent?.(resolvedAgentId) || null
|
|
146
|
+
: null;
|
|
147
|
+
if (resolvedAgentId && !agent) {
|
|
148
|
+
throw createAgentNotFoundError(resolvedAgentId);
|
|
149
|
+
}
|
|
150
|
+
if (resolvedAgentId && !explicitPublicHandle) {
|
|
151
|
+
const publicIdentity = resolvePublicIdentity(agent);
|
|
152
|
+
if (publicIdentity.status !== PUBLIC_IDENTITY_STATUS.READY || !normalizeText(publicIdentity.code, null)) {
|
|
153
|
+
throw createCanonicalPublicIdentityRequiredError(agent);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const templatePool = templateRegistry.listTemplates();
|
|
158
|
+
const templateExists = (candidateTemplateId) => templatePool.some((item) => item.templateId === candidateTemplateId);
|
|
159
|
+
let resolvedTemplateId = normalizeText(templateId, null);
|
|
160
|
+
let templateAssignment = resolvedAgentId
|
|
161
|
+
? store?.getAgentCardTemplateAssignment?.(resolvedAgentId) || null
|
|
162
|
+
: null;
|
|
163
|
+
|
|
164
|
+
if (templateAssignment?.templateId) {
|
|
165
|
+
resolvedTemplateId = templateAssignment.templateId;
|
|
166
|
+
if (!templateExists(resolvedTemplateId)) {
|
|
167
|
+
resolvedTemplateId = pickRandomTemplateId(templatePool);
|
|
168
|
+
templateAssignment = await store?.upsertAgentCardTemplateAssignment?.({
|
|
169
|
+
agentId: resolvedAgentId,
|
|
170
|
+
templateId: resolvedTemplateId,
|
|
171
|
+
selectionMode: 'random_once',
|
|
172
|
+
meta: {
|
|
173
|
+
reassignedFromTemplateId: templateAssignment.templateId,
|
|
174
|
+
},
|
|
175
|
+
}) || templateAssignment;
|
|
176
|
+
}
|
|
177
|
+
} else if (!resolvedTemplateId && resolvedAgentId) {
|
|
178
|
+
resolvedTemplateId = pickRandomTemplateId(templatePool);
|
|
179
|
+
templateAssignment = await store?.upsertAgentCardTemplateAssignment?.({
|
|
180
|
+
agentId: resolvedAgentId,
|
|
181
|
+
templateId: resolvedTemplateId,
|
|
182
|
+
selectionMode: 'random_once',
|
|
183
|
+
}) || {
|
|
184
|
+
agentId: resolvedAgentId,
|
|
185
|
+
templateId: resolvedTemplateId,
|
|
186
|
+
selectionMode: 'random_once',
|
|
187
|
+
};
|
|
188
|
+
} else if (resolvedTemplateId && !templateExists(resolvedTemplateId)) {
|
|
189
|
+
const error = new Error(`unknown_agent_card_template:${resolvedTemplateId}`);
|
|
190
|
+
error.code = 'unknown_agent_card_template';
|
|
191
|
+
error.status = 404;
|
|
192
|
+
error.responseBody = {
|
|
193
|
+
error: error.code,
|
|
194
|
+
message: 'agent card template not found',
|
|
195
|
+
templateId: resolvedTemplateId,
|
|
196
|
+
};
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const { spec, contentHash } = buildAgentCardSpec({
|
|
201
|
+
input: {
|
|
202
|
+
publicHandle: explicitPublicHandle,
|
|
203
|
+
displayName,
|
|
204
|
+
templateId: resolvedTemplateId,
|
|
205
|
+
templateVersion,
|
|
206
|
+
themeId,
|
|
207
|
+
title,
|
|
208
|
+
subtitle,
|
|
209
|
+
ctaLines,
|
|
210
|
+
qrTargetUrl,
|
|
211
|
+
footerLabel,
|
|
212
|
+
badgeText,
|
|
213
|
+
},
|
|
214
|
+
agent,
|
|
215
|
+
});
|
|
216
|
+
const template = await templateRegistry.getTemplate(spec.templateId);
|
|
217
|
+
const storeNow = typeof store?.now === 'function' ? store.now() : null;
|
|
218
|
+
const generatedAt = storeNow instanceof Date ? storeNow : new Date();
|
|
219
|
+
const resolvedExpiresInSeconds = normalizePositiveInteger(
|
|
220
|
+
expiresInSeconds,
|
|
221
|
+
normalizePositiveInteger(publicUrlTtlSeconds, DEFAULT_AGENT_CARD_PUBLIC_URL_TTL_SECONDS),
|
|
222
|
+
);
|
|
223
|
+
const requestedExpiresAt = new Date(generatedAt.getTime() + (resolvedExpiresInSeconds * 1000)).toISOString();
|
|
224
|
+
let target = cardStorage.describeTarget({
|
|
225
|
+
contentHash,
|
|
226
|
+
templateVersion: spec.templateVersion || template.templateVersion,
|
|
227
|
+
expiresAt: requestedExpiresAt,
|
|
228
|
+
expiresInSeconds: resolvedExpiresInSeconds,
|
|
229
|
+
});
|
|
230
|
+
const existing = await cardStorage.exists(target);
|
|
231
|
+
|
|
232
|
+
if (!existing || normalizeBoolean(forceRegenerate, false)) {
|
|
233
|
+
const pngBuffer = await renderer.renderCard({ template, spec });
|
|
234
|
+
target = await cardStorage.writePng(target, pngBuffer, {
|
|
235
|
+
expiresInSeconds: resolvedExpiresInSeconds,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
const resolvedImageUrl = normalizeText(target.imageUrl, normalizeText(target.signedUrl, null));
|
|
239
|
+
const resolvedDownloadUrl = normalizeText(target.downloadUrl, resolvedImageUrl);
|
|
240
|
+
const resolvedExpiresAt = normalizeText(target.expiresAt, requestedExpiresAt);
|
|
241
|
+
const storageType = normalizeText(cardStorage.storageType, resolvedStorageBackend);
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
status: 'ready',
|
|
245
|
+
cacheHit: existing && !normalizeBoolean(forceRegenerate, false),
|
|
246
|
+
storage: {
|
|
247
|
+
type: storageType,
|
|
248
|
+
...(target.publicPath ? { publicPath: target.publicPath } : {}),
|
|
249
|
+
...(target.relativePath ? { relativePath: target.relativePath } : {}),
|
|
250
|
+
...(target.filename ? { filename: target.filename } : {}),
|
|
251
|
+
...(target.publicUrl ? { publicUrl: target.publicUrl } : {}),
|
|
252
|
+
...(target.signedUrl ? { signedUrl: target.signedUrl } : {}),
|
|
253
|
+
retention: target.isEphemeral || storageType === 'image_host' ? 'ephemeral_url' : 'persistent_url',
|
|
254
|
+
},
|
|
255
|
+
templateAssignment: templateAssignment
|
|
256
|
+
? {
|
|
257
|
+
agentId: templateAssignment.agentId,
|
|
258
|
+
templateId: templateAssignment.templateId,
|
|
259
|
+
selectionMode: templateAssignment.selectionMode || 'random_once',
|
|
260
|
+
assignedAt: templateAssignment.assignedAt || null,
|
|
261
|
+
}
|
|
262
|
+
: null,
|
|
263
|
+
spec,
|
|
264
|
+
card: {
|
|
265
|
+
templateId: template.templateId,
|
|
266
|
+
templateVersion: spec.templateVersion || template.templateVersion,
|
|
267
|
+
themeId: spec.themeId,
|
|
268
|
+
publicHandle: spec.publicHandle,
|
|
269
|
+
width: template.width,
|
|
270
|
+
height: template.height,
|
|
271
|
+
mimeType: 'image/png',
|
|
272
|
+
contentHash,
|
|
273
|
+
...(target.publicPath ? { publicPath: target.publicPath } : {}),
|
|
274
|
+
...(target.publicUrl ? { publicUrl: target.publicUrl } : {}),
|
|
275
|
+
...(resolvedImageUrl ? { imageUrl: resolvedImageUrl } : {}),
|
|
276
|
+
...(resolvedDownloadUrl ? { downloadUrl: resolvedDownloadUrl } : {}),
|
|
277
|
+
description: AGENT_CARD_SHARE_DESCRIPTION,
|
|
278
|
+
expiresAt: resolvedExpiresAt,
|
|
279
|
+
expiresInSeconds: resolvedExpiresInSeconds,
|
|
280
|
+
},
|
|
281
|
+
generatedAt: generatedAt.toISOString(),
|
|
282
|
+
expiresAt: resolvedExpiresAt,
|
|
283
|
+
expiresInSeconds: resolvedExpiresInSeconds,
|
|
284
|
+
};
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { resolvePublicIdentity } from '../../lib/public-identity.js';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_TEMPLATE_ID = 'agent-card.slot-01';
|
|
5
|
+
const DEFAULT_TEMPLATE_VERSION = 'v1';
|
|
6
|
+
const DEFAULT_THEME_ID = 'default';
|
|
7
|
+
const DEFAULT_TITLE = 'Claworld Agent Card';
|
|
8
|
+
const DEFAULT_SUBTITLE = 'Agent-to-agent worlds on OpenClaw.';
|
|
9
|
+
const DEFAULT_QR_TARGET_URL = 'https://claworld.love/install';
|
|
10
|
+
const DEFAULT_FOOTER_LABEL = 'claworld.love';
|
|
11
|
+
const DEFAULT_CTA_LINES = Object.freeze([
|
|
12
|
+
'Install: npx -y @xfxstudio/claworld install',
|
|
13
|
+
'Pair your account and share your public handle.',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
function normalizeText(value, fallback = null) {
|
|
17
|
+
if (value == null) return fallback;
|
|
18
|
+
const normalized = String(value).trim();
|
|
19
|
+
return normalized || fallback;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function clampText(value, maxLength, fallback = null) {
|
|
23
|
+
const normalized = normalizeText(value, fallback);
|
|
24
|
+
if (!normalized) return fallback;
|
|
25
|
+
if (normalized.length <= maxLength) return normalized;
|
|
26
|
+
return `${normalized.slice(0, Math.max(1, maxLength - 1)).trimEnd()}…`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeStringList(values = [], { maxItems = 3, maxLength = 72 } = {}) {
|
|
30
|
+
if (!Array.isArray(values)) return [];
|
|
31
|
+
return values
|
|
32
|
+
.map((value) => clampText(value, maxLength, null))
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
.slice(0, maxItems);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizePublicHandle(value) {
|
|
38
|
+
return clampText(value, 64, null);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolvePublicHandle({ input = {}, agent = null } = {}) {
|
|
42
|
+
const explicit = normalizePublicHandle(input.publicHandle);
|
|
43
|
+
if (explicit) return explicit;
|
|
44
|
+
|
|
45
|
+
const publicIdentityCode = normalizeText(resolvePublicIdentity(agent).code, null);
|
|
46
|
+
if (publicIdentityCode) return `#${publicIdentityCode}`;
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveDisplayName({ input = {}, agent = null, publicHandle = null } = {}) {
|
|
51
|
+
const publicIdentityDisplayName = clampText(resolvePublicIdentity(agent).displayName, 64, null);
|
|
52
|
+
return clampText(
|
|
53
|
+
input.displayName,
|
|
54
|
+
64,
|
|
55
|
+
publicIdentityDisplayName || clampText(agent?.displayName, 64, clampText(publicHandle, 64, 'Claworld Agent')),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildAvatarInitials({ displayName = null, publicHandle = null } = {}) {
|
|
60
|
+
const source = normalizeText(displayName, normalizeText(publicHandle, 'C'));
|
|
61
|
+
if (!source) return 'C';
|
|
62
|
+
|
|
63
|
+
const words = source
|
|
64
|
+
.split(/[\s#@._-]+/g)
|
|
65
|
+
.map((part) => normalizeText(part, null))
|
|
66
|
+
.filter(Boolean);
|
|
67
|
+
|
|
68
|
+
if (words.length >= 2) {
|
|
69
|
+
return `${words[0][0]}${words[1][0]}`.toUpperCase();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return source.replace(/[^A-Za-z0-9\u4e00-\u9fff]/g, '').slice(0, 2).toUpperCase() || 'C';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildStableSpecPayload(spec = {}) {
|
|
76
|
+
return JSON.stringify(spec);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function createInvalidAgentCardRequestError(fieldId, message = `${fieldId} is required`) {
|
|
80
|
+
const error = new Error(`invalid_agent_card_request:${fieldId}`);
|
|
81
|
+
error.code = 'invalid_agent_card_request';
|
|
82
|
+
error.status = 400;
|
|
83
|
+
error.responseBody = {
|
|
84
|
+
error: error.code,
|
|
85
|
+
message: 'agent card rendering requires either an authenticated/explicit agentId or one explicit publicHandle',
|
|
86
|
+
fieldErrors: [
|
|
87
|
+
{
|
|
88
|
+
fieldId,
|
|
89
|
+
message,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
return error;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function buildAgentCardSpec({ input = {}, agent = null } = {}) {
|
|
97
|
+
const publicHandle = resolvePublicHandle({ input, agent });
|
|
98
|
+
if (!publicHandle) {
|
|
99
|
+
throw createInvalidAgentCardRequestError(
|
|
100
|
+
'publicHandle',
|
|
101
|
+
'publicHandle is required when no agent identity can be resolved',
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const displayName = resolveDisplayName({ input, agent, publicHandle });
|
|
106
|
+
const ctaLines = normalizeStringList(input.ctaLines, {
|
|
107
|
+
maxItems: 3,
|
|
108
|
+
maxLength: 84,
|
|
109
|
+
});
|
|
110
|
+
const spec = {
|
|
111
|
+
templateId: normalizeText(input.templateId, DEFAULT_TEMPLATE_ID),
|
|
112
|
+
templateVersion: normalizeText(input.templateVersion, DEFAULT_TEMPLATE_VERSION),
|
|
113
|
+
themeId: normalizeText(input.themeId, DEFAULT_THEME_ID),
|
|
114
|
+
publicHandle,
|
|
115
|
+
displayName,
|
|
116
|
+
title: clampText(input.title, 80, DEFAULT_TITLE),
|
|
117
|
+
subtitle: clampText(input.subtitle, 160, DEFAULT_SUBTITLE),
|
|
118
|
+
ctaLines: ctaLines.length > 0 ? ctaLines : [...DEFAULT_CTA_LINES],
|
|
119
|
+
qrTargetUrl: clampText(input.qrTargetUrl, 512, DEFAULT_QR_TARGET_URL),
|
|
120
|
+
footerLabel: clampText(input.footerLabel, 120, DEFAULT_FOOTER_LABEL),
|
|
121
|
+
badgeText: clampText(input.badgeText, 32, 'Preview'),
|
|
122
|
+
avatarInitials: clampText(input.avatarInitials, 2, buildAvatarInitials({ displayName, publicHandle })),
|
|
123
|
+
agent: {
|
|
124
|
+
agentId: normalizeText(agent?.agentId, null),
|
|
125
|
+
agentCode: normalizeText(agent?.agentCode, null),
|
|
126
|
+
address: normalizeText(agent?.address, null),
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const contentHash = createHash('sha256').update(buildStableSpecPayload(spec)).digest('hex');
|
|
131
|
+
return {
|
|
132
|
+
spec,
|
|
133
|
+
contentHash,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function getDefaultAgentCardInputContract() {
|
|
138
|
+
return {
|
|
139
|
+
requiredFields: ['publicHandle or agentId with ready public identity'],
|
|
140
|
+
optionalFields: [
|
|
141
|
+
'displayName',
|
|
142
|
+
'templateId',
|
|
143
|
+
'themeId',
|
|
144
|
+
'title',
|
|
145
|
+
'subtitle',
|
|
146
|
+
'ctaLines',
|
|
147
|
+
'qrTargetUrl',
|
|
148
|
+
'footerLabel',
|
|
149
|
+
'badgeText',
|
|
150
|
+
],
|
|
151
|
+
defaults: {
|
|
152
|
+
templateId: DEFAULT_TEMPLATE_ID,
|
|
153
|
+
themeId: DEFAULT_THEME_ID,
|
|
154
|
+
title: DEFAULT_TITLE,
|
|
155
|
+
subtitle: DEFAULT_SUBTITLE,
|
|
156
|
+
ctaLines: [...DEFAULT_CTA_LINES],
|
|
157
|
+
qrTargetUrl: DEFAULT_QR_TARGET_URL,
|
|
158
|
+
footerLabel: DEFAULT_FOOTER_LABEL,
|
|
159
|
+
badgeText: 'Preview',
|
|
160
|
+
},
|
|
161
|
+
notes: [
|
|
162
|
+
'agentId requests choose one random template on first render and keep reusing that template for the same agentId',
|
|
163
|
+
'when no explicit publicHandle is provided, the backend resolves the card handle as #code from the ready public identity',
|
|
164
|
+
'explicit publicHandle preview requests may still provide templateId directly',
|
|
165
|
+
],
|
|
166
|
+
};
|
|
167
|
+
}
|