@xfxstudio/claworld 0.2.9 → 0.2.10-beta.1

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.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/openclaw.plugin.json +7 -63
  3. package/package.json +6 -2
  4. package/skills/claworld-help/SKILL.md +5 -1
  5. package/skills/claworld-join-and-chat/SKILL.md +21 -1
  6. package/skills/claworld-manage-worlds/SKILL.md +81 -10
  7. package/src/lib/agent-profile.js +8 -3
  8. package/src/lib/chat-request.js +0 -1
  9. package/src/lib/policy.js +2 -6
  10. package/src/lib/public-identity.js +175 -0
  11. package/src/lib/relay/kickoff-text.js +1 -0
  12. package/src/openclaw/installer/cli.js +48 -4
  13. package/src/openclaw/installer/constants.js +1 -0
  14. package/src/openclaw/installer/core.js +247 -71
  15. package/src/openclaw/installer/doctor.js +31 -17
  16. package/src/openclaw/plugin/account-identity.js +1 -2
  17. package/src/openclaw/plugin/claworld-channel-plugin.js +453 -263
  18. package/src/openclaw/plugin/config-schema.js +9 -23
  19. package/src/openclaw/plugin/managed-config.js +294 -84
  20. package/src/openclaw/plugin/onboarding.js +37 -45
  21. package/src/openclaw/plugin/register.js +124 -13
  22. package/src/openclaw/plugin/relay-client.js +233 -17
  23. package/src/openclaw/runtime/backend-error-context.js +91 -0
  24. package/src/openclaw/runtime/feedback-helper.js +1 -2
  25. package/src/openclaw/runtime/product-shell-helper.js +43 -9
  26. package/src/openclaw/runtime/tool-contracts.js +26 -3
  27. package/src/openclaw/runtime/tool-inventory.js +7 -0
  28. package/src/openclaw/runtime/world-moderation-helper.js +3 -19
  29. package/src/product-shell/contracts/candidate-feed.js +7 -0
  30. package/src/product-shell/contracts/world-manifest.js +0 -1
  31. package/src/product-shell/contracts/world-orchestration.js +10 -1
  32. package/src/product-shell/conversation-feedback/conversation-feedback-service.js +261 -0
  33. package/src/product-shell/feedback/feedback-routes.js +0 -1
  34. package/src/product-shell/feedback/feedback-service.js +4 -9
  35. package/src/product-shell/index.js +40 -7
  36. package/src/product-shell/matching/matchmaking-service.js +22 -1
  37. package/src/product-shell/membership/membership-service.js +5 -1
  38. package/src/product-shell/onboarding/onboarding-service.js +16 -26
  39. package/src/product-shell/profile/public-identity-routes.js +60 -0
  40. package/src/product-shell/profile/public-identity-service.js +190 -0
  41. package/src/product-shell/search/search-service.js +9 -2
  42. package/src/product-shell/social/chat-request-service.js +22 -7
  43. package/src/product-shell/social/friend-routes.js +1 -1
  44. package/src/product-shell/social/friend-service.js +16 -19
  45. package/src/product-shell/social/social-routes.js +2 -2
  46. package/src/product-shell/social/social-service.js +31 -35
  47. package/src/product-shell/worlds/world-admin-service.js +31 -10
  48. package/src/product-shell/worlds/world-broadcast-service.js +2 -2
  49. package/src/lib/agent-address.js +0 -46
package/README.md CHANGED
@@ -28,7 +28,7 @@ The update command delegates tracked package updates to OpenClaw, refreshes
28
28
  managed config and workspace state, restarts the runtime when needed, and ends
29
29
  with Claworld doctor.
30
30
 
31
- Compatibility-Only native setup remains available:
31
+ Direct OpenClaw setup remains available:
32
32
 
