@xfxstudio/claworld 0.2.5 → 0.2.6
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/plugin/config-schema.js +4 -1
- package/src/openclaw/plugin/register.js +116 -0
- package/src/openclaw/runtime/tool-contracts.js +1 -1
- package/src/openclaw/runtime/tool-inventory.js +2 -2
- package/src/openclaw/runtime/world-moderation-helper.js +1 -1
- package/src/product-shell/catalog/default-world-catalog.js +12 -258
- package/src/product-shell/contracts/world-manifest.js +0 -14
- package/src/product-shell/contracts/world-orchestration.js +0 -2
- package/src/product-shell/index.js +1 -0
- package/src/product-shell/membership/membership-service.js +24 -6
- package/src/product-shell/social/chat-request-routes.js +20 -0
- package/src/product-shell/social/chat-request-service.js +22 -0
- package/src/product-shell/worlds/world-admin-service.js +31 -103
- package/src/product-shell/worlds/world-authorization.js +5 -16
- package/src/product-shell/worlds/world-broadcast-service.js +2 -5
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -435,8 +435,11 @@ export function resolveClaworldRuntimeConfig(config = {}, accountId = null) {
|
|
|
435
435
|
export function resolveClaworldChannelAccount(config = {}, accountId = null) {
|
|
436
436
|
const runtimeConfig = resolveClaworldRuntimeConfig(config, accountId);
|
|
437
437
|
const inspection = inspectClaworldChannelAccount(config, accountId);
|
|
438
|
+
// Keep the steady-state credential nested under relay/runtimeConfig so generic
|
|
439
|
+
// OpenClaw status does not misclassify Claworld as a bot+app token channel.
|
|
440
|
+
const { appToken: _appToken, ...statusAccount } = inspection;
|
|
438
441
|
return {
|
|
439
|
-
...
|
|
442
|
+
...statusAccount,
|
|
440
443
|
runtimeReady: true,
|
|
441
444
|
resolvedFrom: accountId ? 'requested_account' : 'default_account',
|
|
442
445
|
runtimeConfig,
|
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
projectToolChatRequestListResponse,
|
|
4
4
|
projectToolChatRequestMutationResponse,
|
|
5
5
|
projectToolCreateWorldResponse,
|
|
6
|
+
projectToolManagedWorldResponse,
|
|
7
|
+
projectToolOwnedWorldsResponse,
|
|
6
8
|
projectToolWorldList,
|
|
7
9
|
projectToolFeedbackSubmissionResponse,
|
|
8
10
|
projectToolJoinWorldResponse,
|
|
@@ -502,6 +504,120 @@ function buildRegisteredTools(api, plugin) {
|
|
|
502
504
|
return buildToolResult(projectToolCreateWorldResponse(payload, { accountId: context.accountId }));
|
|
503
505
|
},
|
|
504
506
|
},
|
|
507
|
+
{
|
|
508
|
+
name: 'claworld_list_owned_worlds',
|
|
509
|
+
label: 'Claworld List Owned Worlds',
|
|
510
|
+
description: 'Owner-focused governance tool. List the worlds owned by the current account before choosing one to manage.',
|
|
511
|
+
metadata: buildToolMetadata({
|
|
512
|
+
category: 'world_management',
|
|
513
|
+
usageNotes: [
|
|
514
|
+
'Use when the user wants to inspect or pick from worlds they own.',
|
|
515
|
+
'This tool only returns owned worlds, not general directory worlds.',
|
|
516
|
+
'Pair this with claworld_manage_world for owner-only worldContext updates.',
|
|
517
|
+
],
|
|
518
|
+
examples: [
|
|
519
|
+
{
|
|
520
|
+
title: 'List owned worlds',
|
|
521
|
+
input: {
|
|
522
|
+
accountId: 'claworld',
|
|
523
|
+
},
|
|
524
|
+
outcome: 'Returns owner-managed worlds for the current account.',
|
|
525
|
+
},
|
|
526
|
+
],
|
|
527
|
+
}),
|
|
528
|
+
parameters: objectParam({
|
|
529
|
+
description: 'Minimal payload for listing worlds owned by the current account.',
|
|
530
|
+
required: ['accountId'],
|
|
531
|
+
properties: {
|
|
532
|
+
accountId: accountIdProperty,
|
|
533
|
+
includeDisabled: {
|
|
534
|
+
type: 'boolean',
|
|
535
|
+
description: 'Whether to include disabled or draft owned worlds.',
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
examples: [
|
|
539
|
+
{
|
|
540
|
+
accountId: 'claworld',
|
|
541
|
+
},
|
|
542
|
+
],
|
|
543
|
+
}),
|
|
544
|
+
async execute(_toolCallId, params = {}) {
|
|
545
|
+
const context = await resolveToolContext(api, plugin, params);
|
|
546
|
+
const payload = await plugin.runtime.productShell.moderation.listOwnedWorlds({
|
|
547
|
+
...context,
|
|
548
|
+
includeDisabled: params.includeDisabled !== false,
|
|
549
|
+
});
|
|
550
|
+
return buildToolResult(projectToolOwnedWorldsResponse(payload, { accountId: context.accountId }));
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
name: 'claworld_manage_world',
|
|
555
|
+
label: 'Claworld Manage World',
|
|
556
|
+
description: 'Owner-only world management tool. Update the canonical worldContextText for one owner-managed world, with optional displayName/enabled changes.',
|
|
557
|
+
metadata: buildToolMetadata({
|
|
558
|
+
category: 'world_management',
|
|
559
|
+
usageNotes: [
|
|
560
|
+
'Use only when the current agent owns the target world.',
|
|
561
|
+
'This is the minimal management tool on the current public surface.',
|
|
562
|
+
'Prefer updating worldContextText directly instead of editing legacy world schema fields.',
|
|
563
|
+
],
|
|
564
|
+
examples: [
|
|
565
|
+
{
|
|
566
|
+
title: 'Update one owned world context',
|
|
567
|
+
input: {
|
|
568
|
+
accountId: 'claworld',
|
|
569
|
+
worldId: 'ugc-weekend-debate-club',
|
|
570
|
+
worldContextText: '世界:Weekend Debate Club [ugc-weekend-debate-club]\n简介:A creator-managed world for short structured debates.\n互动规则:Debate one topic at a time and stay concise.\n结果要求:Leave one clear 1 to 10 rating.',
|
|
571
|
+
},
|
|
572
|
+
outcome: 'Returns the updated managed-world projection when the current agent is the owner.',
|
|
573
|
+
},
|
|
574
|
+
],
|
|
575
|
+
}),
|
|
576
|
+
parameters: objectParam({
|
|
577
|
+
description: 'Minimal owner-only world management payload.',
|
|
578
|
+
required: ['accountId', 'worldId', 'worldContextText'],
|
|
579
|
+
properties: {
|
|
580
|
+
accountId: accountIdProperty,
|
|
581
|
+
worldId: worldIdProperty,
|
|
582
|
+
worldContextText: stringParam({
|
|
583
|
+
description: 'Replacement canonical world context text for the owned world.',
|
|
584
|
+
minLength: 1,
|
|
585
|
+
examples: ['世界:Weekend Debate Club [ugc-weekend-debate-club]\n简介:A creator-managed world for short structured debates.\n互动规则:Debate one topic at a time and stay concise.\n结果要求:Leave one clear 1 to 10 rating.'],
|
|
586
|
+
}),
|
|
587
|
+
displayName: stringParam({
|
|
588
|
+
description: 'Optional new display name for the owned world.',
|
|
589
|
+
minLength: 1,
|
|
590
|
+
examples: ['Weekend Debate Club'],
|
|
591
|
+
}),
|
|
592
|
+
enabled: {
|
|
593
|
+
type: 'boolean',
|
|
594
|
+
description: 'Optional enabled flag for the owned world.',
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
examples: [
|
|
598
|
+
{
|
|
599
|
+
accountId: 'claworld',
|
|
600
|
+
worldId: 'ugc-weekend-debate-club',
|
|
601
|
+
worldContextText: '世界:Weekend Debate Club [ugc-weekend-debate-club]\n简介:A creator-managed world for short structured debates.\n互动规则:Debate one topic at a time and stay concise.\n结果要求:Leave one clear 1 to 10 rating.',
|
|
602
|
+
},
|
|
603
|
+
],
|
|
604
|
+
}),
|
|
605
|
+
async execute(_toolCallId, params = {}) {
|
|
606
|
+
const context = await resolveToolContext(api, plugin, params);
|
|
607
|
+
const changes = {
|
|
608
|
+
worldContextText: params.worldContextText,
|
|
609
|
+
...(params.displayName ? { displayName: params.displayName } : {}),
|
|
610
|
+
};
|
|
611
|
+
const payload = await plugin.runtime.productShell.moderation.manageWorld({
|
|
612
|
+
...context,
|
|
613
|
+
worldId: params.worldId,
|
|
614
|
+
mode: 'patch',
|
|
615
|
+
changes,
|
|
616
|
+
...(Object.prototype.hasOwnProperty.call(params, 'enabled') ? { enabled: params.enabled === true } : {}),
|
|
617
|
+
});
|
|
618
|
+
return buildToolResult(projectToolManagedWorldResponse(payload, { accountId: context.accountId }));
|
|
619
|
+
},
|
|
620
|
+
},
|
|
505
621
|
{
|
|
506
622
|
name: 'claworld_request_chat',
|
|
507
623
|
label: 'Claworld Request Chat',
|
|
@@ -76,7 +76,7 @@ function projectParticipantContextField(field = null) {
|
|
|
76
76
|
|
|
77
77
|
function projectWorldRole(worldRole, fallback = null) {
|
|
78
78
|
const normalized = normalizeText(worldRole, fallback);
|
|
79
|
-
return ['owner', '
|
|
79
|
+
return ['owner', 'member'].includes(normalized) ? normalized : fallback;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
function projectWorldStats(stats = null) {
|
|
@@ -22,6 +22,8 @@ export const CLAWORLD_WORLD_TOOL_NAMES = Object.freeze([
|
|
|
22
22
|
|
|
23
23
|
export const CLAWORLD_WORLD_ADMIN_PUBLIC_TOOL_NAMES = Object.freeze([
|
|
24
24
|
'claworld_create_world',
|
|
25
|
+
'claworld_list_owned_worlds',
|
|
26
|
+
'claworld_manage_world',
|
|
25
27
|
]);
|
|
26
28
|
|
|
27
29
|
export const CLAWORLD_COMPATIBILITY_TOOL_NAMES = Object.freeze([
|
|
@@ -35,8 +37,6 @@ export const CLAWORLD_RETIRED_PUBLIC_TOOL_NAMES = Object.freeze([
|
|
|
35
37
|
'claworld_accept_friend_request',
|
|
36
38
|
'claworld_reject_friend_request',
|
|
37
39
|
'claworld_broadcast_world',
|
|
38
|
-
'claworld_list_owned_worlds',
|
|
39
|
-
'claworld_manage_world',
|
|
40
40
|
'claworld_resolve_agent',
|
|
41
41
|
]);
|
|
42
42
|
|
|
@@ -76,7 +76,7 @@ function normalizeWorldStats(stats = null) {
|
|
|
76
76
|
|
|
77
77
|
function normalizeWorldRole(worldRole, fallback = null) {
|
|
78
78
|
const normalized = normalizeText(worldRole, fallback);
|
|
79
|
-
return ['owner', '
|
|
79
|
+
return ['owner', 'member'].includes(normalized) ? normalized : fallback;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
function normalizeManagedWorld(payload = {}) {
|
|
@@ -2,283 +2,37 @@ export const DEFAULT_WORLD_MANIFESTS = Object.freeze([
|
|
|
2
2
|
{
|
|
3
3
|
worldId: 'dating-demo-world',
|
|
4
4
|
displayName: 'Dating Demo World',
|
|
5
|
-
summary: '
|
|
5
|
+
summary: 'A lightweight social world for proving world-scoped join, review, and chat request flow.',
|
|
6
6
|
description:
|
|
7
|
-
'A lightweight social world
|
|
7
|
+
'A lightweight social world where agents compare fit briefly before deciding whether to continue beyond agent chat.',
|
|
8
8
|
category: 'social',
|
|
9
9
|
lifecycle: 'prototype',
|
|
10
10
|
tags: ['dating', 'matching', 'a2a'],
|
|
11
|
-
|
|
12
|
-
'
|
|
13
|
-
prohibitedRules:
|
|
14
|
-
'Do not pressure, harass, manipulate, or ask for unsafe personal details. Do not continue pushing once the other side clearly signals discomfort or disinterest.',
|
|
15
|
-
ratingRules:
|
|
16
|
-
'When the interaction ends, each agent should rate the other side from 1 to 10 based on mutual fit, conversational quality, and respect for the world rules.',
|
|
17
|
-
roles: [
|
|
18
|
-
{
|
|
19
|
-
roleId: 'seeker',
|
|
20
|
-
label: 'Seeker',
|
|
21
|
-
objective: 'Find a compatible person and decide whether to raise hand.',
|
|
22
|
-
promptSummary: 'Represent your human owner faithfully and optimize for respectful fit.',
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
roleId: 'candidate',
|
|
26
|
-
label: 'Candidate',
|
|
27
|
-
objective: 'Assess alignment and decide whether to continue beyond agent-only chat.',
|
|
28
|
-
promptSummary: 'Share enough context to evaluate fit while staying concise and safe.',
|
|
29
|
-
},
|
|
30
|
-
],
|
|
31
|
-
joinSchema: {
|
|
32
|
-
requiredFields: [
|
|
33
|
-
{
|
|
34
|
-
fieldId: 'headline',
|
|
35
|
-
label: 'Headline',
|
|
36
|
-
description: 'One-line self introduction shown during initial discovery.',
|
|
37
|
-
examples: ['Shanghai-based product lead who likes trail running'],
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
fieldId: 'intent',
|
|
41
|
-
label: 'Intent',
|
|
42
|
-
description: 'What kind of connection the user is open to.',
|
|
43
|
-
examples: ['serious relationship', 'new friends first'],
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
fieldId: 'location',
|
|
47
|
-
label: 'Location',
|
|
48
|
-
description: 'Current city or region used for basic filtering.',
|
|
49
|
-
examples: ['Shanghai'],
|
|
50
|
-
},
|
|
51
|
-
],
|
|
52
|
-
optionalFields: [
|
|
53
|
-
{
|
|
54
|
-
fieldId: 'interests',
|
|
55
|
-
label: 'Interests',
|
|
56
|
-
type: 'string[]',
|
|
57
|
-
description: 'Interests or hobbies used for match prompts.',
|
|
58
|
-
examples: ['running', 'indie films', 'cats'],
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
fieldId: 'conversationStyle',
|
|
62
|
-
label: 'Conversation Style',
|
|
63
|
-
description: 'Tone preference for the agent during A2A chat.',
|
|
64
|
-
examples: ['playful but direct'],
|
|
65
|
-
},
|
|
66
|
-
],
|
|
67
|
-
hints: [
|
|
68
|
-
'Keep profile fields concrete so both matching and prompt injection stay stable.',
|
|
69
|
-
'World-level rules should decide when two agents have effectively reached a human handoff threshold.',
|
|
70
|
-
],
|
|
71
|
-
},
|
|
72
|
-
searchSchema: {
|
|
73
|
-
mode: 'profile_overlap_search',
|
|
74
|
-
inputFieldIds: ['intent', 'location', 'interests'],
|
|
75
|
-
summary:
|
|
76
|
-
'Compatibility-only manual search over active online members by intent, location, and shared interests. Candidate feed review is the canonical path before request_chat.',
|
|
77
|
-
hints: [
|
|
78
|
-
'Search defaults to the viewer membership profile when no explicit query is provided.',
|
|
79
|
-
'Only online members with an active world membership are returned.',
|
|
80
|
-
],
|
|
81
|
-
},
|
|
82
|
-
matching: {
|
|
83
|
-
mode: 'scored_push',
|
|
84
|
-
cadence: 'periodic',
|
|
85
|
-
strategySummary:
|
|
86
|
-
'Score active online memberships by intent, location, and interests, deliver candidate summaries first, and route live contact through request_chat after review.',
|
|
87
|
-
candidateSources: ['active_memberships_online'],
|
|
88
|
-
},
|
|
89
|
-
conversationTemplate: {
|
|
90
|
-
mode: 'a2a',
|
|
91
|
-
worldRules: {
|
|
92
|
-
openingText: 'You are in the dating demo world. Clarify fit quickly and stop once both sides can decide whether to continue.',
|
|
93
|
-
turnMessageRules: [{ id: 'nudge-2', atTurn: 2, templateRef: 'world.turn.nudge' }],
|
|
94
|
-
convergence: {
|
|
95
|
-
whenRemainingTurnsLTE: 1,
|
|
96
|
-
text: 'Focus on unresolved blockers and whether both sides have enough information to decide next steps.',
|
|
97
|
-
},
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
resultContract: {
|
|
101
|
-
schemaId: 'dating-demo-world.result.v1',
|
|
102
|
-
outputs: ['match_score', 'recommendation', 'risks', 'evidence'],
|
|
103
|
-
successCriteria: ['both agents can summarize fit', 'next step is explicit'],
|
|
104
|
-
exampleSignals: {
|
|
105
|
-
intentSignals: [{ id: 'intent-1', type: 'intent_match', score: 0.8, summary: 'Stated intent aligns.' }],
|
|
106
|
-
conversationSignals: [
|
|
107
|
-
{ id: 'conv-1', type: 'next_step_ready', score: 0.7, summary: 'One side is ready to stop and move forward.' },
|
|
108
|
-
{ id: 'conv-2', type: 'next_step_ready', score: 0.7, summary: 'The other side is also ready to stop.' },
|
|
109
|
-
],
|
|
110
|
-
agentSignals: [{ id: 'agent-1', type: 'safety', risk: 0.1, summary: 'No major risk surfaced.' }],
|
|
111
|
-
},
|
|
112
|
-
},
|
|
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.\n结果要求:Rate the interaction from 1 to 10 based on fit, clarity, and respect.',
|
|
113
13
|
},
|
|
114
14
|
{
|
|
115
15
|
worldId: 'skill-handoff-world',
|
|
116
16
|
displayName: 'Skill Handoff World',
|
|
117
|
-
summary: '
|
|
17
|
+
summary: 'A small scoped skill-handoff world for proving service-fit review before direct contact.',
|
|
118
18
|
description:
|
|
119
|
-
'A service marketplace world where agents
|
|
19
|
+
'A service marketplace world where agents quickly screen delivery fit before moving to direct human contact.',
|
|
120
20
|
category: 'marketplace',
|
|
121
21
|
lifecycle: 'prototype',
|
|
122
22
|
tags: ['skills', 'services', 'matching'],
|
|
123
|
-
|
|
124
|
-
'
|
|
125
|
-
prohibitedRules:
|
|
126
|
-
'Do not misrepresent capabilities, hide obvious delivery blockers, or pressure the other side into a commitment without clear scope alignment.',
|
|
127
|
-
ratingRules:
|
|
128
|
-
'At the end of the exchange, each agent should rate the other side from 1 to 10 based on scope clarity, responsiveness, and confidence in a productive handoff.',
|
|
129
|
-
roles: [
|
|
130
|
-
{
|
|
131
|
-
roleId: 'buyer',
|
|
132
|
-
label: 'Buyer',
|
|
133
|
-
objective: 'Describe a task crisply enough for rapid screening.',
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
roleId: 'seller',
|
|
137
|
-
label: 'Seller',
|
|
138
|
-
objective: 'Assess fit, delivery scope, and whether to continue to a human handoff.',
|
|
139
|
-
},
|
|
140
|
-
],
|
|
141
|
-
joinSchema: {
|
|
142
|
-
requiredFields: [
|
|
143
|
-
{
|
|
144
|
-
fieldId: 'headline',
|
|
145
|
-
label: 'Headline',
|
|
146
|
-
description: 'What the user can buy or sell in one line.',
|
|
147
|
-
},
|
|
148
|
-
{
|
|
149
|
-
fieldId: 'capabilities',
|
|
150
|
-
label: 'Capabilities',
|
|
151
|
-
type: 'string[]',
|
|
152
|
-
description: 'Skills or request categories used during matching.',
|
|
153
|
-
examples: ['typescript', 'prompt design', 'landing page'],
|
|
154
|
-
},
|
|
155
|
-
],
|
|
156
|
-
optionalFields: [
|
|
157
|
-
{
|
|
158
|
-
fieldId: 'budgetBand',
|
|
159
|
-
label: 'Budget Band',
|
|
160
|
-
description: 'Optional budget or rate band for screening.',
|
|
161
|
-
examples: ['200-500 USD', '50 USD/hour'],
|
|
162
|
-
},
|
|
163
|
-
],
|
|
164
|
-
hints: ['Use this world to prove supply-demand matching before implementing transaction settlement.'],
|
|
165
|
-
},
|
|
166
|
-
searchSchema: {
|
|
167
|
-
mode: 'capability_overlap_search',
|
|
168
|
-
inputFieldIds: ['capabilities', 'budgetBand'],
|
|
169
|
-
summary:
|
|
170
|
-
'Compatibility-only manual search over active online members by capability overlap and optional budget fit. Candidate feed review is the canonical path before request_chat.',
|
|
171
|
-
},
|
|
172
|
-
matching: {
|
|
173
|
-
mode: 'intent_filter',
|
|
174
|
-
cadence: 'on_demand',
|
|
175
|
-
strategySummary:
|
|
176
|
-
'Filter active online world members by capability overlap, deliver candidate summaries first, and let members request_chat before negotiating fit in a short session.',
|
|
177
|
-
candidateSources: ['active_memberships_online'],
|
|
178
|
-
},
|
|
179
|
-
conversationTemplate: {
|
|
180
|
-
mode: 'a2a',
|
|
181
|
-
worldRules: {
|
|
182
|
-
openingText: 'Clarify scope, constraints, and whether a human handoff is justified.',
|
|
183
|
-
},
|
|
184
|
-
},
|
|
185
|
-
resultContract: {
|
|
186
|
-
schemaId: 'skill-handoff-world.result.v1',
|
|
187
|
-
outputs: ['recommendation', 'risks', 'evidence'],
|
|
188
|
-
successCriteria: ['scope is summarized', 'handoff recommendation is explicit'],
|
|
189
|
-
exampleSignals: {
|
|
190
|
-
intentSignals: [{ id: 'intent-1', type: 'intent_match', score: 0.65, summary: 'Capabilities appear relevant.' }],
|
|
191
|
-
conversationSignals: [{ id: 'conv-1', type: 'human_handoff_ready', score: 0.6, summary: 'Seller is ready for human handoff.' }],
|
|
192
|
-
agentSignals: [{ id: 'agent-1', type: 'scope_risk', risk: 0.2, summary: 'A few requirements remain vague.' }],
|
|
193
|
-
},
|
|
194
|
-
},
|
|
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.\n结果要求:Rate the exchange from 1 to 10 based on scope clarity and handoff confidence.',
|
|
195
25
|
},
|
|
196
26
|
{
|
|
197
27
|
worldId: 'job-match-world',
|
|
198
28
|
displayName: 'Job Match World',
|
|
199
|
-
summary: '
|
|
29
|
+
summary: 'A hiring-fit world optimized for profile completeness, matching, and concise A2A screening.',
|
|
200
30
|
description:
|
|
201
|
-
'A recruiting world for agent-assisted screening where
|
|
31
|
+
'A recruiting world for agent-assisted screening where both sides validate fit before escalating to direct human contact.',
|
|
202
32
|
category: 'recruiting',
|
|
203
33
|
lifecycle: 'prototype',
|
|
204
34
|
tags: ['jobs', 'recruiting', 'screening'],
|
|
205
|
-
|
|
206
|
-
'
|
|
207
|
-
prohibitedRules:
|
|
208
|
-
'Do not fabricate experience, compensation expectations, or hiring authority. Do not request sensitive personal data unrelated to the role fit conversation.',
|
|
209
|
-
ratingRules:
|
|
210
|
-
'When the interaction ends, each agent should rate the other side from 1 to 10 based on role fit, signal quality, and likelihood that a human follow-up is worthwhile.',
|
|
211
|
-
roles: [
|
|
212
|
-
{
|
|
213
|
-
roleId: 'candidate',
|
|
214
|
-
label: 'Candidate',
|
|
215
|
-
objective: 'Surface fit and blockers quickly.',
|
|
216
|
-
},
|
|
217
|
-
{
|
|
218
|
-
roleId: 'recruiter',
|
|
219
|
-
label: 'Recruiter',
|
|
220
|
-
objective: 'Determine whether to escalate to human contact.',
|
|
221
|
-
},
|
|
222
|
-
],
|
|
223
|
-
joinSchema: {
|
|
224
|
-
requiredFields: [
|
|
225
|
-
{
|
|
226
|
-
fieldId: 'headline',
|
|
227
|
-
label: 'Headline',
|
|
228
|
-
description: 'Current role or target role.',
|
|
229
|
-
},
|
|
230
|
-
{
|
|
231
|
-
fieldId: 'experienceSummary',
|
|
232
|
-
label: 'Experience Summary',
|
|
233
|
-
description: 'Condensed summary of experience relevant to matching.',
|
|
234
|
-
},
|
|
235
|
-
{
|
|
236
|
-
fieldId: 'targetRole',
|
|
237
|
-
label: 'Target Role',
|
|
238
|
-
description: 'Role or hiring need that anchors matching.',
|
|
239
|
-
},
|
|
240
|
-
],
|
|
241
|
-
optionalFields: [
|
|
242
|
-
{
|
|
243
|
-
fieldId: 'location',
|
|
244
|
-
label: 'Location',
|
|
245
|
-
},
|
|
246
|
-
{
|
|
247
|
-
fieldId: 'workMode',
|
|
248
|
-
label: 'Work Mode',
|
|
249
|
-
description: 'Onsite, hybrid, or remote.',
|
|
250
|
-
},
|
|
251
|
-
],
|
|
252
|
-
hints: ['This world should eventually integrate search/browse, but the current shell only defines the contract.'],
|
|
253
|
-
},
|
|
254
|
-
searchSchema: {
|
|
255
|
-
mode: 'profile_overlap_search',
|
|
256
|
-
inputFieldIds: ['targetRole', 'location', 'workMode'],
|
|
257
|
-
summary:
|
|
258
|
-
'Compatibility-only manual search over active online members by role fit, location, and work mode. Candidate feed review is the canonical path before request_chat.',
|
|
259
|
-
},
|
|
260
|
-
matching: {
|
|
261
|
-
mode: 'profile_overlap',
|
|
262
|
-
cadence: 'periodic',
|
|
263
|
-
strategySummary:
|
|
264
|
-
'Use active online memberships plus target role, experience summary, and location/work mode as the first-pass scoring surface, deliver candidate summaries, and route contact through request_chat after review.',
|
|
265
|
-
candidateSources: ['active_memberships_online'],
|
|
266
|
-
},
|
|
267
|
-
conversationTemplate: {
|
|
268
|
-
mode: 'a2a',
|
|
269
|
-
worldRules: {
|
|
270
|
-
openingText: 'Focus on role fit, constraints, and whether both sides should move to a human conversation.',
|
|
271
|
-
},
|
|
272
|
-
},
|
|
273
|
-
resultContract: {
|
|
274
|
-
schemaId: 'job-match-world.result.v1',
|
|
275
|
-
outputs: ['match_score', 'recommendation', 'risks', 'evidence'],
|
|
276
|
-
successCriteria: ['role fit is explicit', 'human next step is explicit'],
|
|
277
|
-
exampleSignals: {
|
|
278
|
-
intentSignals: [{ id: 'intent-1', type: 'role_fit', score: 0.7, summary: 'Role expectations line up.' }],
|
|
279
|
-
conversationSignals: [{ id: 'conv-1', type: 'next_step_ready', score: 0.55, summary: 'Both sides are ready to stop and proceed.' }],
|
|
280
|
-
agentSignals: [{ id: 'agent-1', type: 'timeline_risk', risk: 0.15, summary: 'Availability still needs confirmation.' }],
|
|
281
|
-
},
|
|
282
|
-
},
|
|
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.\n结果要求:Rate the interaction from 1 to 10 based on fit, clarity, and follow-up likelihood.',
|
|
283
37
|
},
|
|
284
38
|
]);
|
|
@@ -77,16 +77,6 @@ function normalizeField(field = {}, index = 0, { required = false } = {}) {
|
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
function normalizeRole(role = {}, index = 0) {
|
|
81
|
-
const roleId = normalizeText(role.roleId || role.id, `role_${index + 1}`);
|
|
82
|
-
return {
|
|
83
|
-
roleId,
|
|
84
|
-
label: normalizeText(role.label, roleId),
|
|
85
|
-
objective: normalizeText(role.objective, null),
|
|
86
|
-
promptSummary: normalizeText(role.promptSummary, null),
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
80
|
function normalizeExampleSignals(exampleSignals = {}) {
|
|
91
81
|
const normalizeGroup = (key) => (Array.isArray(exampleSignals[key]) ? exampleSignals[key] : []);
|
|
92
82
|
return {
|
|
@@ -207,7 +197,6 @@ export function normalizeWorldManifest(manifest = {}, index = 0) {
|
|
|
207
197
|
ratingRules: normalizedRatingRules,
|
|
208
198
|
}),
|
|
209
199
|
creatorAgentId: normalizeText(manifest.creatorAgentId, null),
|
|
210
|
-
adminAgentIds: normalizeStringList(manifest.adminAgentIds),
|
|
211
200
|
eligibility: normalizeWorldEligibility(manifest.eligibility, 'active'),
|
|
212
201
|
broadcast: normalizeBroadcastConfig(manifest.broadcast, {
|
|
213
202
|
enabled: manifest.broadcast?.enabled === true,
|
|
@@ -224,9 +213,6 @@ export function normalizeWorldManifest(manifest = {}, index = 0) {
|
|
|
224
213
|
: 0,
|
|
225
214
|
}
|
|
226
215
|
: { totalConversationCount: 0 },
|
|
227
|
-
roles: Array.isArray(manifest.roles)
|
|
228
|
-
? manifest.roles.map((role, roleIndex) => normalizeRole(role, roleIndex))
|
|
229
|
-
: [],
|
|
230
216
|
joinSchema,
|
|
231
217
|
searchSchema,
|
|
232
218
|
matching: {
|
|
@@ -174,7 +174,6 @@ function normalizeWorldDetail(payload = {}) {
|
|
|
174
174
|
interactionRules: normalizeText(payload.interactionRules, null),
|
|
175
175
|
prohibitedRules: normalizeText(payload.prohibitedRules, null),
|
|
176
176
|
ratingRules: normalizeText(payload.ratingRules, null),
|
|
177
|
-
adminAgentIds: normalizeStringList(payload.adminAgentIds),
|
|
178
177
|
eligibility: normalizeText(payload.eligibility, 'active'),
|
|
179
178
|
broadcast: normalizeBroadcastConfig(payload.broadcast),
|
|
180
179
|
requiredFields,
|
|
@@ -237,7 +236,6 @@ function normalizeWorldDetail(payload = {}) {
|
|
|
237
236
|
interactionRules: normalizeText(payload.interactionRules || world.interactionRules, null),
|
|
238
237
|
prohibitedRules: normalizeText(payload.prohibitedRules || world.prohibitedRules, null),
|
|
239
238
|
ratingRules: normalizeText(payload.ratingRules || world.ratingRules, null),
|
|
240
|
-
adminAgentIds: normalizeStringList(payload.adminAgentIds || world.adminAgentIds),
|
|
241
239
|
eligibility: normalizeText(payload.eligibility || world.eligibility, 'active'),
|
|
242
240
|
broadcast: normalizeBroadcastConfig(payload.broadcast || world.broadcast),
|
|
243
241
|
requiredFields,
|
|
@@ -132,6 +132,7 @@ export function createClaworldProductShell({
|
|
|
132
132
|
'GET /v1/chat-requests',
|
|
133
133
|
'PUT /v1/chat-requests/approval-policy',
|
|
134
134
|
'POST /v1/chat-requests/:chatRequestId/accept',
|
|
135
|
+
'POST /v1/chat-requests/:chatRequestId/reject',
|
|
135
136
|
'GET /v1/worlds',
|
|
136
137
|
'GET /v1/worlds/:worldId',
|
|
137
138
|
'POST /v1/worlds',
|
|
@@ -81,9 +81,13 @@ export function createMembershipService({ worldService, store = null } = {}) {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
return {
|
|
84
|
-
evaluateJoin({ worldId, participantContextText = null } = {}) {
|
|
84
|
+
evaluateJoin({ worldId, participantContextText = null, profile = null, profileSnapshot = null } = {}) {
|
|
85
85
|
const world = worldService.requireWorld(worldId);
|
|
86
|
-
const normalizedParticipantContextText =
|
|
86
|
+
const normalizedParticipantContextText = resolveNormalizedParticipantContextText({
|
|
87
|
+
world,
|
|
88
|
+
participantContextText,
|
|
89
|
+
profileSnapshot: profileSnapshot || profile,
|
|
90
|
+
});
|
|
87
91
|
const accepted = Boolean(normalizedParticipantContextText);
|
|
88
92
|
|
|
89
93
|
return {
|
|
@@ -132,7 +136,7 @@ export function createMembershipService({ worldService, store = null } = {}) {
|
|
|
132
136
|
};
|
|
133
137
|
},
|
|
134
138
|
|
|
135
|
-
async createMembership({ worldId, agentId, participantContextText } = {}) {
|
|
139
|
+
async createMembership({ worldId, agentId, participantContextText, profile = null, profileSnapshot = null } = {}) {
|
|
136
140
|
const world = worldService.requireWorld(worldId);
|
|
137
141
|
const membershipStore = assertStore();
|
|
138
142
|
const normalizedAgentId = normalizeAgentId(agentId);
|
|
@@ -145,6 +149,7 @@ export function createMembershipService({ worldService, store = null } = {}) {
|
|
|
145
149
|
world,
|
|
146
150
|
agent,
|
|
147
151
|
participantContextText,
|
|
152
|
+
profileSnapshot: profileSnapshot || profile,
|
|
148
153
|
});
|
|
149
154
|
if (!normalizedParticipantContextText) {
|
|
150
155
|
throw createInvalidJoinRequestError(
|
|
@@ -166,7 +171,7 @@ export function createMembershipService({ worldService, store = null } = {}) {
|
|
|
166
171
|
worldId,
|
|
167
172
|
agentId: normalizedAgentId,
|
|
168
173
|
status: 'joined',
|
|
169
|
-
profileSnapshot: normalizeProfileSnapshot(
|
|
174
|
+
profileSnapshot: normalizeProfileSnapshot(profileSnapshot || profile, normalizedParticipantContextText),
|
|
170
175
|
participantContextText: normalizedParticipantContextText,
|
|
171
176
|
});
|
|
172
177
|
|
|
@@ -177,6 +182,8 @@ export function createMembershipService({ worldService, store = null } = {}) {
|
|
|
177
182
|
worldId,
|
|
178
183
|
agentId,
|
|
179
184
|
participantContextText,
|
|
185
|
+
profile = null,
|
|
186
|
+
profileSnapshot = null,
|
|
180
187
|
} = {}) {
|
|
181
188
|
const world = worldService.requireWorld(worldId);
|
|
182
189
|
const membershipStore = assertStore();
|
|
@@ -191,6 +198,7 @@ export function createMembershipService({ worldService, store = null } = {}) {
|
|
|
191
198
|
world,
|
|
192
199
|
agent,
|
|
193
200
|
participantContextText,
|
|
201
|
+
profileSnapshot: profileSnapshot || profile,
|
|
194
202
|
});
|
|
195
203
|
if (!normalizedParticipantContextText) {
|
|
196
204
|
throw createInvalidJoinRequestError(
|
|
@@ -207,14 +215,24 @@ export function createMembershipService({ worldService, store = null } = {}) {
|
|
|
207
215
|
const membership = existingMembership
|
|
208
216
|
? await membershipStore.updateMembership(existingMembership.membershipId, {
|
|
209
217
|
status: 'active',
|
|
210
|
-
profileSnapshot: normalizeProfileSnapshot(
|
|
218
|
+
profileSnapshot: normalizeProfileSnapshot(
|
|
219
|
+
{
|
|
220
|
+
...(existingMembership.profileSnapshot && typeof existingMembership.profileSnapshot === 'object'
|
|
221
|
+
? existingMembership.profileSnapshot
|
|
222
|
+
: {}),
|
|
223
|
+
...((profileSnapshot || profile) && typeof (profileSnapshot || profile) === 'object' && !Array.isArray(profileSnapshot || profile)
|
|
224
|
+
? (profileSnapshot || profile)
|
|
225
|
+
: {}),
|
|
226
|
+
},
|
|
227
|
+
normalizedParticipantContextText,
|
|
228
|
+
),
|
|
211
229
|
participantContextText: normalizedParticipantContextText,
|
|
212
230
|
})
|
|
213
231
|
: await membershipStore.createMembership({
|
|
214
232
|
worldId: world.worldId,
|
|
215
233
|
agentId: normalizedAgentId,
|
|
216
234
|
status: 'active',
|
|
217
|
-
profileSnapshot: normalizeProfileSnapshot(
|
|
235
|
+
profileSnapshot: normalizeProfileSnapshot(profileSnapshot || profile, normalizedParticipantContextText),
|
|
218
236
|
participantContextText: normalizedParticipantContextText,
|
|
219
237
|
});
|
|
220
238
|
|
|
@@ -104,4 +104,24 @@ export function registerChatRequestRoutes(app, { chatRequestService, store }) {
|
|
|
104
104
|
sendChatRequestError(res, error);
|
|
105
105
|
}
|
|
106
106
|
});
|
|
107
|
+
|
|
108
|
+
app.post('/v1/chat-requests/:chatRequestId/reject', async (req, res) => {
|
|
109
|
+
const authAgent = resolveAuthenticatedAgentId({
|
|
110
|
+
store,
|
|
111
|
+
req,
|
|
112
|
+
providedAgentId: req.body?.actorAgentId,
|
|
113
|
+
fieldName: 'actorAgentId',
|
|
114
|
+
});
|
|
115
|
+
if (!authAgent.ok) return res.status(authAgent.status).json(authAgent.body);
|
|
116
|
+
if (!authAgent.agentId) return sendMissingAgentIdentity(res);
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const result = await chatRequestService.rejectChatRequest(req.params.chatRequestId, {
|
|
120
|
+
actorAgentId: authAgent.agentId,
|
|
121
|
+
});
|
|
122
|
+
res.json(result);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
sendChatRequestError(res, error);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
107
127
|
}
|
|
@@ -534,5 +534,27 @@ export function createChatRequestService({
|
|
|
534
534
|
nextAction: resolveAcceptNextAction(kickoff),
|
|
535
535
|
};
|
|
536
536
|
},
|
|
537
|
+
|
|
538
|
+
async rejectChatRequest(chatRequestId, { actorAgentId } = {}) {
|
|
539
|
+
requireAgent(actorAgentId);
|
|
540
|
+
const normalizedChatRequestId = normalizeText(chatRequestId, null);
|
|
541
|
+
if (!normalizedChatRequestId) {
|
|
542
|
+
throw createInvalidChatRequestError('chat_request_id_required', 'chatRequestId is required');
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const result = await assertRelay('rejectChatRequest').rejectChatRequest(normalizedChatRequestId, {
|
|
546
|
+
actorAgentId,
|
|
547
|
+
});
|
|
548
|
+
if (result.status < 200 || result.status >= 300) {
|
|
549
|
+
throw createRelayResponseError(result, 'chat_request_reject_failed');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return {
|
|
553
|
+
status: 'rejected',
|
|
554
|
+
verdict: 'manual_reject',
|
|
555
|
+
chatRequest: projectChatRequest(result.body || {}, actorAgentId),
|
|
556
|
+
nextAction: 'request_rejected_by_peer',
|
|
557
|
+
};
|
|
558
|
+
},
|
|
537
559
|
};
|
|
538
560
|
}
|
|
@@ -12,36 +12,6 @@ function normalizeBoolean(value, fallback = false) {
|
|
|
12
12
|
return fallback;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
function normalizeStringList(values = []) {
|
|
16
|
-
if (!Array.isArray(values)) return [];
|
|
17
|
-
return [...new Set(values.map((value) => normalizeText(value, null)).filter(Boolean))];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function normalizePositiveInteger(value, fallback = null) {
|
|
21
|
-
const parsed = Number(value);
|
|
22
|
-
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
23
|
-
return Math.max(1, Math.trunc(parsed));
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function normalizeWorldEligibility(value, fallback = 'active') {
|
|
27
|
-
const normalized = normalizeText(value, fallback);
|
|
28
|
-
if (normalized === 'joined') return 'joined';
|
|
29
|
-
return 'active';
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function normalizeBroadcastAudience(value, fallback = 'members') {
|
|
33
|
-
const normalized = normalizeText(value, fallback);
|
|
34
|
-
if (normalized === 'admins') return 'admins';
|
|
35
|
-
if (normalized === 'admins_and_owner') return 'admins_and_owner';
|
|
36
|
-
return 'members';
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function normalizeBroadcastReplyPolicy(value, fallback = 'zero') {
|
|
40
|
-
const normalized = normalizeText(value, fallback);
|
|
41
|
-
if (normalized === 'at_most_one') return 'at_most_one';
|
|
42
|
-
return 'zero';
|
|
43
|
-
}
|
|
44
|
-
|
|
45
15
|
function summarizeWorldContextText(worldContextText, fallback = null) {
|
|
46
16
|
const normalized = normalizeText(worldContextText, null);
|
|
47
17
|
if (!normalized) return fallback;
|
|
@@ -78,7 +48,7 @@ function createWorldActionNotAllowedError({
|
|
|
78
48
|
worldId,
|
|
79
49
|
agentId,
|
|
80
50
|
action,
|
|
81
|
-
actorRole =
|
|
51
|
+
actorRole = null,
|
|
82
52
|
allowedRoles = [],
|
|
83
53
|
} = {}) {
|
|
84
54
|
const error = new Error(`world_action_not_allowed:${action}:${worldId}:${agentId}`);
|
|
@@ -87,7 +57,6 @@ function createWorldActionNotAllowedError({
|
|
|
87
57
|
const messageByAction = {
|
|
88
58
|
[WORLD_ACTIONS.VIEW_MANAGEMENT]: 'agent does not have permission to access world management',
|
|
89
59
|
[WORLD_ACTIONS.MANAGE_WORLD]: 'agent does not have permission to manage this world',
|
|
90
|
-
[WORLD_ACTIONS.MANAGE_WORLD_ROLES]: 'only the world owner can manage world admins',
|
|
91
60
|
[WORLD_ACTIONS.CHANGE_ENABLED_STATE]: 'only the world owner can enable or disable this world',
|
|
92
61
|
};
|
|
93
62
|
error.responseBody = {
|
|
@@ -125,7 +94,7 @@ function normalizeAgentId(agentId) {
|
|
|
125
94
|
|
|
126
95
|
function normalizeWorldRole(worldRole, fallback = null) {
|
|
127
96
|
const normalized = normalizeText(worldRole, fallback);
|
|
128
|
-
return [WORLD_ROLES.OWNER, WORLD_ROLES.
|
|
97
|
+
return [WORLD_ROLES.OWNER, WORLD_ROLES.MEMBER].includes(normalized) ? normalized : fallback;
|
|
129
98
|
}
|
|
130
99
|
|
|
131
100
|
function buildDefaultEntryProfileField() {
|
|
@@ -193,42 +162,36 @@ function buildWorldRecord({
|
|
|
193
162
|
creatorAgentId,
|
|
194
163
|
displayName,
|
|
195
164
|
summary,
|
|
196
|
-
description,
|
|
197
|
-
interactionRules,
|
|
198
|
-
prohibitedRules,
|
|
199
|
-
ratingRules,
|
|
200
165
|
worldContextText = null,
|
|
201
166
|
enabled = false,
|
|
202
167
|
status = null,
|
|
203
|
-
schemaVersion = 1,
|
|
204
168
|
existingMetrics = null,
|
|
205
|
-
existingConversationTemplate = null,
|
|
206
169
|
} = {}) {
|
|
207
170
|
const resolvedStatus = status || (enabled ? 'enabled' : 'draft');
|
|
208
171
|
const participantContextField = buildDefaultEntryProfileField();
|
|
172
|
+
const resolvedWorldContextText = buildWorldContextText({
|
|
173
|
+
worldId,
|
|
174
|
+
displayName,
|
|
175
|
+
summary,
|
|
176
|
+
worldContextText,
|
|
177
|
+
interactionRules: null,
|
|
178
|
+
prohibitedRules: null,
|
|
179
|
+
ratingRules: null,
|
|
180
|
+
});
|
|
209
181
|
|
|
210
182
|
return {
|
|
211
183
|
worldId,
|
|
212
184
|
slug: slugify(displayName, worldId),
|
|
213
185
|
displayName,
|
|
214
186
|
summary,
|
|
215
|
-
description,
|
|
187
|
+
description: resolvedWorldContextText,
|
|
216
188
|
category: 'ugc',
|
|
217
189
|
lifecycle: 'creator_managed',
|
|
218
190
|
tags: ['ugc', 'creator-managed'],
|
|
219
|
-
interactionRules,
|
|
220
|
-
prohibitedRules,
|
|
221
|
-
ratingRules,
|
|
222
|
-
worldContextText:
|
|
223
|
-
worldId,
|
|
224
|
-
displayName,
|
|
225
|
-
summary,
|
|
226
|
-
worldContextText,
|
|
227
|
-
interactionRules,
|
|
228
|
-
prohibitedRules,
|
|
229
|
-
ratingRules,
|
|
230
|
-
}),
|
|
231
|
-
roles: [],
|
|
191
|
+
interactionRules: null,
|
|
192
|
+
prohibitedRules: null,
|
|
193
|
+
ratingRules: null,
|
|
194
|
+
worldContextText: resolvedWorldContextText,
|
|
232
195
|
joinSchema: {
|
|
233
196
|
requiredFields: [participantContextField],
|
|
234
197
|
optionalFields: [],
|
|
@@ -236,16 +199,13 @@ function buildWorldRecord({
|
|
|
236
199
|
},
|
|
237
200
|
searchSchema: buildSearchSchema(),
|
|
238
201
|
matching: buildMatchingStrategy(),
|
|
239
|
-
conversationTemplate: buildConversationTemplate(
|
|
240
|
-
|
|
241
|
-
}),
|
|
242
|
-
resultContract: buildResultContract(worldId, ratingRules),
|
|
202
|
+
conversationTemplate: buildConversationTemplate(null, null),
|
|
203
|
+
resultContract: buildResultContract(worldId, null),
|
|
243
204
|
meta: {
|
|
244
205
|
status: resolvedStatus === 'enabled' ? 'creator_enabled' : 'creator_draft',
|
|
245
206
|
persistence: 'store',
|
|
246
207
|
},
|
|
247
208
|
creatorAgentId,
|
|
248
|
-
adminAgentIds: [],
|
|
249
209
|
eligibility: 'active',
|
|
250
210
|
broadcast: {
|
|
251
211
|
enabled: false,
|
|
@@ -255,7 +215,7 @@ function buildWorldRecord({
|
|
|
255
215
|
},
|
|
256
216
|
status: resolvedStatus,
|
|
257
217
|
enabled,
|
|
258
|
-
schemaVersion,
|
|
218
|
+
schemaVersion: 1,
|
|
259
219
|
metrics: existingMetrics || {
|
|
260
220
|
totalConversationCount: 0,
|
|
261
221
|
},
|
|
@@ -339,29 +299,18 @@ export function createWorldAdminService({ worldService, worldAuthorizationServic
|
|
|
339
299
|
return actorAgentId;
|
|
340
300
|
}
|
|
341
301
|
|
|
342
|
-
|
|
343
|
-
const membershipStore = assertStore();
|
|
344
|
-
const memberships = membershipStore.listMemberships({ worldId });
|
|
345
|
-
for (const membership of memberships) {
|
|
346
|
-
if (!['joined', 'active'].includes(membership.status)) continue;
|
|
347
|
-
await membershipStore.updateMembership(membership.membershipId, {
|
|
348
|
-
status: 'stale_profile',
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function requireWorldAction({ worldId, actorAgentId, action } = {}) {
|
|
302
|
+
function requireWorldOwner({ worldId, actorAgentId } = {}) {
|
|
354
303
|
const authorization = worldAuthorizationService.evaluateWorldAction({
|
|
355
304
|
worldId,
|
|
356
305
|
actorAgentId,
|
|
357
|
-
action,
|
|
306
|
+
action: WORLD_ACTIONS.MANAGE_WORLD,
|
|
358
307
|
includeDisabled: true,
|
|
359
308
|
});
|
|
360
309
|
if (!authorization.allowed) {
|
|
361
310
|
throw createWorldActionNotAllowedError({
|
|
362
311
|
worldId: authorization.world.worldId,
|
|
363
312
|
agentId: actorAgentId,
|
|
364
|
-
action,
|
|
313
|
+
action: WORLD_ACTIONS.MANAGE_WORLD,
|
|
365
314
|
actorRole: authorization.worldRole,
|
|
366
315
|
allowedRoles: authorization.allowedRoles,
|
|
367
316
|
});
|
|
@@ -413,8 +362,13 @@ export function createWorldAdminService({ worldService, worldAuthorizationServic
|
|
|
413
362
|
listManagedWorlds({ actorAgentId, creatorAgentId, includeDisabled = true } = {}) {
|
|
414
363
|
const storeBacked = assertStore();
|
|
415
364
|
const resolvedActorAgentId = assertActorAgent(actorAgentId || creatorAgentId);
|
|
416
|
-
return
|
|
417
|
-
.
|
|
365
|
+
return worldService
|
|
366
|
+
.listOwnedWorlds({ creatorAgentId: resolvedActorAgentId, includeDisabled })
|
|
367
|
+
.map((world) => worldAuthorizationService.resolveWorldActorContext({
|
|
368
|
+
worldId: world.worldId,
|
|
369
|
+
actorAgentId: resolvedActorAgentId,
|
|
370
|
+
includeDisabled,
|
|
371
|
+
}))
|
|
418
372
|
.map((context) => projectManagedWorldSummary(storeBacked, context.world, {
|
|
419
373
|
worldRole: context.worldRole,
|
|
420
374
|
}));
|
|
@@ -425,10 +379,9 @@ export function createWorldAdminService({ worldService, worldAuthorizationServic
|
|
|
425
379
|
getManagedWorld({ actorAgentId, creatorAgentId, worldId } = {}) {
|
|
426
380
|
const storeBacked = assertStore();
|
|
427
381
|
const resolvedActorAgentId = assertActorAgent(actorAgentId || creatorAgentId);
|
|
428
|
-
const authorization =
|
|
382
|
+
const authorization = requireWorldOwner({
|
|
429
383
|
worldId,
|
|
430
384
|
actorAgentId: resolvedActorAgentId,
|
|
431
|
-
action: WORLD_ACTIONS.VIEW_MANAGEMENT,
|
|
432
385
|
});
|
|
433
386
|
return projectManagedWorld(storeBacked, authorization.world, {
|
|
434
387
|
worldRole: authorization.worldRole,
|
|
@@ -439,25 +392,10 @@ export function createWorldAdminService({ worldService, worldAuthorizationServic
|
|
|
439
392
|
const resolvedActorAgentId = assertActorAgent(actorAgentId || creatorAgentId);
|
|
440
393
|
const hasChanges = changes && typeof changes === 'object' && !Array.isArray(changes);
|
|
441
394
|
|
|
442
|
-
|
|
395
|
+
const authorization = requireWorldOwner({
|
|
443
396
|
worldId,
|
|
444
397
|
actorAgentId: resolvedActorAgentId,
|
|
445
|
-
action: WORLD_ACTIONS.VIEW_MANAGEMENT,
|
|
446
398
|
});
|
|
447
|
-
if (hasManageWorldChanges(changes)) {
|
|
448
|
-
authorization = requireWorldAction({
|
|
449
|
-
worldId,
|
|
450
|
-
actorAgentId: resolvedActorAgentId,
|
|
451
|
-
action: WORLD_ACTIONS.MANAGE_WORLD,
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
if (enabled != null) {
|
|
455
|
-
authorization = requireWorldAction({
|
|
456
|
-
worldId,
|
|
457
|
-
actorAgentId: resolvedActorAgentId,
|
|
458
|
-
action: WORLD_ACTIONS.CHANGE_ENABLED_STATE,
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
399
|
|
|
462
400
|
const existingWorld = authorization.world;
|
|
463
401
|
if (!hasChanges && enabled == null) {
|
|
@@ -466,7 +404,6 @@ export function createWorldAdminService({ worldService, worldAuthorizationServic
|
|
|
466
404
|
});
|
|
467
405
|
}
|
|
468
406
|
|
|
469
|
-
let nextSchemaVersion = Number(existingWorld.schemaVersion || 1);
|
|
470
407
|
let nextRecord = existingWorld;
|
|
471
408
|
|
|
472
409
|
if (hasChanges) {
|
|
@@ -479,18 +416,12 @@ export function createWorldAdminService({ worldService, worldAuthorizationServic
|
|
|
479
416
|
creatorAgentId: existingWorld.creatorAgentId,
|
|
480
417
|
displayName: nextDisplayName,
|
|
481
418
|
summary: summarizeWorldContextText(nextWorldContextText, nextDisplayName),
|
|
482
|
-
description: nextWorldContextText,
|
|
483
|
-
interactionRules: null,
|
|
484
|
-
prohibitedRules: null,
|
|
485
|
-
ratingRules: null,
|
|
486
419
|
worldContextText: nextWorldContextText,
|
|
487
420
|
enabled: enabled == null ? existingWorld.enabled === true : normalizeBoolean(enabled, false),
|
|
488
421
|
status: enabled == null
|
|
489
422
|
? existingWorld.status
|
|
490
423
|
: (normalizeBoolean(enabled, false) ? 'enabled' : 'disabled'),
|
|
491
|
-
schemaVersion: nextSchemaVersion,
|
|
492
424
|
existingMetrics: existingWorld.metrics || null,
|
|
493
|
-
existingConversationTemplate: existingWorld.conversationTemplate || null,
|
|
494
425
|
});
|
|
495
426
|
} else if (enabled != null) {
|
|
496
427
|
nextRecord = {
|
|
@@ -505,9 +436,6 @@ export function createWorldAdminService({ worldService, worldAuthorizationServic
|
|
|
505
436
|
}
|
|
506
437
|
|
|
507
438
|
const updated = await storeBacked.updateWorldConfig(existingWorld.worldId, nextRecord);
|
|
508
|
-
if (nextSchemaVersion !== Number(existingWorld.schemaVersion || 1)) {
|
|
509
|
-
await markMembershipsStale(existingWorld.worldId);
|
|
510
|
-
}
|
|
511
439
|
const normalizedWorld = worldService.requireWorld(updated.worldId, { includeDisabled: true });
|
|
512
440
|
return projectManagedWorld(storeBacked, normalizedWorld, {
|
|
513
441
|
worldRole: authorization.worldRole,
|
|
@@ -7,13 +7,11 @@ function normalizeText(value, fallback = null) {
|
|
|
7
7
|
export const WORLD_ROLES = Object.freeze({
|
|
8
8
|
OWNER: 'owner',
|
|
9
9
|
MEMBER: 'member',
|
|
10
|
-
NONE: 'none',
|
|
11
10
|
});
|
|
12
11
|
|
|
13
12
|
export const WORLD_ACTIONS = Object.freeze({
|
|
14
13
|
VIEW_MANAGEMENT: 'view_world_management',
|
|
15
14
|
MANAGE_WORLD: 'manage_world',
|
|
16
|
-
MANAGE_WORLD_ROLES: 'manage_world_roles',
|
|
17
15
|
CHANGE_ENABLED_STATE: 'change_world_enabled_state',
|
|
18
16
|
BROADCAST: 'broadcast_world',
|
|
19
17
|
SEARCH: 'search_world',
|
|
@@ -21,10 +19,10 @@ export const WORLD_ACTIONS = Object.freeze({
|
|
|
21
19
|
CREATE_CHAT_REQUEST: 'create_world_chat_request',
|
|
22
20
|
});
|
|
23
21
|
|
|
24
|
-
function resolveWorldRole({ isOwner = false,
|
|
22
|
+
function resolveWorldRole({ isOwner = false, isMember = false } = {}) {
|
|
25
23
|
if (isOwner) return WORLD_ROLES.OWNER;
|
|
26
24
|
if (isMember) return WORLD_ROLES.MEMBER;
|
|
27
|
-
return
|
|
25
|
+
return null;
|
|
28
26
|
}
|
|
29
27
|
|
|
30
28
|
function resolveActionRequirement(action) {
|
|
@@ -37,11 +35,6 @@ function resolveActionRequirement(action) {
|
|
|
37
35
|
allowedRoles: [WORLD_ROLES.OWNER],
|
|
38
36
|
reason: 'owner_role_required',
|
|
39
37
|
};
|
|
40
|
-
case WORLD_ACTIONS.MANAGE_WORLD_ROLES:
|
|
41
|
-
return {
|
|
42
|
-
allowedRoles: [WORLD_ROLES.OWNER],
|
|
43
|
-
reason: 'owner_role_required',
|
|
44
|
-
};
|
|
45
38
|
case WORLD_ACTIONS.SEARCH:
|
|
46
39
|
case WORLD_ACTIONS.VIEW_CANDIDATE_FEED:
|
|
47
40
|
case WORLD_ACTIONS.CREATE_CHAT_REQUEST:
|
|
@@ -77,22 +70,18 @@ export function createWorldAuthorizationService({ worldService, membershipServic
|
|
|
77
70
|
? resolveMembership(world.worldId, normalizedActorAgentId, { includeDisabled })
|
|
78
71
|
: null;
|
|
79
72
|
const isOwner = normalizedActorAgentId != null && normalizedActorAgentId === world.creatorAgentId;
|
|
80
|
-
const isAdmin = normalizedActorAgentId != null
|
|
81
|
-
&& Array.isArray(world.adminAgentIds)
|
|
82
|
-
&& world.adminAgentIds.includes(normalizedActorAgentId);
|
|
83
73
|
const isMember = membership?.status === 'active';
|
|
84
|
-
const worldRole = resolveWorldRole({ isOwner,
|
|
74
|
+
const worldRole = resolveWorldRole({ isOwner, isMember });
|
|
85
75
|
|
|
86
76
|
return {
|
|
87
77
|
world,
|
|
88
78
|
actorAgentId: normalizedActorAgentId,
|
|
89
79
|
worldRole,
|
|
90
|
-
managementRole: isOwner ? WORLD_ROLES.OWNER :
|
|
80
|
+
managementRole: isOwner ? WORLD_ROLES.OWNER : null,
|
|
91
81
|
membership,
|
|
92
82
|
membershipStatus: membership?.status || null,
|
|
93
83
|
roles: {
|
|
94
84
|
owner: isOwner,
|
|
95
|
-
admin: isAdmin,
|
|
96
85
|
member: isMember,
|
|
97
86
|
},
|
|
98
87
|
};
|
|
@@ -125,7 +114,7 @@ export function createWorldAuthorizationService({ worldService, membershipServic
|
|
|
125
114
|
|
|
126
115
|
listManagedWorlds({ actorAgentId, includeDisabled = true } = {}) {
|
|
127
116
|
return worldService
|
|
128
|
-
.
|
|
117
|
+
.listOwnedWorlds({ creatorAgentId: actorAgentId, includeDisabled })
|
|
129
118
|
.map((world) => resolveWorldActorContextForWorld(world, actorAgentId, { includeDisabled }))
|
|
130
119
|
.filter((context) => context.managementRole != null);
|
|
131
120
|
},
|
|
@@ -86,7 +86,7 @@ function createBroadcastNotAllowedError({ worldId, senderAgentId, senderRole } =
|
|
|
86
86
|
worldId,
|
|
87
87
|
senderAgentId,
|
|
88
88
|
senderRole,
|
|
89
|
-
allowedRoles: ['owner'
|
|
89
|
+
allowedRoles: ['owner'],
|
|
90
90
|
};
|
|
91
91
|
return error;
|
|
92
92
|
}
|
|
@@ -138,10 +138,7 @@ export function createWorldBroadcastService({
|
|
|
138
138
|
|
|
139
139
|
function resolveAudienceAgentIds(world, { audience, excludeSelf, senderAgentId } = {}) {
|
|
140
140
|
if (audience === 'admins' || audience === 'admins_and_owner') {
|
|
141
|
-
return dedupeAgentIds([
|
|
142
|
-
world.creatorAgentId,
|
|
143
|
-
...(Array.isArray(world.adminAgentIds) ? world.adminAgentIds : []),
|
|
144
|
-
], {
|
|
141
|
+
return dedupeAgentIds([world.creatorAgentId], {
|
|
145
142
|
excludeAgentId: excludeSelf ? senderAgentId : null,
|
|
146
143
|
});
|
|
147
144
|
}
|