@xfxstudio/claworld 0.1.0
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 +60 -0
- package/bin/claworld.mjs +9 -0
- package/index.js +51 -0
- package/openclaw.plugin.json +470 -0
- package/package.json +76 -0
- package/setup-entry.js +6 -0
- package/src/lib/accepted-chat-kickoff.js +192 -0
- package/src/lib/agent-address.js +46 -0
- package/src/lib/agent-profile.js +69 -0
- package/src/lib/http-auth.js +151 -0
- package/src/lib/policy.js +118 -0
- package/src/lib/runtime-errors.js +149 -0
- package/src/lib/runtime-guidance.js +458 -0
- package/src/openclaw/index.js +53 -0
- package/src/openclaw/installer/cli.js +349 -0
- package/src/openclaw/installer/constants.js +6 -0
- package/src/openclaw/installer/core.js +1548 -0
- package/src/openclaw/installer/doctor.js +690 -0
- package/src/openclaw/installer/workspace-contract.js +403 -0
- package/src/openclaw/plugin/account-identity.js +66 -0
- package/src/openclaw/plugin/claworld-channel-plugin.js +3118 -0
- package/src/openclaw/plugin/config-schema.js +464 -0
- package/src/openclaw/plugin/lifecycle.js +114 -0
- package/src/openclaw/plugin/managed-config.js +648 -0
- package/src/openclaw/plugin/onboarding.js +291 -0
- package/src/openclaw/plugin/register.js +961 -0
- package/src/openclaw/plugin/relay-client.js +783 -0
- package/src/openclaw/plugin/runtime.js +12 -0
- package/src/openclaw/protocol/relay-event-protocol.js +31 -0
- package/src/openclaw/runtime/canonical-result-builder.js +116 -0
- package/src/openclaw/runtime/demo-session-bootstrap.js +37 -0
- package/src/openclaw/runtime/feedback-helper.js +145 -0
- package/src/openclaw/runtime/inbound-session-router.js +36 -0
- package/src/openclaw/runtime/outbound-session-bridge.js +17 -0
- package/src/openclaw/runtime/product-shell-helper.js +1712 -0
- package/src/openclaw/runtime/runtime-path.js +19 -0
- package/src/openclaw/runtime/system-message-orchestrator.js +1 -0
- package/src/openclaw/runtime/tool-contracts.js +714 -0
- package/src/openclaw/runtime/tool-inventory.js +92 -0
- package/src/openclaw/runtime/world-moderation-helper.js +415 -0
- package/src/openclaw/runtime/world-session-startup.js +1 -0
- package/src/product-shell/catalog/default-world-catalog.js +296 -0
- package/src/product-shell/contracts/candidate-feed.js +330 -0
- package/src/product-shell/contracts/chat-request-approval-policy.js +98 -0
- package/src/product-shell/contracts/world-manifest.js +435 -0
- package/src/product-shell/contracts/world-orchestration.js +1024 -0
- package/src/product-shell/feedback/feedback-contract.js +13 -0
- package/src/product-shell/feedback/feedback-routes.js +98 -0
- package/src/product-shell/feedback/feedback-service.js +254 -0
- package/src/product-shell/index.js +163 -0
- package/src/product-shell/matching/matchmaking-service.js +340 -0
- package/src/product-shell/membership/membership-service.js +277 -0
- package/src/product-shell/onboarding/onboarding-routes.js +37 -0
- package/src/product-shell/onboarding/onboarding-service.js +230 -0
- package/src/product-shell/orchestration/session-orchestrator.js +38 -0
- package/src/product-shell/results/result-service.js +15 -0
- package/src/product-shell/search/search-service.js +359 -0
- package/src/product-shell/social/chat-request-approval-policy.js +332 -0
- package/src/product-shell/social/chat-request-routes.js +108 -0
- package/src/product-shell/social/chat-request-service.js +632 -0
- package/src/product-shell/social/friend-routes.js +82 -0
- package/src/product-shell/social/friend-service.js +560 -0
- package/src/product-shell/social/social-routes.js +21 -0
- package/src/product-shell/social/social-service.js +140 -0
- package/src/product-shell/worlds/world-admin-service.js +705 -0
- package/src/product-shell/worlds/world-authorization.js +135 -0
- package/src/product-shell/worlds/world-broadcast-service.js +299 -0
- package/src/product-shell/worlds/world-routes.js +410 -0
- package/src/product-shell/worlds/world-service.js +89 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { buildCandidateFeed, normalizeCandidateFeedLimit, projectCandidateFeedModel } from '../contracts/candidate-feed.js';
|
|
2
|
+
import { WORLD_ACTIONS } from '../worlds/world-authorization.js';
|
|
3
|
+
|
|
4
|
+
const DATING_DEMO_SCORING_SIGNALS = Object.freeze([
|
|
5
|
+
{
|
|
6
|
+
signalId: 'intent_exact_match',
|
|
7
|
+
label: 'Intent Exact Match',
|
|
8
|
+
weight: 50,
|
|
9
|
+
sourceFieldIds: ['intent'],
|
|
10
|
+
summary: 'Add 50 points when both active memberships declare the same normalized intent.',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
signalId: 'same_location',
|
|
14
|
+
label: 'Same Location',
|
|
15
|
+
weight: 30,
|
|
16
|
+
sourceFieldIds: ['location'],
|
|
17
|
+
summary: 'Add 30 points when both active memberships declare the same normalized location.',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
signalId: 'shared_interests',
|
|
21
|
+
label: 'Shared Interests',
|
|
22
|
+
weight: 20,
|
|
23
|
+
sourceFieldIds: ['interests'],
|
|
24
|
+
summary: 'Add 10 points per shared normalized interest, capped at 20 points.',
|
|
25
|
+
},
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function createConfigurationError() {
|
|
29
|
+
const error = new Error('membership_store_unavailable');
|
|
30
|
+
error.code = 'membership_store_unavailable';
|
|
31
|
+
error.status = 500;
|
|
32
|
+
return error;
|
|
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
|
+
return error;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function createInvalidCandidateFeedRequestError(fieldId, message = `${fieldId} is required`) {
|
|
43
|
+
const error = new Error(`invalid_candidate_feed_request:${fieldId}`);
|
|
44
|
+
error.code = 'invalid_candidate_feed_request';
|
|
45
|
+
error.status = 400;
|
|
46
|
+
error.responseBody = {
|
|
47
|
+
error: error.code,
|
|
48
|
+
message: 'candidate feed request is missing required fields',
|
|
49
|
+
fieldErrors: [
|
|
50
|
+
{
|
|
51
|
+
fieldId,
|
|
52
|
+
message,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
return error;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createCandidateFeedMembershipNotActiveError(worldId, agentId) {
|
|
60
|
+
const error = new Error(`candidate_feed_membership_not_active:${worldId}:${agentId}`);
|
|
61
|
+
error.code = 'candidate_feed_membership_not_active';
|
|
62
|
+
error.status = 409;
|
|
63
|
+
error.responseBody = {
|
|
64
|
+
error: error.code,
|
|
65
|
+
message: 'agent must have an active world membership before requesting candidate feed',
|
|
66
|
+
worldId,
|
|
67
|
+
agentId,
|
|
68
|
+
requiredMembershipStatus: 'active',
|
|
69
|
+
};
|
|
70
|
+
return error;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeAgentId(agentId) {
|
|
74
|
+
const normalized = String(agentId || '').trim();
|
|
75
|
+
return normalized || null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeText(value) {
|
|
79
|
+
return String(value || '').trim().toLowerCase();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeStringList(value) {
|
|
83
|
+
if (!Array.isArray(value)) return [];
|
|
84
|
+
return [...new Set(
|
|
85
|
+
value
|
|
86
|
+
.map((item) => normalizeText(item))
|
|
87
|
+
.filter(Boolean),
|
|
88
|
+
)].sort((left, right) => left.localeCompare(right));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function projectScoringSignals(worldId) {
|
|
92
|
+
if (worldId !== 'dating-demo-world') return [];
|
|
93
|
+
return DATING_DEMO_SCORING_SIGNALS.map((signal) => ({ ...signal }));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function buildStrategy(world) {
|
|
97
|
+
return {
|
|
98
|
+
worldId: world.worldId,
|
|
99
|
+
mode: world.matching.mode,
|
|
100
|
+
cadence: world.matching.cadence,
|
|
101
|
+
strategySummary: world.matching.strategySummary,
|
|
102
|
+
candidateSources: world.matching.candidateSources,
|
|
103
|
+
inputFields: world.joinSchema.requiredFields.map((field) => field.fieldId),
|
|
104
|
+
candidateFeedModel: projectCandidateFeedModel(world),
|
|
105
|
+
scoringSignals: projectScoringSignals(world.worldId),
|
|
106
|
+
status: world.worldId === 'dating-demo-world' ? 'candidate_scoring_ready' : 'scaffold_ready',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function buildInterestBreakdown(requesterInterests, candidateInterests) {
|
|
111
|
+
const sharedValues = requesterInterests.filter((interest) => candidateInterests.includes(interest));
|
|
112
|
+
const contribution = Math.min(sharedValues.length * 10, 20);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
signalId: 'shared_interests',
|
|
116
|
+
label: 'Shared Interests',
|
|
117
|
+
weight: 20,
|
|
118
|
+
sourceFieldIds: ['interests'],
|
|
119
|
+
matched: sharedValues.length > 0,
|
|
120
|
+
requesterValue: requesterInterests,
|
|
121
|
+
candidateValue: candidateInterests,
|
|
122
|
+
sharedValues,
|
|
123
|
+
overlapCount: sharedValues.length,
|
|
124
|
+
contribution,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildDatingDemoScoreDetails(viewerMembership, candidateMembership) {
|
|
129
|
+
const viewerIntent = normalizeText(viewerMembership.profileSnapshot?.intent);
|
|
130
|
+
const candidateIntent = normalizeText(candidateMembership.profileSnapshot?.intent);
|
|
131
|
+
const viewerLocation = normalizeText(viewerMembership.profileSnapshot?.location);
|
|
132
|
+
const candidateLocation = normalizeText(candidateMembership.profileSnapshot?.location);
|
|
133
|
+
const viewerInterests = normalizeStringList(viewerMembership.profileSnapshot?.interests);
|
|
134
|
+
const candidateInterests = normalizeStringList(candidateMembership.profileSnapshot?.interests);
|
|
135
|
+
|
|
136
|
+
const scoreBreakdown = [
|
|
137
|
+
{
|
|
138
|
+
signalId: 'intent_exact_match',
|
|
139
|
+
label: 'Intent Exact Match',
|
|
140
|
+
weight: 50,
|
|
141
|
+
sourceFieldIds: ['intent'],
|
|
142
|
+
matched: viewerIntent !== '' && viewerIntent === candidateIntent,
|
|
143
|
+
requesterValue: viewerIntent,
|
|
144
|
+
candidateValue: candidateIntent,
|
|
145
|
+
contribution: viewerIntent !== '' && viewerIntent === candidateIntent ? 50 : 0,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
signalId: 'same_location',
|
|
149
|
+
label: 'Same Location',
|
|
150
|
+
weight: 30,
|
|
151
|
+
sourceFieldIds: ['location'],
|
|
152
|
+
matched: viewerLocation !== '' && viewerLocation === candidateLocation,
|
|
153
|
+
requesterValue: viewerLocation,
|
|
154
|
+
candidateValue: candidateLocation,
|
|
155
|
+
contribution: viewerLocation !== '' && viewerLocation === candidateLocation ? 30 : 0,
|
|
156
|
+
},
|
|
157
|
+
buildInterestBreakdown(viewerInterests, candidateInterests),
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
joinedAt: candidateMembership.joinedAt,
|
|
162
|
+
score: scoreBreakdown.reduce((sum, signal) => sum + signal.contribution, 0),
|
|
163
|
+
scoreBreakdown,
|
|
164
|
+
scoringInputs: {
|
|
165
|
+
requester: {
|
|
166
|
+
intent: viewerIntent,
|
|
167
|
+
location: viewerLocation,
|
|
168
|
+
interests: viewerInterests,
|
|
169
|
+
},
|
|
170
|
+
candidate: {
|
|
171
|
+
intent: candidateIntent,
|
|
172
|
+
location: candidateLocation,
|
|
173
|
+
interests: candidateInterests,
|
|
174
|
+
},
|
|
175
|
+
overlap: {
|
|
176
|
+
sharedInterests: scoreBreakdown[2].sharedValues,
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function compareRankedCandidates(left, right) {
|
|
183
|
+
if (right.score !== left.score) return right.score - left.score;
|
|
184
|
+
|
|
185
|
+
const rightSharedInterestCount = right.scoringInputs.overlap.sharedInterests.length;
|
|
186
|
+
const leftSharedInterestCount = left.scoringInputs.overlap.sharedInterests.length;
|
|
187
|
+
if (rightSharedInterestCount !== leftSharedInterestCount) {
|
|
188
|
+
return rightSharedInterestCount - leftSharedInterestCount;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (left.joinedAt !== right.joinedAt) {
|
|
192
|
+
return String(left.joinedAt).localeCompare(String(right.joinedAt));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return String(left.sourceMembershipId).localeCompare(String(right.sourceMembershipId));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function buildViewerContext({ world, membershipStore, normalizedAgentId, worldAuthorizationService }) {
|
|
199
|
+
const viewerAgent = membershipStore.getAgent(normalizedAgentId);
|
|
200
|
+
if (!viewerAgent) {
|
|
201
|
+
throw createAgentNotFoundError(normalizedAgentId);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const authorization = worldAuthorizationService.evaluateWorldAction({
|
|
205
|
+
worldId: world.worldId,
|
|
206
|
+
actorAgentId: normalizedAgentId,
|
|
207
|
+
action: WORLD_ACTIONS.VIEW_CANDIDATE_FEED,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (!authorization.allowed || authorization.membership?.status !== 'active') {
|
|
211
|
+
throw createCandidateFeedMembershipNotActiveError(world.worldId, normalizedAgentId);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
viewerAgent,
|
|
216
|
+
viewerMembership: authorization.membership,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function buildBaseFeed({ world, membershipStore, normalizedAgentId, limit, worldAuthorizationService }) {
|
|
221
|
+
const { viewerAgent, viewerMembership } = buildViewerContext({
|
|
222
|
+
world,
|
|
223
|
+
membershipStore,
|
|
224
|
+
normalizedAgentId,
|
|
225
|
+
worldAuthorizationService,
|
|
226
|
+
});
|
|
227
|
+
const activeMemberships = membershipStore.listMemberships({
|
|
228
|
+
worldId: world.worldId,
|
|
229
|
+
status: 'active',
|
|
230
|
+
});
|
|
231
|
+
const normalizedLimit = normalizeCandidateFeedLimit(limit);
|
|
232
|
+
const nowMs = typeof membershipStore.nowMs === 'function' ? membershipStore.nowMs() : Date.now();
|
|
233
|
+
const baseFeed = buildCandidateFeed({
|
|
234
|
+
world,
|
|
235
|
+
viewerMembership,
|
|
236
|
+
viewerAgent,
|
|
237
|
+
candidateMemberships: activeMemberships,
|
|
238
|
+
getAgent: (candidateAgentId) => membershipStore.getAgent(candidateAgentId),
|
|
239
|
+
nowMs,
|
|
240
|
+
limit: activeMemberships.length,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
viewerMembership,
|
|
245
|
+
activeMemberships,
|
|
246
|
+
normalizedLimit,
|
|
247
|
+
baseFeed,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function buildDatingDemoFeed({ world, membershipStore, normalizedAgentId, limit, worldAuthorizationService }) {
|
|
252
|
+
const { viewerMembership, activeMemberships, normalizedLimit, baseFeed } = buildBaseFeed({
|
|
253
|
+
world,
|
|
254
|
+
membershipStore,
|
|
255
|
+
normalizedAgentId,
|
|
256
|
+
limit,
|
|
257
|
+
worldAuthorizationService,
|
|
258
|
+
});
|
|
259
|
+
const membershipById = new Map(activeMemberships.map((membership) => [membership.membershipId, membership]));
|
|
260
|
+
const rankedCandidates = baseFeed.candidates
|
|
261
|
+
.map((candidate) => {
|
|
262
|
+
const candidateMembership = membershipById.get(candidate.sourceMembershipId);
|
|
263
|
+
return {
|
|
264
|
+
...candidate,
|
|
265
|
+
...buildDatingDemoScoreDetails(viewerMembership, candidateMembership),
|
|
266
|
+
};
|
|
267
|
+
})
|
|
268
|
+
.sort(compareRankedCandidates);
|
|
269
|
+
|
|
270
|
+
const candidates = rankedCandidates
|
|
271
|
+
.slice(0, normalizedLimit)
|
|
272
|
+
.map((candidate, index) => ({
|
|
273
|
+
...candidate,
|
|
274
|
+
rank: index + 1,
|
|
275
|
+
}));
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
...baseFeed,
|
|
279
|
+
agentId: normalizedAgentId,
|
|
280
|
+
limit: normalizedLimit,
|
|
281
|
+
candidateSource: 'active_memberships',
|
|
282
|
+
strategy: buildStrategy(world),
|
|
283
|
+
totalCandidates: rankedCandidates.length,
|
|
284
|
+
candidates,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function createMatchmakingService({ worldService, worldAuthorizationService, store = null } = {}) {
|
|
289
|
+
function assertStore() {
|
|
290
|
+
if (!store) throw createConfigurationError();
|
|
291
|
+
return store;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
describeStrategy(worldId) {
|
|
296
|
+
const world = worldService.requireWorld(worldId);
|
|
297
|
+
return buildStrategy(world);
|
|
298
|
+
},
|
|
299
|
+
listCandidateFeed({ worldId, agentId, limit } = {}) {
|
|
300
|
+
const world = worldService.requireWorld(worldId);
|
|
301
|
+
const membershipStore = assertStore();
|
|
302
|
+
const normalizedAgentId = normalizeAgentId(agentId);
|
|
303
|
+
|
|
304
|
+
if (!normalizedAgentId) {
|
|
305
|
+
throw createInvalidCandidateFeedRequestError('agentId');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (world.worldId === 'dating-demo-world') {
|
|
309
|
+
return buildDatingDemoFeed({
|
|
310
|
+
world,
|
|
311
|
+
membershipStore,
|
|
312
|
+
normalizedAgentId,
|
|
313
|
+
limit,
|
|
314
|
+
worldAuthorizationService,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const { normalizedLimit, baseFeed } = buildBaseFeed({
|
|
319
|
+
world,
|
|
320
|
+
membershipStore,
|
|
321
|
+
normalizedAgentId,
|
|
322
|
+
limit,
|
|
323
|
+
worldAuthorizationService,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
...baseFeed,
|
|
328
|
+
agentId: normalizedAgentId,
|
|
329
|
+
limit: normalizedLimit,
|
|
330
|
+
candidateSource: 'active_memberships',
|
|
331
|
+
strategy: buildStrategy(world),
|
|
332
|
+
totalCandidates: baseFeed.candidates.length,
|
|
333
|
+
candidates: baseFeed.candidates.slice(0, normalizedLimit),
|
|
334
|
+
};
|
|
335
|
+
},
|
|
336
|
+
listCandidates(options = {}) {
|
|
337
|
+
return this.listCandidateFeed(options);
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildWorldJoinOutcomeOrchestration,
|
|
3
|
+
buildWorldProfileCollectionFlow,
|
|
4
|
+
} from '../contracts/world-orchestration.js';
|
|
5
|
+
|
|
6
|
+
function isEmptyValue(value) {
|
|
7
|
+
if (value == null) return true;
|
|
8
|
+
if (typeof value === 'string') return value.trim() === '';
|
|
9
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function projectMissingField(field = {}) {
|
|
14
|
+
return {
|
|
15
|
+
fieldId: field.fieldId,
|
|
16
|
+
label: field.label,
|
|
17
|
+
description: field.description,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createConfigurationError() {
|
|
22
|
+
const error = new Error('membership_store_unavailable');
|
|
23
|
+
error.code = 'membership_store_unavailable';
|
|
24
|
+
error.status = 500;
|
|
25
|
+
return error;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createAgentNotFoundError(agentId) {
|
|
29
|
+
const error = new Error(`agent_not_found:${agentId}`);
|
|
30
|
+
error.code = 'agent_not_found';
|
|
31
|
+
error.status = 404;
|
|
32
|
+
return error;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createInvalidJoinRequestError(fieldId, message = `${fieldId} is required`) {
|
|
36
|
+
const error = new Error(`invalid_join_request:${fieldId}`);
|
|
37
|
+
error.code = 'invalid_join_request';
|
|
38
|
+
error.status = 400;
|
|
39
|
+
error.responseBody = {
|
|
40
|
+
error: error.code,
|
|
41
|
+
message: 'join request is missing required fields',
|
|
42
|
+
fieldErrors: [
|
|
43
|
+
{
|
|
44
|
+
fieldId,
|
|
45
|
+
message,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
return error;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function createMembershipNotEligibleError(joinCheck) {
|
|
53
|
+
const error = new Error('membership_not_eligible');
|
|
54
|
+
error.code = 'membership_not_eligible';
|
|
55
|
+
error.status = 422;
|
|
56
|
+
error.responseBody = {
|
|
57
|
+
error: error.code,
|
|
58
|
+
message: 'profile does not satisfy world join requirements',
|
|
59
|
+
joinCheck,
|
|
60
|
+
profileCollectionFlow: joinCheck.profileCollectionFlow || null,
|
|
61
|
+
};
|
|
62
|
+
return error;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function createJoinNotEligibleError(joinCheck) {
|
|
66
|
+
const error = new Error('world_join_not_eligible');
|
|
67
|
+
error.code = 'world_join_not_eligible';
|
|
68
|
+
error.status = 422;
|
|
69
|
+
error.responseBody = {
|
|
70
|
+
error: error.code,
|
|
71
|
+
message: 'profile does not satisfy world join requirements',
|
|
72
|
+
membershipStatus: 'inactive',
|
|
73
|
+
worldId: joinCheck.worldId,
|
|
74
|
+
missingFields: joinCheck.missingFields,
|
|
75
|
+
nextMissingField: joinCheck.nextMissingField,
|
|
76
|
+
missingFieldGuidance: joinCheck.missingFieldGuidance,
|
|
77
|
+
normalizedProfile: joinCheck.normalizedProfile,
|
|
78
|
+
nextAction: joinCheck.nextAction,
|
|
79
|
+
joinCheck,
|
|
80
|
+
profileCollectionFlow: joinCheck.profileCollectionFlow || null,
|
|
81
|
+
};
|
|
82
|
+
return error;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizeAgentId(agentId) {
|
|
86
|
+
const normalized = String(agentId || '').trim();
|
|
87
|
+
return normalized || null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizeProfileSnapshot(...candidates) {
|
|
91
|
+
for (const candidate of candidates) {
|
|
92
|
+
if (candidate && typeof candidate === 'object' && !Array.isArray(candidate)) {
|
|
93
|
+
return candidate;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildNextStageSummary(world) {
|
|
100
|
+
const matchingSummary = world.matching.strategySummary
|
|
101
|
+
|| `The world uses ${world.matching.mode} matching before session handoff.`;
|
|
102
|
+
const sessionSummary = `Matched agents then enter a ${world.sessionTemplate.mode} session with up to ${world.sessionTemplate.maxTurns} turns.`;
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
stage: 'matchmaking',
|
|
106
|
+
summary: `${matchingSummary} ${sessionSummary}`.trim(),
|
|
107
|
+
matchingMode: world.matching.mode,
|
|
108
|
+
matchingCadence: world.matching.cadence,
|
|
109
|
+
sessionMode: world.sessionTemplate.mode,
|
|
110
|
+
maxTurns: world.sessionTemplate.maxTurns,
|
|
111
|
+
turnTimeoutMs: world.sessionTemplate.turnTimeoutMs,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function createMembershipService({ worldService, store = null } = {}) {
|
|
116
|
+
function assertStore() {
|
|
117
|
+
if (!store) throw createConfigurationError();
|
|
118
|
+
return store;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
evaluateJoin({ worldId, profile = {}, maxFieldsPerStep = 1 } = {}) {
|
|
123
|
+
const world = worldService.requireWorld(worldId);
|
|
124
|
+
const normalizedProfile = profile && typeof profile === 'object' ? profile : {};
|
|
125
|
+
const orderedMissingFields = world.joinSchema.requiredFields.filter((field) =>
|
|
126
|
+
isEmptyValue(normalizedProfile[field.fieldId]),
|
|
127
|
+
);
|
|
128
|
+
const missingFields = orderedMissingFields.map((field) => projectMissingField(field));
|
|
129
|
+
const nextMissingField = missingFields[0] || null;
|
|
130
|
+
|
|
131
|
+
const joinCheck = {
|
|
132
|
+
worldId: world.worldId,
|
|
133
|
+
accepted: missingFields.length === 0,
|
|
134
|
+
status: missingFields.length === 0 ? 'eligible' : 'needs_profile',
|
|
135
|
+
missingFields,
|
|
136
|
+
nextMissingField,
|
|
137
|
+
missingFieldGuidance: {
|
|
138
|
+
mode: nextMissingField ? 'ordered_required_fields' : 'complete',
|
|
139
|
+
orderedMissingFields: missingFields,
|
|
140
|
+
orderedMissingFieldIds: missingFields.map((field) => field.fieldId),
|
|
141
|
+
nextMissingField,
|
|
142
|
+
remainingRequiredFieldCount: missingFields.length,
|
|
143
|
+
},
|
|
144
|
+
normalizedProfile,
|
|
145
|
+
nextAction:
|
|
146
|
+
missingFields.length === 0 ? 'join_world_when_membership_persistence_exists' : 'collect_missing_profile_fields',
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const worldDetail = worldService.describeWorldDetail(world.worldId);
|
|
150
|
+
return {
|
|
151
|
+
...joinCheck,
|
|
152
|
+
profileCollectionFlow: buildWorldProfileCollectionFlow({
|
|
153
|
+
worldDetail,
|
|
154
|
+
joinCheck,
|
|
155
|
+
profile: normalizedProfile,
|
|
156
|
+
maxFieldsPerStep,
|
|
157
|
+
}),
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
async createMembership({ worldId, agentId, profileSnapshot } = {}) {
|
|
161
|
+
worldService.requireWorld(worldId);
|
|
162
|
+
const membershipStore = assertStore();
|
|
163
|
+
const normalizedAgentId = normalizeAgentId(agentId);
|
|
164
|
+
if (!normalizedAgentId) {
|
|
165
|
+
throw createInvalidJoinRequestError('agentId');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const agent = membershipStore.getAgent(normalizedAgentId);
|
|
169
|
+
|
|
170
|
+
if (!agent) {
|
|
171
|
+
throw createAgentNotFoundError(normalizedAgentId);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const normalizedProfileSnapshot = normalizeProfileSnapshot(profileSnapshot, agent.profile);
|
|
175
|
+
const joinCheck = this.evaluateJoin({
|
|
176
|
+
worldId,
|
|
177
|
+
profile: normalizedProfileSnapshot,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (!joinCheck.accepted) {
|
|
181
|
+
throw createMembershipNotEligibleError(joinCheck);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const existingMembership = membershipStore.listMemberships({
|
|
185
|
+
worldId,
|
|
186
|
+
agentId: normalizedAgentId,
|
|
187
|
+
})[0] || null;
|
|
188
|
+
|
|
189
|
+
if (existingMembership) {
|
|
190
|
+
return { membership: existingMembership, created: false };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const membership = await membershipStore.createMembership({
|
|
194
|
+
worldId,
|
|
195
|
+
agentId: normalizedAgentId,
|
|
196
|
+
status: 'joined',
|
|
197
|
+
profileSnapshot: joinCheck.normalizedProfile,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
return { membership, created: true };
|
|
201
|
+
},
|
|
202
|
+
async joinWorld({ worldId, agentId, profile, profileSnapshot, maxFieldsPerStep = 1 } = {}) {
|
|
203
|
+
const world = worldService.requireWorld(worldId);
|
|
204
|
+
const membershipStore = assertStore();
|
|
205
|
+
const normalizedAgentId = normalizeAgentId(agentId);
|
|
206
|
+
|
|
207
|
+
if (!normalizedAgentId) {
|
|
208
|
+
throw createInvalidJoinRequestError('agentId');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const agent = membershipStore.getAgent(normalizedAgentId);
|
|
212
|
+
if (!agent) {
|
|
213
|
+
throw createAgentNotFoundError(normalizedAgentId);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const effectiveProfile = normalizeProfileSnapshot(profile, profileSnapshot, agent.profile);
|
|
217
|
+
const joinCheck = this.evaluateJoin({
|
|
218
|
+
worldId: world.worldId,
|
|
219
|
+
profile: effectiveProfile,
|
|
220
|
+
maxFieldsPerStep,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (!joinCheck.accepted) {
|
|
224
|
+
throw createJoinNotEligibleError(joinCheck);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const existingMembership = membershipStore.listMemberships({
|
|
228
|
+
worldId: world.worldId,
|
|
229
|
+
agentId: normalizedAgentId,
|
|
230
|
+
})[0] || null;
|
|
231
|
+
|
|
232
|
+
const membership = existingMembership
|
|
233
|
+
? await membershipStore.updateMembership(existingMembership.membershipId, {
|
|
234
|
+
status: 'active',
|
|
235
|
+
profileSnapshot: joinCheck.normalizedProfile,
|
|
236
|
+
})
|
|
237
|
+
: await membershipStore.createMembership({
|
|
238
|
+
worldId: world.worldId,
|
|
239
|
+
agentId: normalizedAgentId,
|
|
240
|
+
status: 'active',
|
|
241
|
+
profileSnapshot: joinCheck.normalizedProfile,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
membership,
|
|
246
|
+
membershipStatus: membership.status,
|
|
247
|
+
created: !existingMembership,
|
|
248
|
+
nextStageSummary: buildNextStageSummary(world),
|
|
249
|
+
orchestration: buildWorldJoinOutcomeOrchestration({
|
|
250
|
+
worldDetail: worldService.describeWorldDetail(world.worldId),
|
|
251
|
+
joinResult: {
|
|
252
|
+
membershipStatus: membership.status,
|
|
253
|
+
membership,
|
|
254
|
+
nextStageSummary: buildNextStageSummary(world),
|
|
255
|
+
},
|
|
256
|
+
}),
|
|
257
|
+
};
|
|
258
|
+
},
|
|
259
|
+
getMembership({ worldId, agentId, includeDisabled = false } = {}) {
|
|
260
|
+
const normalizedAgentId = normalizeAgentId(agentId);
|
|
261
|
+
if (!normalizedAgentId) return null;
|
|
262
|
+
worldService.requireWorld(worldId, { includeDisabled });
|
|
263
|
+
return assertStore().listMemberships({ worldId, agentId: normalizedAgentId })[0] || null;
|
|
264
|
+
},
|
|
265
|
+
listMemberships({ worldId, agentId = null, status = null, includeDisabled = false } = {}) {
|
|
266
|
+
worldService.requireWorld(worldId, { includeDisabled });
|
|
267
|
+
return assertStore().listMemberships({ worldId, agentId, status });
|
|
268
|
+
},
|
|
269
|
+
listMembershipsAcrossWorlds({ agentId = null, status = null } = {}) {
|
|
270
|
+
return assertStore().listMemberships({ agentId, status });
|
|
271
|
+
},
|
|
272
|
+
countMemberships({ worldId, agentId = null, status = null, includeDisabled = false } = {}) {
|
|
273
|
+
worldService.requireWorld(worldId, { includeDisabled });
|
|
274
|
+
return assertStore().countMemberships({ worldId, agentId, status });
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { authenticateAppTokenRequest } from '../../lib/http-auth.js';
|
|
2
|
+
|
|
3
|
+
function sendOnboardingError(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
|
+
export function registerOnboardingRoutes(app, { onboardingService, store = null }) {
|
|
13
|
+
app.get('/v1/meta/install', (_req, res) => {
|
|
14
|
+
res.json(onboardingService.getInstallManifest());
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
app.get('/v1/onboarding/plan', (req, res) => {
|
|
18
|
+
res.json(onboardingService.getFirstRunPlan({ worldId: req.query.worldId }));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
app.post('/v1/onboarding/activate', (req, res) => {
|
|
22
|
+
const auth = authenticateAppTokenRequest({ store, req });
|
|
23
|
+
if (auth.present && !auth.ok) {
|
|
24
|
+
return res.status(401).json(auth.error);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const result = onboardingService.activateInstallation({
|
|
29
|
+
auth,
|
|
30
|
+
input: req.body || {},
|
|
31
|
+
});
|
|
32
|
+
return res.status(result.created ? 201 : 200).json(result);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return sendOnboardingError(res, error);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|