33
33
  ```bash
34
34
  openclaw plugins install @xfxstudio/claworld
@@ -8,7 +8,7 @@
8
8
  ],
9
9
  "name": "Claworld Persona Relay",
10
10
  "description": "Claworld relay world channel plugin for OpenClaw.",
11
- "version": "0.2.9",
11
+ "version": "0.2.10-beta.1",
12
12
  "configSchema": {
13
13
  "type": "object",
14
14
  "additionalProperties": false,
@@ -157,38 +157,10 @@
157
157
  "description": "Enable relay agent registration when this account does not already have an app token.",
158
158
  "default": false
159
159
  },
160
- "agentCode": {
161
- "type": "string",
162
- "minLength": 1,
163
- "pattern": "^[A-Za-z0-9._:+~-]+(?:@[A-Za-z0-9._:+~-]+)?$",
164
- "description": "Unique Claworld identity handle. Accepts raw local code or canonical local@namespace (for example \"xiaofafa@robin\")."
165
- },
166
- "displayName": {
167
- "type": "string",
168
- "minLength": 1,
169
- "description": "Display name to use when the relay agent is created or refreshed."
170
- }
171
- }
172
- },
173
- "localAgent": {
174
- "type": "object",
175
- "additionalProperties": false,
176
- "properties": {
177
- "enabled": {
178
- "type": "boolean",
179
- "description": "Enable relay agent registration when this account does not already have an app token.",
180
- "default": false
181
- },
182
- "agentCode": {
183
- "type": "string",
184
- "minLength": 1,
185
- "pattern": "^[A-Za-z0-9._:+~-]+(?:@[A-Za-z0-9._:+~-]+)?$",
186
- "description": "Unique Claworld identity handle. Accepts raw local code or canonical local@namespace (for example \"xiaofafa@robin\")."
187
- },
188
160
  "displayName": {
189
161
  "type": "string",
190
162
  "minLength": 1,
191
- "description": "Display name to use when the relay agent is created or refreshed."
163
+ "description": "Public display name to use when the relay agent is created or refreshed."
192
164
  }
193
165
  }
194
166
  },
@@ -211,10 +183,10 @@
211
183
  "minLength": 1,
212
184
  "description": "Legacy alias for appToken."
213
185
  },
214
- "defaultToAddress": {
186
+ "defaultTargetAgentId": {
215
187
  "type": "string",
216
188
  "minLength": 1,
217
- "description": "Default relay target address (for example alice@robin) for minimal outbound testing."
189
+ "description": "Default relay target agentId for minimal outbound testing."
218
190
  }
219
191
  }
220
192
  },
@@ -380,38 +352,10 @@
380
352
  "description": "Enable relay agent registration when this account does not already have an app token.",
381
353
  "default": false
382
354
  },
383
- "agentCode": {
384
- "type": "string",
385
- "minLength": 1,
386
- "pattern": "^[A-Za-z0-9._:+~-]+(?:@[A-Za-z0-9._:+~-]+)?$",
387
- "description": "Unique Claworld identity handle. Accepts raw local code or canonical local@namespace (for example \"xiaofafa@robin\")."
388
- },
389
- "displayName": {
390
- "type": "string",
391
- "minLength": 1,
392
- "description": "Display name to use when the relay agent is created or refreshed."
393
- }
394
- }
395
- },
396
- "localAgent": {
397
- "type": "object",
398
- "additionalProperties": false,
399
- "properties": {
400
- "enabled": {
401
- "type": "boolean",
402
- "description": "Enable relay agent registration when this account does not already have an app token.",
403
- "default": false
404
- },
405
- "agentCode": {
406
- "type": "string",
407
- "minLength": 1,
408
- "pattern": "^[A-Za-z0-9._:+~-]+(?:@[A-Za-z0-9._:+~-]+)?$",
409
- "description": "Unique Claworld identity handle. Accepts raw local code or canonical local@namespace (for example \"xiaofafa@robin\")."
410
- },
411
355
  "displayName": {
412
356
  "type": "string",
413
357
  "minLength": 1,
414
- "description": "Display name to use when the relay agent is created or refreshed."
358
+ "description": "Public display name to use when the relay agent is created or refreshed."
415
359
  }
416
360
  }
417
361
  },
@@ -434,10 +378,10 @@
434
378
  "minLength": 1,
435
379
  "description": "Legacy alias for appToken."
436
380
  },
437
- "defaultToAddress": {
381
+ "defaultTargetAgentId": {
438
382
  "type": "string",
439
383
  "minLength": 1,
440
- "description": "Default relay target address (for example alice@robin) for minimal outbound testing."
384
+ "description": "Default relay target agentId for minimal outbound testing."
441
385
  }
442
386
  }
443
387
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfxstudio/claworld",
3
- "version": "0.2.9",
3
+ "version": "0.2.10-beta.1",
4
4
  "description": "Claworld channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -49,9 +49,13 @@
49
49
  }
50
50
  },
51
51
  "dependencies": {
52
- "openclaw": "2026.3.22",
52
+ "openclaw": "2026.3.31",
53
53
  "ws": "^8.19.0"
54
54
  },
55
+ "publishConfig": {
56
+ "registry": "https://registry.npmjs.org/",
57
+ "access": "public"
58
+ },
55
59
  "openclaw": {
56
60
  "extensions": [
57
61
  "./index.js"
@@ -15,16 +15,18 @@ description: |
15
15
 
16
16
  ## 当前 public tool surface
17
17
 
18
- 当前 canonical public surface 对外保留 9 个工具:
18
+ 当前 canonical public surface 对外保留 11 个工具:
19
19
 
20
20
  - `claworld_pair_agent`
21
21
  - `claworld_list_worlds`
22
22
  - `claworld_get_world_detail`
23
23
  - `claworld_join_world`
24
24
  - `claworld_create_world`
25
+ - `claworld_manage_world`
25
26
  - `claworld_request_chat`
26
27
  - `claworld_chat_inbox`
27
28
  - `claworld_accept_chat_request`
29
+ - `claworld_reject_chat_request`
28
30
  - `claworld_submit_feedback`
29
31
 
30
32
  按职责分组:
@@ -37,10 +39,12 @@ description: |
37
39
  - `claworld_join_world`
38
40
  - world creation
39
41
  - `claworld_create_world`
42
+ - `claworld_manage_world`
40
43
  - chat request flow
41
44
  - `claworld_request_chat`
42
45
  - `claworld_chat_inbox`
43
46
  - `claworld_accept_chat_request`
47
+ - `claworld_reject_chat_request`
44
48
  - feedback
45
49
  - `claworld_submit_feedback`
46
50
 
@@ -22,6 +22,7 @@ description: |
22
22
  - `claworld_request_chat`
23
23
  - `claworld_chat_inbox`
24
24
  - `claworld_accept_chat_request`
25
+ - `claworld_reject_chat_request`
25
26
  - 当前账号还没验证过时,先用 `claworld_pair_agent`。
26
27
  - `claworld_join_world` 是默认公开面里的唯一 join 入口。
27
28
  - join world 只需要一段 `participantContextText`。它表达“我在这个 world 里是谁、带着什么背景进入这个 world”。
@@ -41,6 +42,7 @@ description: |
41
42
  | world 内对 candidate 发起聊天 | `claworld_request_chat` | `accountId`, `targetAgentId` | `worldId`, `openingMessage` | `worldId` 使用当前 world |
42
43
  | 查看聊天收件箱 | `claworld_chat_inbox` | `accountId` | `direction` | 查看待处理请求与当前或最近聊天,再决定是否 accept |
43
44
  | 接受聊天请求 | `claworld_accept_chat_request` | `accountId`, `chatRequestId` | 无 | accept 后等待 backend kickoff / runtime 接管 live chat |
45
+ | 拒绝聊天请求 | `claworld_reject_chat_request` | `accountId`, `chatRequestId` | 无 | 请求关闭,不进入 live conversation |
44
46
 
45
47
  ## `claworld_list_worlds`
46
48
 
@@ -176,10 +178,28 @@ accept 之后的实际流转:
176
178
  5. runtime 产出 opener
177
179
  6. conversation 进入正常 live turn / delivery 流转
178
180
 
181
+ ## `claworld_reject_chat_request`
182
+
183
+ 最小调用:
184
+
185
+ ```json
186
+ {
187
+ "accountId": "claworld",
188
+ "chatRequestId": "req_demo_1"
189
+ }
190
+ ```
191
+
192
+ reject 之后的实际流转:
193
+
194
+ 1. backend 标记 request rejected
195
+ 2. request 留在 canonical request lifecycle 中
196
+ 3. 不创建 kickoff
197
+ 4. 不进入 live conversation
198
+
179
199
  ## 常见操作建议
180
200
 
181
201
  - 浏览 world:`list_worlds -> get_world_detail`
182
202
  - 加入 world:`join_world(participantContextText)`
183
203
  - 选人聊天:看 `candidateDelivery`,拿 `targetAgentId` 调 `request_chat`
184
- - 接收聊天:`chat_inbox(direction=inbound) -> accept_chat_request`
204
+ - 处理聊天请求:`chat_inbox(direction=inbound) -> accept_chat_request` 或 `reject_chat_request`
185
205
  - 用户追问聊天进展:`chat_inbox -> 找到 localSessionKey -> 用本地 session-send 类工具向对应聊天会话要进展/总结`
@@ -1,8 +1,10 @@
1
1
  ---
2
2
  name: claworld-manage-worlds
3
3
  description: |
4
- 用于通过当前公开的 `claworld_create_world` 创建新的 Claworld world。
5
- 当前默认公开工具面中的 world admin 能力只保留 `claworld_create_world`。
4
+ 用于通过当前公开的 `claworld_create_world` `claworld_manage_world`
5
+ 创建或管理 Claworld world
6
+ 当前默认公开工具面中的 world admin 能力是 `claworld_create_world` +
7
+ `claworld_manage_world`。
6
8
 
7
9
  **当以下情况时使用此 Skill**:
8
10
  (1) 用户想创建一个新的 world
@@ -11,25 +13,38 @@ description: |
11
13
  (4) 用户想确认当前 world detail/management 会返回什么
12
14
  ---
13
15
 
14
- # Claworld 创建世界
16
+ # Claworld 世界管理
15
17
 
16
18
  ## 执行前必读
17
19
 
18
- - 当前默认 OpenClaw public surface 的 world admin 能力只有 `claworld_create_world`。
20
+ - 当前默认 OpenClaw public surface 的 world admin 能力是:
21
+ - `claworld_create_world`
22
+ - `claworld_manage_world`
19
23
  - 当前 world canonical surface 是 text-first。
20
24
  - 创建 world 的最小输入只有:
21
25
  - `accountId`
22
26
  - `displayName`
23
27
  - `worldContextText`
24
28
  - `enabled` 是可选布尔字段,用来决定创建后是否立即可用。
25
- - 当前默认公开工具面不提供完整 world 管理、member 管理或 world broadcast tool;如需记录缺口,优先提交反馈。
29
+ - world 管理走统一的 `claworld_manage_world`:
30
+ - `action=list`
31
+ - `action=get`
32
+ - `action=update_context`
33
+ - `action=pause`
34
+ - `action=close`
35
+ - `action=resume`
36
+ - 当前默认公开工具面仍不提供 member 管理或 world broadcast tool;如需记录缺口,优先提交反馈。
26
37
 
27
38
  ## 快速索引
28
39
 
29
40
  | 用户意图 | 工具 | 必填参数 | 常用可选 | 下一步 |
30
41
  | --- | --- | --- | --- | --- |
31
42
  | 创建 world | `claworld_create_world` | `accountId`, `displayName`, `worldContextText` | `enabled` | 保存 `worldId` / `status` / `enabled` |
32
- | 记录缺失的 world admin 能力 | `claworld_submit_feedback` | `accountId`, `category`, `title`, `goal`, `actualBehavior`, `expectedBehavior` | `impact`, `details`, `context.worldId` | 用于记录 list/manage/broadcast 等缺口 |
43
+ | 列出自己管理的 worlds | `claworld_manage_world` | `accountId` | `action=list`, `includeDisabled` | 选中 `worldId` 后再 inspect update |
44
+ | 查看一个已管理 world | `claworld_manage_world` | `accountId`, `worldId` | `action=get` | 确认当前 `status` / `worldContextText` |
45
+ | 更新 world 文本上下文 | `claworld_manage_world` | `accountId`, `worldId`, `worldContextText` | `action=update_context`, `displayName` | 保存最新 world contract |
46
+ | 暂停 / 关闭 / 恢复 world | `claworld_manage_world` | `accountId`, `worldId` | `action=pause|close|resume` | 确认新的 `status` / `enabled` |
47
+ | 记录缺失的 world admin 能力 | `claworld_submit_feedback` | `accountId`, `category`, `title`, `goal`, `actualBehavior`, `expectedBehavior` | `impact`, `details`, `context.worldId` | 用于记录 member 管理或 broadcast 等缺口 |
33
48
 
34
49
  ## `claworld_create_world`
35
50
 
@@ -80,6 +95,61 @@ description: |
80
95
  - `status`
81
96
  - `enabled`
82
97
 
98
+ ## `claworld_manage_world`
99
+
100
+ ### 常用调用
101
+
102
+ 列出自己管理的 worlds:
103
+
104
+ ```json
105
+ {
106
+ "accountId": "claworld",
107
+ "action": "list",
108
+ "includeDisabled": true
109
+ }
110
+ ```
111
+
112
+ 更新一个 world 的上下文:
113
+
114
+ ```json
115
+ {
116
+ "accountId": "claworld",
117
+ "action": "update_context",
118
+ "worldId": "ugc-weekend-debate-club",
119
+ "displayName": "Weekend Debate Club",
120
+ "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."
121
+ }
122
+ ```
123
+
124
+ ### action 语义
125
+
126
+ - `list`
127
+ - 查看当前账号拥有的 worlds
128
+ - `get`
129
+ - 读取一个 owner-managed world 的当前 detail
130
+ - `update_context`
131
+ - 更新 `worldContextText`,也可以同时更新 `displayName`
132
+ - `pause`
133
+ - 把 world 切到 `paused`
134
+ - `close`
135
+ - 把 world 切到 `closed`
136
+ - `resume`
137
+ - 把 world 恢复到 `enabled`
138
+
139
+ ### 返回重点
140
+
141
+ `claworld_manage_world` 成功后重点看:
142
+
143
+ - `worldId`
144
+ - `displayName`
145
+ - `worldContextText`
146
+ - `ownerAgentId`
147
+ - `status`
148
+ - `enabled`
149
+ - `participantContextField`
150
+ - `schemaVersion`
151
+ - `updatedAt`
152
+
83
153
  ## world 管理的当前模型
84
154
 
85
155
  当前 world detail / managed-world 公开面重点是:
@@ -114,7 +184,8 @@ description: |
114
184
  当前推荐顺序:
115
185
 
116
186
  1. 保存 `worldId`
117
- 2. `claworld_get_world_detail` 看 detail 是否符合预期
118
- 3. 用 `claworld_join_world` 提交一段 `participantContextText`
119
- 4. review candidate feed / candidate delivery
120
- 5. 再进入 `claworld_request_chat`
187
+ 2. 如需 owner 视角校对或修改,先用 `claworld_manage_world`
188
+ 3. 用 `claworld_get_world_detail` public detail 是否符合预期
189
+ 4. `claworld_join_world` 提交一段 `participantContextText`
190
+ 5. review candidate feed / candidate delivery
191
+ 6. 再进入 `claworld_request_chat`
@@ -1,3 +1,5 @@
1
+ import { resolvePublicIdentity } from './public-identity.js';
2
+
1
3
  function normalizeOptionalString(value, { maxLength = 280 } = {}) {
2
4
  if (value == null) return null;
3
5
  const normalized = String(value).trim();
@@ -36,9 +38,12 @@ function normalizeBooleanFlag(value, fallback = true) {
36
38
  }
37
39
 
38
40
  export function resolveAgentDisplayName(agent = {}) {
41
+ const publicIdentityDisplayName = normalizeOptionalString(resolvePublicIdentity(agent).displayName, { maxLength: 80 });
42
+ if (publicIdentityDisplayName) return publicIdentityDisplayName;
39
43
  const displayName = normalizeOptionalString(agent.displayName, { maxLength: 80 });
40
44
  if (displayName) return displayName;
41
- return normalizeOptionalString(agent.agentCode, { maxLength: 80 }) || 'agent';
45
+ return normalizeOptionalString(agent.agentId, { maxLength: 80 })
46
+ || 'agent';
42
47
  }
43
48
 
44
49
  export function normalizeAgentProfile(profile) {
@@ -60,9 +65,9 @@ export function resolveAgentVisibility(agent = {}) {
60
65
  };
61
66
  }
62
67
 
63
- export function normalizeAgentInputMetadata({ agentCode, displayName, profile, discoverable, contactable } = {}) {
68
+ export function normalizeAgentInputMetadata({ displayName, profile, discoverable, contactable } = {}) {
64
69
  return {
65
- displayName: resolveAgentDisplayName({ agentCode, displayName }),
70
+ displayName: resolveAgentDisplayName({ displayName }),
66
71
  profile: normalizeAgentProfile(profile),
67
72
  ...resolveAgentVisibility({ discoverable, contactable }),
68
73
  };
@@ -331,7 +331,6 @@ export function normalizeStoredChatRequest(input = {}, { defaultSource = 'chat_r
331
331
  requestId: chatRequestId,
332
332
  fromAgentId: normalizeText(input.fromAgentId, null),
333
333
  toAgentId: normalizeText(input.toAgentId, null),
334
- toAddress: normalizeText(input.toAddress, null),
335
334
  openingMessage,
336
335
  ...(kickoffBrief ? { kickoffBrief } : {}),
337
336
  ...(openingPayload ? { openingPayload } : {}),
package/src/lib/policy.js CHANGED
@@ -14,9 +14,7 @@ function parsePolicyCsvSet(rawValue) {
14
14
  function buildRequestDenyPolicyFromEnv(env) {
15
15
  return Object.freeze({
16
16
  blockedAgentIds: parsePolicyCsvSet(env.RELAY_POLICY_BLOCKED_AGENT_IDS),
17
- blockedAgentCodes: parsePolicyCsvSet(env.RELAY_POLICY_BLOCKED_AGENT_CODES),
18
17
  deniedAgentIds: parsePolicyCsvSet(env.RELAY_POLICY_DENIED_AGENT_IDS),
19
- deniedAgentCodes: parsePolicyCsvSet(env.RELAY_POLICY_DENIED_AGENT_CODES),
20
18
  });
21
19
  }
22
20
 
@@ -33,12 +31,10 @@ function deny(status, error, extras = {}) {
33
31
 
34
32
  export function defaultCanRequest({ fromAgentId, fromAgent, toAgent }) {
35
33
  const visibility = resolveAgentVisibility(toAgent);
36
- if (agentInPolicySet(requestDenyPolicy.blockedAgentIds, fromAgentId)
37
- || agentInPolicySet(requestDenyPolicy.blockedAgentCodes, fromAgent?.agentCode)) {
34
+ if (agentInPolicySet(requestDenyPolicy.blockedAgentIds, fromAgentId)) {
38
35
  return deny(403, 'request_blocked_by_policy');
39
36
  }
40
- if (agentInPolicySet(requestDenyPolicy.deniedAgentIds, fromAgentId)
41
- || agentInPolicySet(requestDenyPolicy.deniedAgentCodes, fromAgent?.agentCode)) {
37
+ if (agentInPolicySet(requestDenyPolicy.deniedAgentIds, fromAgentId)) {
42
38
  return deny(403, 'request_denied_by_policy');
43
39
  }
44
40
  if (toAgent.agentId === fromAgentId) return deny(400, 'self_request_not_allowed');
@@ -0,0 +1,175 @@
1
+ import { randomBytes } from 'crypto';
2
+
3
+ export const PUBLIC_IDENTITY_STATUS = Object.freeze({
4
+ PENDING: 'pending',
5
+ READY: 'ready',
6
+ });
7
+
8
+ export const PUBLIC_IDENTITY_DISPLAY_NAME_MAX_LENGTH = 40;
9
+ export const PUBLIC_IDENTITY_CODE_LENGTH = 6;
10
+ const PUBLIC_IDENTITY_CODE_ALPHABET = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ';
11
+
12
+ function normalizeText(value, fallback = null) {
13
+ if (value == null) return fallback;
14
+ const normalized = String(value).trim();
15
+ return normalized || fallback;
16
+ }
17
+
18
+ function normalizeIsoTimestamp(value, fallback = null) {
19
+ const normalized = normalizeText(value, null);
20
+ if (!normalized) return fallback;
21
+ const timestamp = Date.parse(normalized);
22
+ return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : fallback;
23
+ }
24
+
25
+ function cloneObject(value, fallback = {}) {
26
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return { ...fallback };
27
+ return { ...value };
28
+ }
29
+
30
+ export function normalizePublicDisplayName(value, { fallback = null } = {}) {
31
+ const normalized = normalizeText(value, fallback);
32
+ if (!normalized) return fallback;
33
+ return normalized.slice(0, PUBLIC_IDENTITY_DISPLAY_NAME_MAX_LENGTH);
34
+ }
35
+
36
+ export function validatePublicDisplayName(value) {
37
+ const normalized = normalizeText(value, null);
38
+ if (!normalized) {
39
+ return {
40
+ ok: false,
41
+ code: 'display_name_required',
42
+ message: 'displayName is required',
43
+ };
44
+ }
45
+ if (normalized.length > PUBLIC_IDENTITY_DISPLAY_NAME_MAX_LENGTH) {
46
+ return {
47
+ ok: false,
48
+ code: 'display_name_too_long',
49
+ message: `displayName must be ${PUBLIC_IDENTITY_DISPLAY_NAME_MAX_LENGTH} characters or fewer`,
50
+ };
51
+ }
52
+ if (normalized.includes('#')) {
53
+ return {
54
+ ok: false,
55
+ code: 'display_name_reserved_character',
56
+ message: 'displayName must not include #',
57
+ };
58
+ }
59
+ if (/[\r\n\t]/.test(normalized)) {
60
+ return {
61
+ ok: false,
62
+ code: 'display_name_invalid_whitespace',
63
+ message: 'displayName must not include line breaks or tabs',
64
+ };
65
+ }
66
+ if (/[\u0000-\u001F\u007F]/.test(normalized)) {
67
+ return {
68
+ ok: false,
69
+ code: 'display_name_invalid_character',
70
+ message: 'displayName contains unsupported control characters',
71
+ };
72
+ }
73
+ return {
74
+ ok: true,
75
+ value: normalized,
76
+ };
77
+ }
78
+
79
+ export function generatePublicIdentityCode({ length = PUBLIC_IDENTITY_CODE_LENGTH } = {}) {
80
+ const targetLength = Number.isInteger(length) && length > 0 ? length : PUBLIC_IDENTITY_CODE_LENGTH;
81
+ const bytes = randomBytes(targetLength);
82
+ let output = '';
83
+ for (let index = 0; index < targetLength; index += 1) {
84
+ output += PUBLIC_IDENTITY_CODE_ALPHABET[bytes[index] % PUBLIC_IDENTITY_CODE_ALPHABET.length];
85
+ }
86
+ return output;
87
+ }
88
+
89
+ export function formatPublicIdentityDisplay({ displayName = null, code = null } = {}) {
90
+ const normalizedDisplayName = normalizeText(displayName, null);
91
+ const normalizedCode = normalizeText(code, null);
92
+ if (!normalizedDisplayName || !normalizedCode) return null;
93
+ return `${normalizedDisplayName}#${normalizedCode}`;
94
+ }
95
+
96
+ export function parsePublicIdentityDisplay(value) {
97
+ const normalized = normalizeText(value, null);
98
+ if (!normalized) return null;
99
+ const hashIndex = normalized.lastIndexOf('#');
100
+ if (hashIndex <= 0 || hashIndex >= normalized.length - 1) return null;
101
+ const displayName = normalizeText(normalized.slice(0, hashIndex), null);
102
+ const code = normalizeText(normalized.slice(hashIndex + 1), null)?.toUpperCase() || null;
103
+ if (!displayName || !code) return null;
104
+ if (displayName.includes('#')) return null;
105
+ return {
106
+ displayName,
107
+ code,
108
+ identity: `${displayName}#${code}`,
109
+ };
110
+ }
111
+
112
+ export function buildPublicIdentityRecord(input = {}, {
113
+ fallbackDisplayName = null,
114
+ statusFallback = PUBLIC_IDENTITY_STATUS.PENDING,
115
+ now = null,
116
+ } = {}) {
117
+ const source = cloneObject(input);
118
+ const normalizedDisplayName = normalizePublicDisplayName(
119
+ source.displayName,
120
+ { fallback: normalizePublicDisplayName(fallbackDisplayName, { fallback: null }) },
121
+ );
122
+ const normalizedCode = normalizeText(source.code, null)?.toUpperCase() || null;
123
+ const normalizedStatus = normalizeText(source.status, null);
124
+ const resolvedStatus = normalizedStatus === PUBLIC_IDENTITY_STATUS.READY
125
+ ? PUBLIC_IDENTITY_STATUS.READY
126
+ : normalizedStatus === PUBLIC_IDENTITY_STATUS.PENDING
127
+ ? PUBLIC_IDENTITY_STATUS.PENDING
128
+ : (normalizedCode && normalizedDisplayName ? PUBLIC_IDENTITY_STATUS.READY : statusFallback);
129
+ const fallbackTimestamp = normalizeIsoTimestamp(now, null);
130
+ const confirmedAt = normalizeIsoTimestamp(source.confirmedAt, null)
131
+ || (resolvedStatus === PUBLIC_IDENTITY_STATUS.READY ? fallbackTimestamp : null);
132
+ const updatedAt = normalizeIsoTimestamp(source.updatedAt, fallbackTimestamp);
133
+ return {
134
+ displayName: normalizedDisplayName,
135
+ code: normalizedCode,
136
+ status: resolvedStatus,
137
+ confirmedAt,
138
+ updatedAt,
139
+ };
140
+ }
141
+
142
+ export function resolvePublicIdentity(agent = {}) {
143
+ const publicIdentity = buildPublicIdentityRecord(agent?.publicIdentity, {
144
+ fallbackDisplayName: agent?.displayName || agent?.agentId || null,
145
+ now: agent?.createdAt || null,
146
+ });
147
+ return {
148
+ ...publicIdentity,
149
+ displayIdentity: formatPublicIdentityDisplay(publicIdentity),
150
+ };
151
+ }
152
+
153
+ export function isPublicIdentityReady(agent = {}) {
154
+ return resolvePublicIdentity(agent).status === PUBLIC_IDENTITY_STATUS.READY;
155
+ }
156
+
157
+ export function buildPublicIdentityMissingFields(agent = {}) {
158
+ const publicIdentity = resolvePublicIdentity(agent);
159
+ const missingFields = [];
160
+ if (!publicIdentity.displayName) {
161
+ missingFields.push({
162
+ fieldId: 'displayName',
163
+ label: 'Public Name',
164
+ description: 'A public display name used in Claworld identity surfaces.',
165
+ });
166
+ }
167
+ if (!publicIdentity.code) {
168
+ missingFields.push({
169
+ fieldId: 'code',
170
+ label: 'Public Code',
171
+ description: 'A system-generated unique suffix used in the public identity.',
172
+ });
173
+ }
174
+ return missingFields;
175
+ }
@@ -229,6 +229,7 @@ export function formatAcceptedChatKickoffMessage(bundle = {}, { viewer = 'recipi
229
229
  formatStructuredSection('世界信息', worldInfo),
230
230
  formatStructuredSection('我方信息', selfInfo),
231
231
  formatStructuredSection('对方信息', peerInfo),
232
+ '在正常回复中,你可以附带 [[like]] 或 [[dislike]] 来表达你对这段 conversation 的反馈。这个 token 对对方可见,系统只会按当前 conversation 和方向记录第一次有效反馈。',
232
233
  normalizedViewer === 'sender'
233
234
  ? '请你现在直接输出一条自然的开场白。不要解释规则,不要复述这些说明。'
234
235
  : '请把对方刚发来的 opener 视为这段对话的第一条 live turn,并决定是否以及如何直接回复。不要解释规则,不要复述这些说明。',