@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,230 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import {
|
|
3
|
+
CLAWORLD_DOCTOR_COMMAND,
|
|
4
|
+
CLAWORLD_INSTALLER_COMMAND,
|
|
5
|
+
CLAWORLD_INSTALLER_PACKAGE_NAME,
|
|
6
|
+
CLAWORLD_OPENCLAW_MIN_HOST_VERSION,
|
|
7
|
+
CLAWORLD_UPDATE_COMMAND,
|
|
8
|
+
} from '../../openclaw/installer/constants.js';
|
|
9
|
+
|
|
10
|
+
const DEFAULT_INSTALL_CHANNEL_ID = 'claworld';
|
|
11
|
+
const DEFAULT_INSTALL_ACCOUNT_ID = 'claworld';
|
|
12
|
+
const DEFAULT_INSTALL_LOCAL_AGENT_ID = 'claworld';
|
|
13
|
+
const DEFAULT_ACTIVATION_DISPLAY_NAME = 'Claworld Agent';
|
|
14
|
+
|
|
15
|
+
function normalizeText(value, fallback = null) {
|
|
16
|
+
if (value == null) return fallback;
|
|
17
|
+
const normalized = String(value).trim();
|
|
18
|
+
return normalized || fallback;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function projectRecommendedWorld(world = {}) {
|
|
22
|
+
return {
|
|
23
|
+
worldId: world.worldId,
|
|
24
|
+
displayName: world.displayName,
|
|
25
|
+
summary: world.summary,
|
|
26
|
+
category: world.category,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createConfigurationError() {
|
|
31
|
+
const error = new Error('onboarding_store_unavailable');
|
|
32
|
+
error.code = 'onboarding_store_unavailable';
|
|
33
|
+
error.status = 500;
|
|
34
|
+
error.responseBody = {
|
|
35
|
+
error: error.code,
|
|
36
|
+
message: 'onboarding activation requires a configured metadata store',
|
|
37
|
+
};
|
|
38
|
+
return error;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function createInvalidActivationRequestError(fieldId, message) {
|
|
42
|
+
const error = new Error(`invalid_onboarding_activation:${fieldId}`);
|
|
43
|
+
error.code = 'invalid_onboarding_activation';
|
|
44
|
+
error.status = 400;
|
|
45
|
+
error.responseBody = {
|
|
46
|
+
error: error.code,
|
|
47
|
+
message: 'dialog-first onboarding activation only accepts backend-issued identity flow inputs',
|
|
48
|
+
fieldErrors: [
|
|
49
|
+
{
|
|
50
|
+
fieldId,
|
|
51
|
+
message,
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
return error;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createHiddenActivationAgentCode() {
|
|
59
|
+
return `claworld_install_${randomUUID().replace(/-/g, '').slice(0, 20)}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function createActivatedResponse({ agent, appToken, created, bindingSource }) {
|
|
63
|
+
return {
|
|
64
|
+
status: 'activated',
|
|
65
|
+
created: created === true,
|
|
66
|
+
bindingSource: normalizeText(bindingSource, null),
|
|
67
|
+
agentId: normalizeText(agent?.agentId, null),
|
|
68
|
+
appToken: normalizeText(appToken, null),
|
|
69
|
+
agentCode: null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function validateActivationInput(input = {}) {
|
|
74
|
+
const normalizedInput = input && typeof input === 'object' && !Array.isArray(input) ? input : {};
|
|
75
|
+
const blockedFields = [
|
|
76
|
+
['agentId', 'dialog-first activation must not receive a user-provided agentId'],
|
|
77
|
+
['agentCode', 'dialog-first activation must not receive a user-provided agentCode'],
|
|
78
|
+
['code', 'dialog-first activation must not receive a user-provided code'],
|
|
79
|
+
['appToken', 'dialog-first activation must not receive a user-provided appToken in the request body'],
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
for (const [fieldId, message] of blockedFields) {
|
|
83
|
+
if (normalizeText(normalizedInput[fieldId], null)) {
|
|
84
|
+
throw createInvalidActivationRequestError(fieldId, message);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
displayName: normalizeText(normalizedInput.displayName, DEFAULT_ACTIVATION_DISPLAY_NAME),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function createActivatedAgent({ store, displayName }) {
|
|
94
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
95
|
+
try {
|
|
96
|
+
return store.createAgent({
|
|
97
|
+
agentCode: createHiddenActivationAgentCode(),
|
|
98
|
+
displayName,
|
|
99
|
+
});
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (error?.message === 'address already exists') continue;
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
throw new Error('failed_to_generate_activation_identity');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function createOnboardingService({ worldService, store = null } = {}) {
|
|
110
|
+
function assertStore() {
|
|
111
|
+
if (!store) throw createConfigurationError();
|
|
112
|
+
return store;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
getInstallManifest() {
|
|
117
|
+
const recommendedWorlds = worldService.listWorlds().slice(0, 3).map((world) => projectRecommendedWorld(world));
|
|
118
|
+
return {
|
|
119
|
+
product: 'claworld',
|
|
120
|
+
mode: 'agent_install',
|
|
121
|
+
installer: {
|
|
122
|
+
command: CLAWORLD_INSTALLER_COMMAND,
|
|
123
|
+
packageName: CLAWORLD_INSTALLER_PACKAGE_NAME,
|
|
124
|
+
minHostVersion: CLAWORLD_OPENCLAW_MIN_HOST_VERSION,
|
|
125
|
+
canonicalFlow: 'installer_first',
|
|
126
|
+
lifecycle: {
|
|
127
|
+
install: CLAWORLD_INSTALLER_COMMAND,
|
|
128
|
+
doctor: CLAWORLD_DOCTOR_COMMAND,
|
|
129
|
+
update: CLAWORLD_UPDATE_COMMAND,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
plugin: {
|
|
133
|
+
pluginId: DEFAULT_INSTALL_CHANNEL_ID,
|
|
134
|
+
packageName: CLAWORLD_INSTALLER_PACKAGE_NAME,
|
|
135
|
+
installMode: 'npm',
|
|
136
|
+
minHostVersion: CLAWORLD_OPENCLAW_MIN_HOST_VERSION,
|
|
137
|
+
},
|
|
138
|
+
setup: {
|
|
139
|
+
channelId: DEFAULT_INSTALL_CHANNEL_ID,
|
|
140
|
+
defaultAccountId: DEFAULT_INSTALL_ACCOUNT_ID,
|
|
141
|
+
defaultLocalAgentId: DEFAULT_INSTALL_LOCAL_AGENT_ID,
|
|
142
|
+
configPath: 'channels.claworld.accounts.<accountId>',
|
|
143
|
+
credentialField: 'appToken',
|
|
144
|
+
managedBy: 'host_agent_or_openclaw_runtime',
|
|
145
|
+
userInput: {
|
|
146
|
+
requiresCode: false,
|
|
147
|
+
requiresAgentId: false,
|
|
148
|
+
requiresToken: false,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
compatibility: {
|
|
152
|
+
nativeOpenclaw: {
|
|
153
|
+
status: 'compatibility_only',
|
|
154
|
+
commands: [
|
|
155
|
+
'openclaw onboard',
|
|
156
|
+
'openclaw channels add claworld --app-token <token>',
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
activation: {
|
|
161
|
+
endpoint: '/v1/onboarding/activate',
|
|
162
|
+
method: 'POST',
|
|
163
|
+
requiresUserCode: false,
|
|
164
|
+
canonicalIdentityField: 'agentId',
|
|
165
|
+
credentialField: 'appToken',
|
|
166
|
+
responseFields: ['status', 'agentId', 'appToken', 'agentCode'],
|
|
167
|
+
},
|
|
168
|
+
verification: {
|
|
169
|
+
statusRoute: '/plugins/claworld/status',
|
|
170
|
+
pairTool: 'claworld_pair_agent',
|
|
171
|
+
expectedBinding: 'runtime_bound_by_app_token',
|
|
172
|
+
},
|
|
173
|
+
steps: [
|
|
174
|
+
`run ${CLAWORLD_INSTALLER_COMMAND}`,
|
|
175
|
+
'installer validates OpenClaw availability and minimum host version',
|
|
176
|
+
'installer verifies or installs the claworld OpenClaw plugin package',
|
|
177
|
+
'installer writes or refreshes the managed claworld channel config',
|
|
178
|
+
'installer calls POST /v1/onboarding/activate to obtain agentId and appToken when reuse is not possible',
|
|
179
|
+
'installer persists the returned appToken into the managed claworld account config',
|
|
180
|
+
'installer reloads or starts the runtime and verifies the relay binding',
|
|
181
|
+
`ongoing lifecycle uses ${CLAWORLD_UPDATE_COMMAND} for tracked package updates plus managed repair, then ${CLAWORLD_DOCTOR_COMMAND} for health confirmation`,
|
|
182
|
+
],
|
|
183
|
+
recommendedWorlds,
|
|
184
|
+
status: 'ready',
|
|
185
|
+
};
|
|
186
|
+
},
|
|
187
|
+
getFirstRunPlan({ worldId = null } = {}) {
|
|
188
|
+
const preferredWorld = worldId ? worldService.getWorld(worldId) : null;
|
|
189
|
+
const fallbackWorld = worldService.getWorld('dating-demo-world');
|
|
190
|
+
const selectedWorld = preferredWorld || fallbackWorld || worldService.getWorld(worldService.listWorldIds()[0]);
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
selectedWorld: selectedWorld ? { worldId: selectedWorld.worldId, displayName: selectedWorld.displayName } : null,
|
|
194
|
+
actions: [
|
|
195
|
+
'install the claworld plugin and write the managed config shape',
|
|
196
|
+
'activate the install through POST /v1/onboarding/activate',
|
|
197
|
+
'persist the returned appToken and verify the runtime binding',
|
|
198
|
+
'collect required world profile fields',
|
|
199
|
+
'validate world membership eligibility',
|
|
200
|
+
'start the first A2A loop or deliver world content',
|
|
201
|
+
],
|
|
202
|
+
status: 'ready',
|
|
203
|
+
};
|
|
204
|
+
},
|
|
205
|
+
activateInstallation({ auth = null, input = {} } = {}) {
|
|
206
|
+
const activationInput = validateActivationInput(input);
|
|
207
|
+
assertStore();
|
|
208
|
+
|
|
209
|
+
if (auth?.ok) {
|
|
210
|
+
return createActivatedResponse({
|
|
211
|
+
agent: auth.agent,
|
|
212
|
+
appToken: auth.token,
|
|
213
|
+
created: false,
|
|
214
|
+
bindingSource: 'provided_app_token',
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const createdAgent = createActivatedAgent({
|
|
219
|
+
store,
|
|
220
|
+
displayName: activationInput.displayName,
|
|
221
|
+
});
|
|
222
|
+
return createActivatedResponse({
|
|
223
|
+
agent: createdAgent,
|
|
224
|
+
appToken: createdAgent.bootstrapCredential?.token || null,
|
|
225
|
+
created: true,
|
|
226
|
+
bindingSource: 'created_agent_app_token',
|
|
227
|
+
});
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createSystemMessageOrchestrator } from '../../lib/runtime-guidance.js';
|
|
2
|
+
|
|
3
|
+
export function createSessionOrchestrator({
|
|
4
|
+
worldService,
|
|
5
|
+
resultService,
|
|
6
|
+
systemMessages = createSystemMessageOrchestrator(),
|
|
7
|
+
} = {}) {
|
|
8
|
+
return {
|
|
9
|
+
previewSession({ worldId, sessionId = 'ses_preview' } = {}) {
|
|
10
|
+
const world = worldService.requireWorld(worldId);
|
|
11
|
+
const openingPlan = systemMessages.planMessages({
|
|
12
|
+
sessionId,
|
|
13
|
+
trigger: 'session_started',
|
|
14
|
+
worldRules: world.sessionTemplate.worldRules,
|
|
15
|
+
});
|
|
16
|
+
const convergencePlan = systemMessages.planMessages({
|
|
17
|
+
sessionId,
|
|
18
|
+
trigger: 'convergence',
|
|
19
|
+
remainingTurns: Math.max(world.sessionTemplate.maxTurns - 1, 0),
|
|
20
|
+
worldRules: world.sessionTemplate.worldRules,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
worldId: world.worldId,
|
|
25
|
+
sessionTemplate: {
|
|
26
|
+
mode: world.sessionTemplate.mode,
|
|
27
|
+
maxTurns: world.sessionTemplate.maxTurns,
|
|
28
|
+
turnTimeoutMs: world.sessionTemplate.turnTimeoutMs,
|
|
29
|
+
raiseHandPolicy: world.sessionTemplate.raiseHandPolicy,
|
|
30
|
+
},
|
|
31
|
+
openingPlan,
|
|
32
|
+
convergencePlan,
|
|
33
|
+
resultPreview: resultService.preview({ world, sessionId }),
|
|
34
|
+
status: 'preview_ready',
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createCanonicalResultBuilder } from '../../openclaw/runtime/canonical-result-builder.js';
|
|
2
|
+
|
|
3
|
+
export function createResultService({ builder = createCanonicalResultBuilder() } = {}) {
|
|
4
|
+
return {
|
|
5
|
+
schema: builder.schema,
|
|
6
|
+
preview({ world, sessionId = 'ses_preview' } = {}) {
|
|
7
|
+
return builder.build({
|
|
8
|
+
sessionId,
|
|
9
|
+
intentSignals: world.resultContract.exampleSignals.intentSignals,
|
|
10
|
+
conversationSignals: world.resultContract.exampleSignals.conversationSignals,
|
|
11
|
+
agentSignals: world.resultContract.exampleSignals.agentSignals,
|
|
12
|
+
});
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { projectSearchModel } from '../contracts/world-manifest.js';
|
|
2
|
+
import { WORLD_ACTIONS } from '../worlds/world-authorization.js';
|
|
3
|
+
|
|
4
|
+
function isEmptyValue(value) {
|
|
5
|
+
if (value == null) return true;
|
|
6
|
+
if (typeof value === 'string') return value.trim() === '';
|
|
7
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeText(value, fallback = null) {
|
|
12
|
+
if (value == null) return fallback;
|
|
13
|
+
const normalized = String(value).trim();
|
|
14
|
+
return normalized || fallback;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeAgentId(agentId) {
|
|
18
|
+
return normalizeText(agentId, null);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeInteger(value, fallback = 0) {
|
|
22
|
+
const parsed = Number(value);
|
|
23
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
24
|
+
return Math.max(0, Math.trunc(parsed));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeStringList(value) {
|
|
28
|
+
if (!Array.isArray(value)) return [];
|
|
29
|
+
return [...new Set(
|
|
30
|
+
value
|
|
31
|
+
.map((entry) => normalizeText(entry, null)?.toLowerCase() || null)
|
|
32
|
+
.filter(Boolean),
|
|
33
|
+
)];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeComparableText(value) {
|
|
37
|
+
return normalizeText(value, null)?.toLowerCase() || null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeSearchLimit(limit, fallback = 10) {
|
|
41
|
+
const normalized = normalizeInteger(limit, fallback);
|
|
42
|
+
if (normalized <= 0) return fallback;
|
|
43
|
+
return Math.min(normalized, 25);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createConfigurationError() {
|
|
47
|
+
const error = new Error('membership_store_unavailable');
|
|
48
|
+
error.code = 'membership_store_unavailable';
|
|
49
|
+
error.status = 500;
|
|
50
|
+
return error;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function createInvalidSearchRequestError(fieldId, message = `${fieldId} is required`) {
|
|
54
|
+
const error = new Error(`invalid_search_request:${fieldId}`);
|
|
55
|
+
error.code = 'invalid_search_request';
|
|
56
|
+
error.status = 400;
|
|
57
|
+
error.responseBody = {
|
|
58
|
+
error: error.code,
|
|
59
|
+
message: 'world search request is missing required fields',
|
|
60
|
+
fieldErrors: [
|
|
61
|
+
{
|
|
62
|
+
fieldId,
|
|
63
|
+
message,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
return error;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function createAgentNotFoundError(agentId) {
|
|
71
|
+
const error = new Error(`agent_not_found:${agentId}`);
|
|
72
|
+
error.code = 'agent_not_found';
|
|
73
|
+
error.status = 404;
|
|
74
|
+
return error;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function createSearchMembershipNotActiveError(worldId, agentId) {
|
|
78
|
+
const error = new Error(`world_search_membership_not_active:${worldId}:${agentId}`);
|
|
79
|
+
error.code = 'world_search_membership_not_active';
|
|
80
|
+
error.status = 409;
|
|
81
|
+
error.responseBody = {
|
|
82
|
+
error: error.code,
|
|
83
|
+
message: 'agent must have an active world membership before searching the world',
|
|
84
|
+
worldId,
|
|
85
|
+
agentId,
|
|
86
|
+
requiredMembershipStatus: 'active',
|
|
87
|
+
};
|
|
88
|
+
return error;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizeQuery(world, query = {}, viewerProfile = {}) {
|
|
92
|
+
const baseProfile = viewerProfile && typeof viewerProfile === 'object' ? viewerProfile : {};
|
|
93
|
+
const overrides = query && typeof query === 'object' ? query : {};
|
|
94
|
+
|
|
95
|
+
return Object.fromEntries(
|
|
96
|
+
world.searchSchema.inputFields
|
|
97
|
+
.map((field) => {
|
|
98
|
+
const hasOverride = Object.prototype.hasOwnProperty.call(overrides, field.fieldId);
|
|
99
|
+
const value = hasOverride ? overrides[field.fieldId] : baseProfile[field.fieldId];
|
|
100
|
+
return [field.fieldId, value];
|
|
101
|
+
})
|
|
102
|
+
.filter(([, value]) => !isEmptyValue(value)),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resolveFieldWeight(worldId, field) {
|
|
107
|
+
const fieldId = field.fieldId;
|
|
108
|
+
if (worldId === 'dating-demo-world') {
|
|
109
|
+
if (fieldId === 'intent') return { exact: 50, overlapUnit: 10, overlapCap: 20 };
|
|
110
|
+
if (fieldId === 'location') return { exact: 30, overlapUnit: 10, overlapCap: 20 };
|
|
111
|
+
if (fieldId === 'interests') return { exact: 20, overlapUnit: 10, overlapCap: 20 };
|
|
112
|
+
}
|
|
113
|
+
if (worldId === 'skill-handoff-world') {
|
|
114
|
+
if (fieldId === 'capabilities') return { exact: 50, overlapUnit: 10, overlapCap: 50 };
|
|
115
|
+
if (fieldId === 'budgetBand') return { exact: 20, overlapUnit: 10, overlapCap: 20 };
|
|
116
|
+
}
|
|
117
|
+
if (worldId === 'job-match-world') {
|
|
118
|
+
if (fieldId === 'targetRole') return { exact: 50, overlapUnit: 10, overlapCap: 20 };
|
|
119
|
+
if (fieldId === 'location') return { exact: 15, overlapUnit: 10, overlapCap: 20 };
|
|
120
|
+
if (fieldId === 'workMode') return { exact: 15, overlapUnit: 10, overlapCap: 20 };
|
|
121
|
+
if (fieldId === 'experienceSummary') return { exact: 20, overlapUnit: 10, overlapCap: 20 };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
exact: field.required ? 30 : 15,
|
|
126
|
+
overlapUnit: field.required ? 10 : 6,
|
|
127
|
+
overlapCap: field.required ? 20 : 12,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function compareField(field, queryValue, candidateValue, worldId) {
|
|
132
|
+
if (isEmptyValue(queryValue) || isEmptyValue(candidateValue)) return null;
|
|
133
|
+
|
|
134
|
+
const weights = resolveFieldWeight(worldId, field);
|
|
135
|
+
|
|
136
|
+
if (field.type === 'string[]') {
|
|
137
|
+
const queryItems = normalizeStringList(queryValue);
|
|
138
|
+
const candidateItems = normalizeStringList(candidateValue);
|
|
139
|
+
const sharedValues = queryItems.filter((entry) => candidateItems.includes(entry));
|
|
140
|
+
if (sharedValues.length === 0) return null;
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
fieldId: field.fieldId,
|
|
144
|
+
label: field.label,
|
|
145
|
+
matchType: 'overlap',
|
|
146
|
+
queryValue: queryItems,
|
|
147
|
+
candidateValue: candidateItems,
|
|
148
|
+
sharedValues,
|
|
149
|
+
contribution: Math.min(sharedValues.length * weights.overlapUnit, weights.overlapCap),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const normalizedQueryValue = normalizeComparableText(queryValue);
|
|
154
|
+
const normalizedCandidateValue = normalizeComparableText(candidateValue);
|
|
155
|
+
if (!normalizedQueryValue || !normalizedCandidateValue) return null;
|
|
156
|
+
if (normalizedQueryValue !== normalizedCandidateValue) return null;
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
fieldId: field.fieldId,
|
|
160
|
+
label: field.label,
|
|
161
|
+
matchType: 'exact',
|
|
162
|
+
queryValue: normalizeText(queryValue, ''),
|
|
163
|
+
candidateValue: normalizeText(candidateValue, ''),
|
|
164
|
+
sharedValues: [],
|
|
165
|
+
contribution: weights.exact,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function projectSummaryField(field, profile = {}) {
|
|
170
|
+
const value = profile[field.fieldId];
|
|
171
|
+
if (isEmptyValue(value)) return null;
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
fieldId: field.fieldId,
|
|
175
|
+
label: field.label,
|
|
176
|
+
value: Array.isArray(value) ? value : normalizeText(value, null),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function projectProfileSummary(world, profile = {}, agent = null) {
|
|
181
|
+
return {
|
|
182
|
+
displayName: normalizeText(agent?.displayName, null),
|
|
183
|
+
headline: normalizeText(profile.headline, null),
|
|
184
|
+
requiredFields: world.joinSchema.requiredFields
|
|
185
|
+
.map((field) => projectSummaryField(field, profile))
|
|
186
|
+
.filter(Boolean),
|
|
187
|
+
optionalFields: world.joinSchema.optionalFields
|
|
188
|
+
.map((field) => projectSummaryField(field, profile))
|
|
189
|
+
.filter(Boolean),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function resolveSearchResultAgentId(item = {}, fallback = null) {
|
|
194
|
+
return normalizeText(item.agentId, normalizeText(item.playerId, fallback));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function compareSearchResults(left, right) {
|
|
198
|
+
if (right.score !== left.score) return right.score - left.score;
|
|
199
|
+
if (right.matchedFieldIds.length !== left.matchedFieldIds.length) {
|
|
200
|
+
return right.matchedFieldIds.length - left.matchedFieldIds.length;
|
|
201
|
+
}
|
|
202
|
+
if (left.joinedAt !== right.joinedAt) {
|
|
203
|
+
return String(left.joinedAt).localeCompare(String(right.joinedAt));
|
|
204
|
+
}
|
|
205
|
+
return String(resolveSearchResultAgentId(left, '')).localeCompare(String(resolveSearchResultAgentId(right, '')));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function compareRecentFallbackResults(left, right) {
|
|
209
|
+
if (left.lastHeartbeatAt !== right.lastHeartbeatAt) {
|
|
210
|
+
return String(right.lastHeartbeatAt || '').localeCompare(String(left.lastHeartbeatAt || ''));
|
|
211
|
+
}
|
|
212
|
+
if (left.joinedAt !== right.joinedAt) {
|
|
213
|
+
return String(right.joinedAt || '').localeCompare(String(left.joinedAt || ''));
|
|
214
|
+
}
|
|
215
|
+
return String(resolveSearchResultAgentId(left, '')).localeCompare(String(resolveSearchResultAgentId(right, '')));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function summarizeMatchedFields(matchedFields = []) {
|
|
219
|
+
if (matchedFields.length === 0) {
|
|
220
|
+
return 'Recently active online world member with no direct profile overlap in the current query.';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return `Matched on ${matchedFields.map((field) => field.label).join(', ')}.`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function createWorldSearchService({
|
|
227
|
+
worldService,
|
|
228
|
+
worldAuthorizationService,
|
|
229
|
+
store = null,
|
|
230
|
+
presence = null,
|
|
231
|
+
} = {}) {
|
|
232
|
+
function assertStore() {
|
|
233
|
+
if (!store) throw createConfigurationError();
|
|
234
|
+
return store;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function resolvePresence(agentId) {
|
|
238
|
+
if (!presence) {
|
|
239
|
+
return {
|
|
240
|
+
online: true,
|
|
241
|
+
connectedAt: null,
|
|
242
|
+
lastHeartbeatAt: null,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return presence.getPresence(agentId);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function buildViewerContext(world, membershipStore, normalizedAgentId) {
|
|
249
|
+
const viewerAgent = membershipStore.getAgent(normalizedAgentId);
|
|
250
|
+
if (!viewerAgent) {
|
|
251
|
+
throw createAgentNotFoundError(normalizedAgentId);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const authorization = worldAuthorizationService.evaluateWorldAction({
|
|
255
|
+
worldId: world.worldId,
|
|
256
|
+
actorAgentId: normalizedAgentId,
|
|
257
|
+
action: WORLD_ACTIONS.SEARCH,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
if (!authorization.allowed || authorization.membership?.status !== 'active') {
|
|
261
|
+
throw createSearchMembershipNotActiveError(world.worldId, normalizedAgentId);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
viewerAgent,
|
|
266
|
+
viewerMembership: authorization.membership,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
describeSearch(worldId) {
|
|
272
|
+
const world = worldService.requireWorld(worldId);
|
|
273
|
+
return projectSearchModel(world);
|
|
274
|
+
},
|
|
275
|
+
searchWorld({ worldId, agentId, query = {}, limit = null } = {}) {
|
|
276
|
+
const world = worldService.requireWorld(worldId);
|
|
277
|
+
const membershipStore = assertStore();
|
|
278
|
+
const normalizedAgentId = normalizeAgentId(agentId);
|
|
279
|
+
if (!normalizedAgentId) {
|
|
280
|
+
throw createInvalidSearchRequestError('agentId');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const { viewerAgent, viewerMembership } = buildViewerContext(world, membershipStore, normalizedAgentId);
|
|
284
|
+
const searchInput = normalizeQuery(world, query, viewerMembership.profileSnapshot || viewerAgent.profile || {});
|
|
285
|
+
const normalizedLimit = normalizeSearchLimit(limit, world.searchSchema.defaultLimit);
|
|
286
|
+
const onlineOnly = world.searchSchema.onlineOnly;
|
|
287
|
+
const activeMemberships = membershipStore.listMemberships({
|
|
288
|
+
worldId: world.worldId,
|
|
289
|
+
status: 'active',
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const items = activeMemberships
|
|
293
|
+
.filter((membership) => membership.agentId !== normalizedAgentId)
|
|
294
|
+
.map((membership) => {
|
|
295
|
+
const candidateAgent = membershipStore.getAgent(membership.agentId);
|
|
296
|
+
if (!candidateAgent) return null;
|
|
297
|
+
|
|
298
|
+
const presenceState = resolvePresence(candidateAgent.agentId);
|
|
299
|
+
if (onlineOnly && presenceState.online !== true) return null;
|
|
300
|
+
|
|
301
|
+
const matchedFields = world.searchSchema.inputFields
|
|
302
|
+
.map((field) => compareField(
|
|
303
|
+
field,
|
|
304
|
+
searchInput[field.fieldId],
|
|
305
|
+
membership.profileSnapshot?.[field.fieldId],
|
|
306
|
+
world.worldId,
|
|
307
|
+
))
|
|
308
|
+
.filter(Boolean);
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
agentId: candidateAgent.agentId,
|
|
312
|
+
playerId: candidateAgent.agentId,
|
|
313
|
+
membershipId: membership.membershipId,
|
|
314
|
+
worldId: world.worldId,
|
|
315
|
+
displayName: normalizeText(candidateAgent.displayName, candidateAgent.agentCode),
|
|
316
|
+
headline: normalizeText(membership.profileSnapshot?.headline, null),
|
|
317
|
+
address: candidateAgent.address,
|
|
318
|
+
online: presenceState.online === true,
|
|
319
|
+
connectedAt: presenceState.connectedAt,
|
|
320
|
+
lastHeartbeatAt: presenceState.lastHeartbeatAt,
|
|
321
|
+
score: matchedFields.reduce((sum, field) => sum + field.contribution, 0),
|
|
322
|
+
matchedFieldIds: matchedFields.map((field) => field.fieldId),
|
|
323
|
+
matchedFields,
|
|
324
|
+
resultType: matchedFields.length > 0 ? 'matched' : 'fallback',
|
|
325
|
+
reasonSummary: summarizeMatchedFields(matchedFields),
|
|
326
|
+
joinedAt: membership.joinedAt,
|
|
327
|
+
profileSummary: projectProfileSummary(world, membership.profileSnapshot || {}, candidateAgent),
|
|
328
|
+
};
|
|
329
|
+
})
|
|
330
|
+
.filter(Boolean);
|
|
331
|
+
|
|
332
|
+
const matchedItems = items
|
|
333
|
+
.filter((item) => item.resultType === 'matched')
|
|
334
|
+
.sort(compareSearchResults);
|
|
335
|
+
const fallbackItems = items
|
|
336
|
+
.filter((item) => item.resultType === 'fallback')
|
|
337
|
+
.sort(compareRecentFallbackResults);
|
|
338
|
+
const orderedItems = [...matchedItems, ...fallbackItems];
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
worldId: world.worldId,
|
|
342
|
+
agentId: normalizedAgentId,
|
|
343
|
+
viewerMembershipId: viewerMembership.membershipId,
|
|
344
|
+
searchModel: projectSearchModel(world),
|
|
345
|
+
searchInput,
|
|
346
|
+
onlineOnly,
|
|
347
|
+
candidateSource: onlineOnly ? 'active_memberships_online' : 'active_memberships',
|
|
348
|
+
limit: normalizedLimit,
|
|
349
|
+
totalMatches: orderedItems.length,
|
|
350
|
+
items: orderedItems.slice(0, normalizedLimit),
|
|
351
|
+
nextAction: orderedItems.length > 0 ? 'select_player_and_start_conversation' : 'broaden_search_or_wait',
|
|
352
|
+
status: orderedItems.length > 0 ? 'search_ready' : 'no_matches',
|
|
353
|
+
};
|
|
354
|
+
},
|
|
355
|
+
searchPlayers(options = {}) {
|
|
356
|
+
return this.searchWorld(options);
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
}
|