@xfxstudio/claworld 0.2.13 → 0.2.15
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 +4 -4
- package/index.js +0 -1
- package/openclaw.plugin.json +1 -97
- package/package.json +1 -1
- package/skills/claworld-help/SKILL.md +47 -27
- package/skills/claworld-join-and-chat/SKILL.md +13 -9
- package/src/openclaw/index.js +0 -3
- package/src/openclaw/plugin/account-identity.js +0 -1
- package/src/openclaw/plugin/claworld-channel-plugin.js +73 -319
- package/src/openclaw/plugin/config-schema.js +1 -55
- package/src/openclaw/plugin/managed-config.js +1 -42
- package/src/openclaw/plugin/onboarding.js +1 -1
- package/src/openclaw/plugin/register.js +302 -233
- package/src/openclaw/plugin/relay-client.js +9 -6
- package/src/openclaw/runtime/product-shell-helper.js +11 -364
- package/src/openclaw/runtime/tool-contracts.js +0 -182
- package/src/openclaw/runtime/tool-inventory.js +4 -27
- package/src/lib/agent-profile.js +0 -74
- package/src/lib/http-auth.js +0 -151
- package/src/lib/policy.js +0 -114
- package/src/openclaw/installer/constants.js +0 -14
- package/src/product-shell/agent-cards/card-routes.js +0 -64
- package/src/product-shell/agent-cards/card-service.js +0 -287
- package/src/product-shell/agent-cards/spec-builder.js +0 -167
- package/src/product-shell/agent-cards/storage/image-host-storage.js +0 -192
- package/src/product-shell/agent-cards/storage/local-public-storage.js +0 -74
- package/src/product-shell/agent-cards/svg-renderer.js +0 -325
- package/src/product-shell/agent-cards/template-registry.js +0 -131
- package/src/product-shell/catalog/default-world-catalog.js +0 -38
- package/src/product-shell/contracts/candidate-feed.js +0 -393
- package/src/product-shell/contracts/world-manifest.js +0 -369
- package/src/product-shell/conversation-feedback/conversation-feedback-service.js +0 -261
- package/src/product-shell/feedback/feedback-contract.js +0 -13
- package/src/product-shell/feedback/feedback-routes.js +0 -98
- package/src/product-shell/feedback/feedback-service.js +0 -252
- package/src/product-shell/index.js +0 -212
- package/src/product-shell/matching/matchmaking-service.js +0 -395
- package/src/product-shell/membership/membership-service.js +0 -284
- package/src/product-shell/onboarding/onboarding-routes.js +0 -37
- package/src/product-shell/onboarding/onboarding-service.js +0 -222
- package/src/product-shell/orchestration/world-conversation-orchestrator.js +0 -28
- package/src/product-shell/profile/profile-service.js +0 -142
- package/src/product-shell/profile/public-identity-routes.js +0 -160
- package/src/product-shell/profile/public-identity-service.js +0 -192
- package/src/product-shell/search/search-service.js +0 -393
- package/src/product-shell/social/chat-request-approval-policy.js +0 -332
- package/src/product-shell/social/chat-request-routes.js +0 -130
- package/src/product-shell/social/chat-request-service.js +0 -723
- package/src/product-shell/social/friend-routes.js +0 -82
- package/src/product-shell/social/friend-service.js +0 -557
- package/src/product-shell/social/social-routes.js +0 -21
- package/src/product-shell/social/social-service.js +0 -136
- package/src/product-shell/worlds/world-admin-service.js +0 -486
- package/src/product-shell/worlds/world-authorization.js +0 -136
- package/src/product-shell/worlds/world-broadcast-service.js +0 -296
- package/src/product-shell/worlds/world-routes.js +0 -403
- package/src/product-shell/worlds/world-service.js +0 -89
- package/src/product-shell/worlds/world-text.js +0 -75
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
|
|
5
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
|
|
7
|
-
const TEMPLATE_DEFINITIONS = Object.freeze([
|
|
8
|
-
{
|
|
9
|
-
templateId: 'agent-card.figma-v2',
|
|
10
|
-
templateVersion: 'v1',
|
|
11
|
-
width: 1077,
|
|
12
|
-
height: 1443,
|
|
13
|
-
templatePath: path.join(__dirname, 'templates', 'agent-card.figma-v2.svg'),
|
|
14
|
-
description: 'Embedded Figma-export card with centered name/handle over the illustrated shrimp poster.',
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
templateId: 'agent-card.slot-01',
|
|
18
|
-
templateVersion: 'v1',
|
|
19
|
-
width: 1792,
|
|
20
|
-
height: 2400,
|
|
21
|
-
templatePath: path.join(__dirname, 'templates', 'agent-card.slot-01.svg'),
|
|
22
|
-
description: 'Slot-driven poster card with wide centered name/handle fields and shrink-to-fit metadata.',
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
templateId: 'agent-card.slot-02',
|
|
26
|
-
templateVersion: 'v1',
|
|
27
|
-
width: 1792,
|
|
28
|
-
height: 2400,
|
|
29
|
-
templatePath: path.join(__dirname, 'templates', 'agent-card.slot-02.svg'),
|
|
30
|
-
description: 'Slot-driven right-side card with centered adaptive text fields and tighter width constraints.',
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
templateId: 'agent-card.slot-03',
|
|
34
|
-
templateVersion: 'v1',
|
|
35
|
-
width: 1792,
|
|
36
|
-
height: 2400,
|
|
37
|
-
templatePath: path.join(__dirname, 'templates', 'agent-card.slot-03.svg'),
|
|
38
|
-
description: 'Slot-driven wide centered card with larger adaptive text fields over an embedded background.',
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
templateId: 'agent-card.slot-04',
|
|
42
|
-
templateVersion: 'v1',
|
|
43
|
-
width: 1792,
|
|
44
|
-
height: 2400,
|
|
45
|
-
templatePath: path.join(__dirname, 'templates', 'agent-card.slot-04.svg'),
|
|
46
|
-
description: 'Slot-driven centered card with compact adaptive text fields over an embedded earthy background.',
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
templateId: 'agent-card.slot-05',
|
|
50
|
-
templateVersion: 'v1',
|
|
51
|
-
width: 1792,
|
|
52
|
-
height: 2400,
|
|
53
|
-
templatePath: path.join(__dirname, 'templates', 'agent-card.slot-05.svg'),
|
|
54
|
-
description: 'Figma-adjusted poster card with left-anchored adaptive name and handle slots.',
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
templateId: 'agent-card.slot-06',
|
|
58
|
-
templateVersion: 'v1',
|
|
59
|
-
width: 1792,
|
|
60
|
-
height: 2400,
|
|
61
|
-
templatePath: path.join(__dirname, 'templates', 'agent-card.slot-06.svg'),
|
|
62
|
-
description: 'Slot-driven right-column card with centered adaptive text fields over an embedded background.',
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
templateId: 'agent-card.slot-07',
|
|
66
|
-
templateVersion: 'v1',
|
|
67
|
-
width: 1792,
|
|
68
|
-
height: 2400,
|
|
69
|
-
templatePath: path.join(__dirname, 'templates', 'agent-card.slot-07.svg'),
|
|
70
|
-
description: 'Figma-adjusted warm poster card with large left-anchored adaptive name and handle slots.',
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
templateId: 'agent-card.slot-08',
|
|
74
|
-
templateVersion: 'v1',
|
|
75
|
-
width: 1792,
|
|
76
|
-
height: 2400,
|
|
77
|
-
templatePath: path.join(__dirname, 'templates', 'agent-card.slot-08.svg'),
|
|
78
|
-
description: 'Figma-adjusted rotated poster card with adaptive slanted name and handle slots.',
|
|
79
|
-
},
|
|
80
|
-
]);
|
|
81
|
-
|
|
82
|
-
function normalizeText(value, fallback = null) {
|
|
83
|
-
if (value == null) return fallback;
|
|
84
|
-
const normalized = String(value).trim();
|
|
85
|
-
return normalized || fallback;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function createAgentCardTemplateRegistry() {
|
|
89
|
-
const sourceCache = new Map();
|
|
90
|
-
const templatesById = new Map(TEMPLATE_DEFINITIONS.map((template) => [template.templateId, template]));
|
|
91
|
-
|
|
92
|
-
async function loadSource(template) {
|
|
93
|
-
const cached = sourceCache.get(template.templateId);
|
|
94
|
-
if (cached) return cached;
|
|
95
|
-
const source = await fs.readFile(template.templatePath, 'utf8');
|
|
96
|
-
sourceCache.set(template.templateId, source);
|
|
97
|
-
return source;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
listTemplates() {
|
|
102
|
-
return TEMPLATE_DEFINITIONS.map((template) => ({
|
|
103
|
-
templateId: template.templateId,
|
|
104
|
-
templateVersion: template.templateVersion,
|
|
105
|
-
width: template.width,
|
|
106
|
-
height: template.height,
|
|
107
|
-
description: template.description,
|
|
108
|
-
}));
|
|
109
|
-
},
|
|
110
|
-
async getTemplate(templateId = TEMPLATE_DEFINITIONS[0].templateId) {
|
|
111
|
-
const normalizedTemplateId = normalizeText(templateId, TEMPLATE_DEFINITIONS[0].templateId);
|
|
112
|
-
const template = templatesById.get(normalizedTemplateId) || null;
|
|
113
|
-
if (!template) {
|
|
114
|
-
const error = new Error(`unknown_agent_card_template:${normalizedTemplateId}`);
|
|
115
|
-
error.code = 'unknown_agent_card_template';
|
|
116
|
-
error.status = 404;
|
|
117
|
-
error.responseBody = {
|
|
118
|
-
error: error.code,
|
|
119
|
-
message: 'agent card template not found',
|
|
120
|
-
templateId: normalizedTemplateId,
|
|
121
|
-
};
|
|
122
|
-
throw error;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return {
|
|
126
|
-
...template,
|
|
127
|
-
source: await loadSource(template),
|
|
128
|
-
};
|
|
129
|
-
},
|
|
130
|
-
};
|
|
131
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
export const DEFAULT_WORLD_MANIFESTS = Object.freeze([
|
|
2
|
-
{
|
|
3
|
-
worldId: 'dating-demo-world',
|
|
4
|
-
displayName: 'Dating Demo World',
|
|
5
|
-
summary: 'A lightweight social world for proving world-scoped join, review, and chat request flow.',
|
|
6
|
-
description:
|
|
7
|
-
'A lightweight social world where agents compare fit briefly before deciding whether to continue beyond agent chat.',
|
|
8
|
-
category: 'social',
|
|
9
|
-
lifecycle: 'prototype',
|
|
10
|
-
tags: ['dating', 'matching', 'a2a'],
|
|
11
|
-
worldContextText:
|
|
12
|
-
'世界:Dating Demo World [dating-demo-world]\n简介:A lightweight social world for proving world-scoped join, review, and chat request flow.\n互动规则:Clarify fit quickly, stay respectful, and stop once next steps are clear.\n禁止事项:Do not pressure, harass, manipulate, or ask for unsafe personal details.',
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
worldId: 'skill-handoff-world',
|
|
16
|
-
displayName: 'Skill Handoff World',
|
|
17
|
-
summary: 'A small scoped skill-handoff world for proving service-fit review before direct contact.',
|
|
18
|
-
description:
|
|
19
|
-
'A service marketplace world where agents quickly screen delivery fit before moving to direct human contact.',
|
|
20
|
-
category: 'marketplace',
|
|
21
|
-
lifecycle: 'prototype',
|
|
22
|
-
tags: ['skills', 'services', 'matching'],
|
|
23
|
-
worldContextText:
|
|
24
|
-
'世界:Skill Handoff World [skill-handoff-world]\n简介:A service marketplace world where agents screen delivery fit before moving to direct human contact.\n互动规则:Clarify scope, constraints, and whether a handoff is justified.\n禁止事项:Do not misrepresent capabilities or hide delivery blockers.',
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
worldId: 'job-match-world',
|
|
28
|
-
displayName: 'Job Match World',
|
|
29
|
-
summary: 'A hiring-fit world optimized for profile completeness, matching, and concise A2A screening.',
|
|
30
|
-
description:
|
|
31
|
-
'A recruiting world for agent-assisted screening where both sides validate fit before escalating to direct human contact.',
|
|
32
|
-
category: 'recruiting',
|
|
33
|
-
lifecycle: 'prototype',
|
|
34
|
-
tags: ['jobs', 'recruiting', 'screening'],
|
|
35
|
-
worldContextText:
|
|
36
|
-
'世界:Job Match World [job-match-world]\n简介:A recruiting world for agent-assisted screening before direct human contact.\n互动规则:Focus on fit, constraints, and whether a next step is justified.\n禁止事项:Do not fabricate experience, compensation, or hiring authority.',
|
|
37
|
-
},
|
|
38
|
-
]);
|
|
@@ -1,393 +0,0 @@
|
|
|
1
|
-
const DEFAULT_CANDIDATE_FEED_TTL_MS = 6 * 60 * 60 * 1000;
|
|
2
|
-
const MAX_CANDIDATE_FEED_LIMIT = 25;
|
|
3
|
-
const DEFAULT_CANDIDATE_FEED_LIMIT = 10;
|
|
4
|
-
|
|
5
|
-
export const CANDIDATE_OBJECT_FIELDS = Object.freeze([
|
|
6
|
-
'candidateId',
|
|
7
|
-
'worldId',
|
|
8
|
-
'targetAgentId',
|
|
9
|
-
'sourceMembershipId',
|
|
10
|
-
'online',
|
|
11
|
-
'requestChat',
|
|
12
|
-
'profileSummary',
|
|
13
|
-
'compatibilitySignals',
|
|
14
|
-
'deliveryReason',
|
|
15
|
-
'worldFeedbackSummary',
|
|
16
|
-
'expiresAt',
|
|
17
|
-
]);
|
|
18
|
-
|
|
19
|
-
export const WORLD_FEEDBACK_SUMMARY_FIELDS = Object.freeze([
|
|
20
|
-
'likesReceived',
|
|
21
|
-
'dislikesReceived',
|
|
22
|
-
]);
|
|
23
|
-
|
|
24
|
-
export const DATING_DEMO_SCORING_FIELDS = Object.freeze([
|
|
25
|
-
'joinedAt',
|
|
26
|
-
'rank',
|
|
27
|
-
'score',
|
|
28
|
-
'scoreBreakdown',
|
|
29
|
-
'scoringInputs',
|
|
30
|
-
]);
|
|
31
|
-
|
|
32
|
-
export const PROFILE_SUMMARY_FIELDS = Object.freeze([
|
|
33
|
-
'displayName',
|
|
34
|
-
'headline',
|
|
35
|
-
'requiredFields',
|
|
36
|
-
'optionalFields',
|
|
37
|
-
]);
|
|
38
|
-
|
|
39
|
-
export const PROFILE_SUMMARY_FIELD_FIELDS = Object.freeze([
|
|
40
|
-
'fieldId',
|
|
41
|
-
'label',
|
|
42
|
-
'value',
|
|
43
|
-
]);
|
|
44
|
-
|
|
45
|
-
export const COMPATIBILITY_SIGNAL_FIELDS = Object.freeze([
|
|
46
|
-
'signalId',
|
|
47
|
-
'type',
|
|
48
|
-
'fieldIds',
|
|
49
|
-
'score',
|
|
50
|
-
'summary',
|
|
51
|
-
]);
|
|
52
|
-
|
|
53
|
-
export const DELIVERY_REASON_FIELDS = Object.freeze([
|
|
54
|
-
'code',
|
|
55
|
-
'matchedFieldIds',
|
|
56
|
-
'summary',
|
|
57
|
-
]);
|
|
58
|
-
|
|
59
|
-
export const LIVE_DELIVERY_EVENT_ENVELOPE_FIELDS = Object.freeze([
|
|
60
|
-
'event',
|
|
61
|
-
'data',
|
|
62
|
-
]);
|
|
63
|
-
|
|
64
|
-
export const LIVE_DELIVERY_EVENT_PAYLOAD_FIELDS = Object.freeze([
|
|
65
|
-
'deliveryId',
|
|
66
|
-
'worldId',
|
|
67
|
-
'agentId',
|
|
68
|
-
'viewerMembershipId',
|
|
69
|
-
'deliveredAt',
|
|
70
|
-
'candidate',
|
|
71
|
-
]);
|
|
72
|
-
|
|
73
|
-
function normalizeText(value, fallback = null) {
|
|
74
|
-
if (value == null) return fallback;
|
|
75
|
-
const normalized = String(value).trim();
|
|
76
|
-
return normalized || fallback;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function normalizeValue(value, field = {}) {
|
|
80
|
-
if (value == null) return null;
|
|
81
|
-
|
|
82
|
-
if (field.type === 'string[]') {
|
|
83
|
-
if (!Array.isArray(value)) return [];
|
|
84
|
-
return [...new Set(value.map((entry) => normalizeText(entry, null)).filter(Boolean))];
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const normalized = normalizeText(value, null);
|
|
88
|
-
return normalized;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function buildRequestChatPayload(world, candidateMembership) {
|
|
92
|
-
const targetAgentId = normalizeText(candidateMembership?.agentId, null);
|
|
93
|
-
if (!targetAgentId) return null;
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
worldId: world.worldId,
|
|
97
|
-
targetAgentId,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function normalizeComparableArray(value) {
|
|
102
|
-
if (!Array.isArray(value)) return [];
|
|
103
|
-
return [...new Set(value.map((entry) => normalizeText(entry, null)?.toLowerCase()).filter(Boolean))];
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function normalizeComparableText(value) {
|
|
107
|
-
return normalizeText(value, null)?.toLowerCase() || null;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function tokenizeText(value) {
|
|
111
|
-
return [...new Set(
|
|
112
|
-
String(value || '')
|
|
113
|
-
.toLowerCase()
|
|
114
|
-
.split(/[^a-z0-9\u4e00-\u9fff]+/i)
|
|
115
|
-
.map((entry) => entry.trim())
|
|
116
|
-
.filter((entry) => entry.length >= 2),
|
|
117
|
-
)];
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function projectSummaryField(field = {}, profile = {}) {
|
|
121
|
-
const normalizedValue = normalizeValue(profile[field.fieldId], field);
|
|
122
|
-
if (normalizedValue == null) return null;
|
|
123
|
-
if (Array.isArray(normalizedValue) && normalizedValue.length === 0) return null;
|
|
124
|
-
|
|
125
|
-
return {
|
|
126
|
-
fieldId: field.fieldId,
|
|
127
|
-
label: field.label,
|
|
128
|
-
value: normalizedValue,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function projectProfileSummary(world, profile = {}, agent = null) {
|
|
133
|
-
return {
|
|
134
|
-
displayName: normalizeText(agent?.displayName, null),
|
|
135
|
-
headline: normalizeText(profile.participantContextText, normalizeText(profile.headline, null)),
|
|
136
|
-
requiredFields: world.joinSchema.requiredFields
|
|
137
|
-
.map((field) => projectSummaryField(field, profile))
|
|
138
|
-
.filter(Boolean),
|
|
139
|
-
optionalFields: world.joinSchema.optionalFields
|
|
140
|
-
.map((field) => projectSummaryField(field, profile))
|
|
141
|
-
.filter(Boolean),
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function createCompatibilitySignal({ sourceMembershipId, type, fieldIds, score, summary }) {
|
|
146
|
-
return {
|
|
147
|
-
signalId: `${sourceMembershipId}:${type}:${fieldIds.join('+') || 'world'}`,
|
|
148
|
-
type,
|
|
149
|
-
fieldIds,
|
|
150
|
-
score,
|
|
151
|
-
summary,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function compareFieldValues(field, viewerValue, candidateValue, sourceMembershipId) {
|
|
156
|
-
if (field.fieldId === 'participantContextText') {
|
|
157
|
-
const viewerTokens = tokenizeText(viewerValue);
|
|
158
|
-
const candidateTokens = tokenizeText(candidateValue);
|
|
159
|
-
const sharedItems = viewerTokens.filter((item) => candidateTokens.includes(item));
|
|
160
|
-
if (sharedItems.length === 0) return null;
|
|
161
|
-
|
|
162
|
-
return createCompatibilitySignal({
|
|
163
|
-
sourceMembershipId,
|
|
164
|
-
type: 'field_overlap',
|
|
165
|
-
fieldIds: [field.fieldId],
|
|
166
|
-
score: Math.min(sharedItems.length * 0.08, 0.48),
|
|
167
|
-
summary: `Shared context: ${sharedItems.slice(0, 6).join(', ')}.`,
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (field.type === 'string[]') {
|
|
172
|
-
const viewerItems = normalizeComparableArray(viewerValue);
|
|
173
|
-
const candidateItems = normalizeComparableArray(candidateValue);
|
|
174
|
-
const sharedItems = viewerItems.filter((item) => candidateItems.includes(item));
|
|
175
|
-
if (sharedItems.length === 0) return null;
|
|
176
|
-
|
|
177
|
-
return createCompatibilitySignal({
|
|
178
|
-
sourceMembershipId,
|
|
179
|
-
type: 'field_overlap',
|
|
180
|
-
fieldIds: [field.fieldId],
|
|
181
|
-
score: field.required ? 0.25 : 0.15,
|
|
182
|
-
summary: `Shared ${field.label}: ${sharedItems.slice(0, 3).join(', ')}.`,
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const normalizedViewerValue = normalizeComparableText(viewerValue);
|
|
187
|
-
const normalizedCandidateValue = normalizeComparableText(candidateValue);
|
|
188
|
-
if (!normalizedViewerValue || !normalizedCandidateValue) return null;
|
|
189
|
-
if (normalizedViewerValue !== normalizedCandidateValue) return null;
|
|
190
|
-
|
|
191
|
-
return createCompatibilitySignal({
|
|
192
|
-
sourceMembershipId,
|
|
193
|
-
type: 'field_alignment',
|
|
194
|
-
fieldIds: [field.fieldId],
|
|
195
|
-
score: field.required ? 0.35 : 0.1,
|
|
196
|
-
summary: `Matching ${field.label}: ${normalizeText(candidateValue, '')}.`.trim(),
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function buildCompatibilitySignals(world, viewerProfile = {}, candidateProfile = {}, sourceMembershipId) {
|
|
201
|
-
const signals = [...world.joinSchema.requiredFields, ...world.joinSchema.optionalFields]
|
|
202
|
-
.map((field) => compareFieldValues(field, viewerProfile[field.fieldId], candidateProfile[field.fieldId], sourceMembershipId))
|
|
203
|
-
.filter(Boolean);
|
|
204
|
-
|
|
205
|
-
if (signals.length > 0) {
|
|
206
|
-
return signals.sort((left, right) => right.score - left.score || left.fieldIds[0].localeCompare(right.fieldIds[0]));
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return [
|
|
210
|
-
createCompatibilitySignal({
|
|
211
|
-
sourceMembershipId,
|
|
212
|
-
type: 'world_ready',
|
|
213
|
-
fieldIds: [],
|
|
214
|
-
score: 0.05,
|
|
215
|
-
summary: 'Candidate is online with an active world membership and is ready for agent review before live session handoff.',
|
|
216
|
-
}),
|
|
217
|
-
];
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function buildDeliveryReason(signals = []) {
|
|
221
|
-
const matchedFieldIds = [...new Set(signals.flatMap((signal) => signal.fieldIds).filter(Boolean))];
|
|
222
|
-
|
|
223
|
-
if (matchedFieldIds.length === 0) {
|
|
224
|
-
return {
|
|
225
|
-
code: 'world_membership_ready',
|
|
226
|
-
matchedFieldIds: [],
|
|
227
|
-
summary: 'Delivered for manual review because the candidate is online and active in this world, even though no direct profile overlap signal was detected yet.',
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return {
|
|
232
|
-
code: 'profile_overlap_ready_for_review',
|
|
233
|
-
matchedFieldIds,
|
|
234
|
-
summary:
|
|
235
|
-
`Delivered for agent review because the world found overlap on ${matchedFieldIds.join(', ')} ` +
|
|
236
|
-
'without starting a live session yet.',
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function resolveCandidateFeedTtlMs(world) {
|
|
241
|
-
if (world.matching.cadence === 'on_demand') return 60 * 60 * 1000;
|
|
242
|
-
return DEFAULT_CANDIDATE_FEED_TTL_MS;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function calculateRankingScore(signals = []) {
|
|
246
|
-
return signals.reduce((total, signal) => total + Number(signal.score || 0), 0);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function resolveCandidateFields(world) {
|
|
250
|
-
return world.worldId === 'dating-demo-world'
|
|
251
|
-
? [...CANDIDATE_OBJECT_FIELDS, ...DATING_DEMO_SCORING_FIELDS]
|
|
252
|
-
: [...CANDIDATE_OBJECT_FIELDS];
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function projectLiveDeliveryEvent(world, candidateFields) {
|
|
256
|
-
return {
|
|
257
|
-
schemaId: `${world.worldId}.candidate-delivery-event.v1`,
|
|
258
|
-
eventName: 'world.candidate.delivered',
|
|
259
|
-
envelopeFields: LIVE_DELIVERY_EVENT_ENVELOPE_FIELDS,
|
|
260
|
-
payloadFields: LIVE_DELIVERY_EVENT_PAYLOAD_FIELDS,
|
|
261
|
-
candidateFieldPath: 'data.candidate',
|
|
262
|
-
candidateFields,
|
|
263
|
-
candidateModelId: `${world.worldId}.candidate-feed.v1`,
|
|
264
|
-
viewerRequirement: 'active_membership',
|
|
265
|
-
summary:
|
|
266
|
-
'Future live delivery will reuse the current candidate object inside a push event after delivery workers/subscriptions are added.',
|
|
267
|
-
status: 'planned_live_delivery',
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
export function normalizeCandidateFeedLimit(limit) {
|
|
272
|
-
const normalized = Number(limit);
|
|
273
|
-
if (!Number.isFinite(normalized) || normalized <= 0) return DEFAULT_CANDIDATE_FEED_LIMIT;
|
|
274
|
-
return Math.min(MAX_CANDIDATE_FEED_LIMIT, Math.floor(normalized));
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
export function projectCandidateFeedModel(world) {
|
|
278
|
-
const candidateFields = resolveCandidateFields(world);
|
|
279
|
-
|
|
280
|
-
return {
|
|
281
|
-
modelId: `${world.worldId}.candidate-feed.v1`,
|
|
282
|
-
worldId: world.worldId,
|
|
283
|
-
deliveryMode: 'agent_review_before_live_session',
|
|
284
|
-
viewerRequirement: 'active_membership',
|
|
285
|
-
previewRoute: `/v1/worlds/${world.worldId}/candidates?agentId=:agentId`,
|
|
286
|
-
candidateFields,
|
|
287
|
-
profileSummaryFields: PROFILE_SUMMARY_FIELDS,
|
|
288
|
-
profileSummaryFieldShape: PROFILE_SUMMARY_FIELD_FIELDS,
|
|
289
|
-
compatibilitySignalFields: COMPATIBILITY_SIGNAL_FIELDS,
|
|
290
|
-
deliveryReasonFields: DELIVERY_REASON_FIELDS,
|
|
291
|
-
worldFeedbackSummaryFields: WORLD_FEEDBACK_SUMMARY_FIELDS,
|
|
292
|
-
requestChatAction: {
|
|
293
|
-
action: 'request_chat',
|
|
294
|
-
requiredFields: ['worldId', 'targetAgentId'],
|
|
295
|
-
summary:
|
|
296
|
-
'After reviewing a candidate, create a world-scoped chat request with worldId and targetAgentId.',
|
|
297
|
-
},
|
|
298
|
-
liveDeliveryEvent: projectLiveDeliveryEvent(world, candidateFields),
|
|
299
|
-
summary:
|
|
300
|
-
'Active online members can review candidate opportunities first, then call request_chat with the selected targetAgentId when they want to start a world-scoped conversation request.',
|
|
301
|
-
status: 'phase1_candidate_feed',
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function projectCandidateOpportunity({
|
|
306
|
-
world,
|
|
307
|
-
viewerProfile,
|
|
308
|
-
candidateMembership,
|
|
309
|
-
candidateAgent,
|
|
310
|
-
candidatePresence,
|
|
311
|
-
expiresAt,
|
|
312
|
-
}) {
|
|
313
|
-
const profileSnapshot = candidateMembership.profileSnapshot || {};
|
|
314
|
-
const compatibilitySignals = buildCompatibilitySignals(
|
|
315
|
-
world,
|
|
316
|
-
viewerProfile,
|
|
317
|
-
profileSnapshot,
|
|
318
|
-
candidateMembership.membershipId,
|
|
319
|
-
);
|
|
320
|
-
const requestChat = buildRequestChatPayload(world, candidateMembership);
|
|
321
|
-
|
|
322
|
-
return {
|
|
323
|
-
candidateId: `cand_${candidateMembership.membershipId}`,
|
|
324
|
-
worldId: world.worldId,
|
|
325
|
-
sourceMembershipId: candidateMembership.membershipId,
|
|
326
|
-
targetAgentId: requestChat?.targetAgentId || null,
|
|
327
|
-
online: candidatePresence?.online === true,
|
|
328
|
-
requestChat,
|
|
329
|
-
profileSummary: projectProfileSummary(world, profileSnapshot, candidateAgent),
|
|
330
|
-
compatibilitySignals,
|
|
331
|
-
deliveryReason: buildDeliveryReason(compatibilitySignals),
|
|
332
|
-
expiresAt,
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
export function buildCandidateFeed({
|
|
337
|
-
world,
|
|
338
|
-
viewerMembership,
|
|
339
|
-
viewerAgent = null,
|
|
340
|
-
candidateMemberships = [],
|
|
341
|
-
getAgent = () => null,
|
|
342
|
-
getPresence = () => ({ online: true }),
|
|
343
|
-
nowMs = Date.now(),
|
|
344
|
-
limit = DEFAULT_CANDIDATE_FEED_LIMIT,
|
|
345
|
-
}) {
|
|
346
|
-
const normalizedNowMs = Number.isFinite(Number(nowMs)) ? Number(nowMs) : Date.now();
|
|
347
|
-
const generatedAt = new Date(normalizedNowMs).toISOString();
|
|
348
|
-
const expiresAt = new Date(normalizedNowMs + resolveCandidateFeedTtlMs(world)).toISOString();
|
|
349
|
-
const normalizedLimit = normalizeCandidateFeedLimit(limit);
|
|
350
|
-
const viewerProfile = viewerMembership?.profileSnapshot || viewerAgent?.profile || {};
|
|
351
|
-
|
|
352
|
-
const candidates = candidateMemberships
|
|
353
|
-
.filter((membership) => membership?.status === 'active' && membership.membershipId !== viewerMembership.membershipId)
|
|
354
|
-
.map((membership) => ({
|
|
355
|
-
membership,
|
|
356
|
-
candidatePresence: getPresence(membership.agentId),
|
|
357
|
-
}))
|
|
358
|
-
.filter(({ candidatePresence }) => candidatePresence?.online === true)
|
|
359
|
-
.map(({ membership, candidatePresence }) => {
|
|
360
|
-
const opportunity = projectCandidateOpportunity({
|
|
361
|
-
world,
|
|
362
|
-
viewerProfile,
|
|
363
|
-
candidateMembership: membership,
|
|
364
|
-
candidateAgent: getAgent(membership.agentId),
|
|
365
|
-
candidatePresence,
|
|
366
|
-
expiresAt,
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
return {
|
|
370
|
-
opportunity,
|
|
371
|
-
rankingScore: calculateRankingScore(opportunity.compatibilitySignals),
|
|
372
|
-
};
|
|
373
|
-
})
|
|
374
|
-
.sort(
|
|
375
|
-
(left, right) =>
|
|
376
|
-
right.rankingScore - left.rankingScore
|
|
377
|
-
|| left.opportunity.candidateId.localeCompare(right.opportunity.candidateId),
|
|
378
|
-
)
|
|
379
|
-
.slice(0, normalizedLimit)
|
|
380
|
-
.map((entry) => entry.opportunity);
|
|
381
|
-
|
|
382
|
-
return {
|
|
383
|
-
worldId: world.worldId,
|
|
384
|
-
viewerMembershipId: viewerMembership.membershipId,
|
|
385
|
-
generatedAt,
|
|
386
|
-
expiresAt,
|
|
387
|
-
deliveryMode: 'agent_review_before_live_session',
|
|
388
|
-
nextAction: 'review_candidates_then_request_chat',
|
|
389
|
-
candidateModel: projectCandidateFeedModel(world),
|
|
390
|
-
candidates,
|
|
391
|
-
status: 'feed_ready',
|
|
392
|
-
};
|
|
393
|
-
}
|