@xfxstudio/claworld 2026.4.16-testing.3 → 2026.4.21-testing.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.
@@ -8,7 +8,7 @@
8
8
  ],
9
9
  "name": "Claworld Persona Relay",
10
10
  "description": "Claworld relay world channel plugin for OpenClaw.",
11
- "version": "2026.4.16-testing.3",
11
+ "version": "2026.4.21-testing.1",
12
12
  "configSchema": {
13
13
  "type": "object",
14
14
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfxstudio/claworld",
3
- "version": "2026.4.16-testing.3",
3
+ "version": "2026.4.21-testing.1",
4
4
  "description": "Claworld channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -297,6 +297,17 @@ openclaw agents bind --agent main --bind claworld:claworld
297
297
  - `context.tags`
298
298
  - `context.metadata`
299
299
 
300
+ 这些不是 feedback 入参,不要手填:
301
+
302
+ - `openclawVersion`
303
+ - `pluginVersion`
304
+ - `modelProvider`
305
+ - `modelId`
306
+ - `osCategory`
307
+
308
+ 这些 diagnostics 由 plugin/runtime 在提交时自动补齐;提交成功后的结果里会回显一份非敏感摘要。
309
+ 如果某项当前拿不到,结果里允许是 `null`,但 feedback 仍然应该成功记录。
310
+
300
311
  ## 重要规则
301
312
 
302
313
  - 多账号环境下始终显式传 `accountId`
@@ -411,9 +411,11 @@ world-scoped chat:
411
411
 
412
412
  - `filters`
413
413
  - `counts.global`
414
+ - `counts.global.recentRequestStatusCounts`
414
415
  - `counts.global.chatStatusCounts`
415
416
  - `counts.filtered`
416
417
  - `pendingRequests`
418
+ - `recentRequests`
417
419
  - `chats`
418
420
  - `chatRequestId`
419
421
  - `status`
@@ -426,10 +428,17 @@ world-scoped chat:
426
428
  - 不传 `filters` 时,默认同时看 inbound 和 outbound
427
429
  - `filters.direction` 用于区分 inbound / outbound
428
430
  - `filters.mode` 用于区分 direct / world
429
- - `filters.status` 用于看 `pending`、`opening`、`ending`、`active`、`silent`、`kickoff_failed`、`ended`
431
+ - `filters.status` 用于看 request 状态 `pending`、`expired`、`rejected`,以及 chat 状态 `opening`、`ending`、`active`、`silent`、`kickoff_failed`、`ended`
430
432
  - `filters.worldId`、`filters.chatRequestId`、`filters.conversationKey`、`filters.localSessionKey` 用于精确定位
431
433
  - `filters.counterpartyAgentId` 用于按对端缩小范围
432
434
 
435
+ list 返回语义:
436
+
437
+ - `pendingRequests` 只放还可处理的 request
438
+ - `recentRequests` 放已经终态但还没形成 chat 的 request,例如 `expired`、`rejected`
439
+ - `chats` 只放已经建立或 kickoff 中的 chat
440
+ - 如果用户说“昨天还是 pending,今天怎么没了”,先查 `recentRequests`,或直接用 `filters.status=expired|rejected`
441
+
433
442
  ### 处理请求
434
443
 
435
444
  accept:
@@ -0,0 +1,222 @@
1
+ import { accessSync, constants as FS_CONSTANTS } from 'fs';
2
+ import path from 'path';
3
+ import { spawnSync } from 'child_process';
4
+
5
+ export const DEFAULT_OPENCLAW_BIN = 'openclaw';
6
+
7
+ function normalizeText(value, fallback = null) {
8
+ if (value == null) return fallback;
9
+ const normalized = String(value).trim();
10
+ return normalized || fallback;
11
+ }
12
+
13
+ function createOpenclawVersionError(code, message, context = {}) {
14
+ const error = new Error(message);
15
+ error.code = code;
16
+ error.context = context;
17
+ return error;
18
+ }
19
+
20
+ function isExplicitCommandPath(command = '') {
21
+ const normalized = String(command || '').trim();
22
+ if (!normalized) return false;
23
+ return normalized.includes('/') || normalized.includes('\\') || path.isAbsolute(normalized);
24
+ }
25
+
26
+ function splitPathEnvEntries(pathValue = '') {
27
+ return String(pathValue || '')
28
+ .split(path.delimiter)
29
+ .map((entry) => entry.trim())
30
+ .filter(Boolean);
31
+ }
32
+
33
+ function isNodeModulesBinEntry(entry = '') {
34
+ const normalized = path.resolve(String(entry || ''));
35
+ return path.basename(normalized) === '.bin'
36
+ && path.basename(path.dirname(normalized)) === 'node_modules';
37
+ }
38
+
39
+ function resolveCommandNameCandidates(command = '', env = process.env) {
40
+ const normalized = String(command || '').trim();
41
+ if (!normalized) return [];
42
+ if (process.platform !== 'win32' || path.extname(normalized)) {
43
+ return [normalized];
44
+ }
45
+
46
+ const pathExt = splitPathEnvEntries(env?.PATHEXT || process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
47
+ .map((ext) => ext.toLowerCase());
48
+ return [...new Set([normalized, ...pathExt.map((ext) => `${normalized}${ext}`)])];
49
+ }
50
+
51
+ function hasExecutableAccess(filePath = '') {
52
+ try {
53
+ accessSync(filePath, FS_CONSTANTS.X_OK);
54
+ return true;
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ function resolveOpenclawCliBinary({
61
+ openclawBin = DEFAULT_OPENCLAW_BIN,
62
+ env = process.env,
63
+ } = {}) {
64
+ const requestedBin = normalizeText(openclawBin, DEFAULT_OPENCLAW_BIN);
65
+ if (
66
+ requestedBin !== DEFAULT_OPENCLAW_BIN
67
+ || isExplicitCommandPath(requestedBin)
68
+ ) {
69
+ return {
70
+ requestedBin,
71
+ binaryPath: requestedBin,
72
+ binarySource: isExplicitCommandPath(requestedBin) ? 'explicit_path' : 'explicit_command',
73
+ skippedLocalBinaryPaths: [],
74
+ };
75
+ }
76
+
77
+ const candidates = [];
78
+ for (const entry of splitPathEnvEntries(env?.PATH || process.env.PATH || '')) {
79
+ for (const name of resolveCommandNameCandidates(requestedBin, env)) {
80
+ const candidatePath = path.join(entry, name);
81
+ if (hasExecutableAccess(candidatePath)) {
82
+ candidates.push(candidatePath);
83
+ }
84
+ }
85
+ }
86
+
87
+ const uniqueCandidates = [...new Set(candidates)];
88
+ const hostCandidates = uniqueCandidates.filter((candidate) => !isNodeModulesBinEntry(path.dirname(candidate)));
89
+ const localCandidates = uniqueCandidates.filter((candidate) => isNodeModulesBinEntry(path.dirname(candidate)));
90
+ const binaryPath = hostCandidates[0] || uniqueCandidates[0] || requestedBin;
91
+
92
+ return {
93
+ requestedBin,
94
+ binaryPath,
95
+ binarySource: hostCandidates[0]
96
+ ? 'host_path'
97
+ : uniqueCandidates[0]
98
+ ? 'package_local_path'
99
+ : 'default_command',
100
+ skippedLocalBinaryPaths: localCandidates.filter((candidate) => candidate !== binaryPath),
101
+ };
102
+ }
103
+
104
+ function defaultCommandRunner({
105
+ bin,
106
+ args = [],
107
+ cwd = process.cwd(),
108
+ env = process.env,
109
+ } = {}) {
110
+ const result = spawnSync(bin, args, {
111
+ cwd,
112
+ env,
113
+ encoding: 'utf8',
114
+ stdio: ['ignore', 'pipe', 'pipe'],
115
+ shell: false,
116
+ windowsHide: true,
117
+ });
118
+ if (result.error) throw result.error;
119
+ return {
120
+ status: Number.isInteger(result.status) ? result.status : 1,
121
+ stdout: result.stdout || '',
122
+ stderr: result.stderr || '',
123
+ };
124
+ }
125
+
126
+ function parseCommandVersion(text) {
127
+ const match = String(text || '').match(/OpenClaw\s+([0-9]+(?:\.[0-9]+)+(?:-[0-9A-Za-z.-]+)?)/i)
128
+ || String(text || '').match(/([0-9]+(?:\.[0-9]+)+(?:-[0-9A-Za-z.-]+)?)/);
129
+ return match ? match[1] : null;
130
+ }
131
+
132
+ async function executeCommand({
133
+ commandRunner = defaultCommandRunner,
134
+ bin,
135
+ args = [],
136
+ cwd = process.cwd(),
137
+ env = process.env,
138
+ dryRun = false,
139
+ } = {}) {
140
+ const resolvedBin = resolveOpenclawCliBinary({
141
+ openclawBin: bin,
142
+ env,
143
+ });
144
+ const effectiveBin = commandRunner === defaultCommandRunner
145
+ ? resolvedBin.binaryPath
146
+ : resolvedBin.requestedBin;
147
+ if (dryRun) {
148
+ return {
149
+ status: 0,
150
+ stdout: '',
151
+ stderr: '',
152
+ requestedBin: resolvedBin.requestedBin,
153
+ resolvedBin: resolvedBin.binaryPath,
154
+ binSource: resolvedBin.binarySource,
155
+ skippedBinCandidates: resolvedBin.skippedLocalBinaryPaths,
156
+ };
157
+ }
158
+
159
+ let result;
160
+ try {
161
+ result = await commandRunner({
162
+ bin: effectiveBin,
163
+ args,
164
+ cwd,
165
+ env,
166
+ });
167
+ } catch (error) {
168
+ throw createOpenclawVersionError(
169
+ 'openclaw_command_failed',
170
+ error?.message || String(error),
171
+ {
172
+ bin: resolvedBin.binaryPath,
173
+ args,
174
+ },
175
+ );
176
+ }
177
+
178
+ return {
179
+ status: Number.isInteger(result?.status) ? result.status : 1,
180
+ stdout: result?.stdout || '',
181
+ stderr: result?.stderr || '',
182
+ requestedBin: resolvedBin.requestedBin,
183
+ resolvedBin: resolvedBin.binaryPath,
184
+ binSource: resolvedBin.binarySource,
185
+ skippedBinCandidates: resolvedBin.skippedLocalBinaryPaths,
186
+ };
187
+ }
188
+
189
+ export async function detectOpenclawHost({
190
+ openclawBin = DEFAULT_OPENCLAW_BIN,
191
+ commandRunner = defaultCommandRunner,
192
+ cwd = process.cwd(),
193
+ env = process.env,
194
+ dryRun = false,
195
+ } = {}) {
196
+ const result = await executeCommand({
197
+ commandRunner,
198
+ bin: openclawBin,
199
+ args: ['--version'],
200
+ cwd,
201
+ env,
202
+ dryRun,
203
+ });
204
+ const version = parseCommandVersion(result.stdout || result.stderr);
205
+ if (!version) {
206
+ throw createOpenclawVersionError(
207
+ 'openclaw_version_unreadable',
208
+ 'Unable to determine the installed OpenClaw version.',
209
+ { result },
210
+ );
211
+ }
212
+ return {
213
+ version,
214
+ raw: (result.stdout || result.stderr || '').trim(),
215
+ requestedBin: result.requestedBin || openclawBin,
216
+ binaryPath: result.resolvedBin || openclawBin,
217
+ binarySource: result.binSource || 'default_command',
218
+ skippedLocalBinaryPaths: Array.isArray(result.skippedBinCandidates)
219
+ ? result.skippedBinCandidates
220
+ : [],
221
+ };
222
+ }
@@ -2313,6 +2313,8 @@ export function createClaworldChannelPlugin({
2313
2313
  cfg: runtimeContext.cfg || cfg,
2314
2314
  accountId: runtimeConfig.accountId || accountId || null,
2315
2315
  runtimeConfig,
2316
+ runtime: runtimeResolution.runtime,
2317
+ runtimeSource: runtimeResolution.runtimeSource,
2316
2318
  agentId: configuredContext.agentId || runtimeConfig.relay?.agentId || null,
2317
2319
  bindingSource: 'runtime_context',
2318
2320
  };
@@ -2324,6 +2326,8 @@ export function createClaworldChannelPlugin({
2324
2326
  cfg,
2325
2327
  accountId: runtimeConfig.accountId || accountId || null,
2326
2328
  runtimeConfig,
2329
+ runtime: runtimeResolution.runtime,
2330
+ runtimeSource: runtimeResolution.runtimeSource,
2327
2331
  agentId: configuredContext.agentId || runtimeConfig.relay?.agentId || null,
2328
2332
  bindingSource: binding.bindingSource,
2329
2333
  };
@@ -3314,6 +3318,7 @@ async function generateRuntimeProfileCard(context = {}) {
3314
3318
  cfg: resolvedContext.cfg || {},
3315
3319
  accountId: resolvedContext.accountId || null,
3316
3320
  runtimeConfig: resolvedContext.runtimeConfig || null,
3321
+ runtime: resolvedContext.runtime || null,
3317
3322
  agentId: resolvedContext.agentId || null,
3318
3323
  category: context.category || null,
3319
3324
  title: context.title || null,
@@ -161,12 +161,12 @@ export async function loadCurrentConfig(api) {
161
161
  if (api?.config && typeof api.config.loadConfig === 'function') {
162
162
  return await api.config.loadConfig();
163
163
  }
164
- if (api?.runtime?.config && typeof api.runtime.config.loadConfig === 'function') {
165
- return await api.runtime.config.loadConfig();
166
- }
167
164
  if (api?.config && typeof api.config === 'object') {
168
165
  return api.config;
169
166
  }
167
+ if (api?.runtime?.config && typeof api.runtime.config.loadConfig === 'function') {
168
+ return await api.runtime.config.loadConfig();
169
+ }
170
170
  return {};
171
171
  }
172
172
 
@@ -237,6 +237,7 @@ export async function resolveToolContext(
237
237
  if (bindRuntime && typeof plugin.helpers?.resolveToolRuntimeContext === 'function') {
238
238
  const resolvedContext = await plugin.helpers.resolveToolRuntimeContext({
239
239
  cfg,
240
+ runtime: api?.runtime || null,
240
241
  accountId,
241
242
  runtimeConfig,
242
243
  agentId: normalizeText(params.agentId, runtimeConfig.relay?.agentId || null),
@@ -82,6 +82,8 @@ const CHAT_INBOX_FILTER_MODES = Object.freeze([
82
82
  ]);
83
83
  const CHAT_INBOX_FILTER_STATUSES = Object.freeze([
84
84
  'pending',
85
+ 'expired',
86
+ 'rejected',
85
87
  'opening',
86
88
  'ending',
87
89
  'active',
@@ -942,7 +944,7 @@ function buildRegisteredTools(api, plugin) {
942
944
  'The backend resolves the target by agentCode.',
943
945
  'If the current displayName for that agentCode no longer matches, the tool can still route by the current owner and return an explicit warning with the current displayName.',
944
946
  'Do not use this tool for replying inside an already-open Claworld chat, for runtime live turns, or for pulling progress from a local chat session.',
945
- 'After creation, use claworld_chat_inbox to inspect pending, opening, ending, active, silent, or ended status, or wait for the peer to accept.',
947
+ 'After creation, use claworld_chat_inbox to inspect pending, expired, rejected, opening, ending, active, silent, or ended status, or wait for the peer to accept.',
946
948
  'Once accepted, the runtime owns the live conversation loop.',
947
949
  ],
948
950
  examples: [
@@ -1018,12 +1020,13 @@ function buildRegisteredTools(api, plugin) {
1018
1020
  {
1019
1021
  name: 'claworld_chat_inbox',
1020
1022
  label: 'Claworld Chat Inbox',
1021
- description: 'Use in the main session to inspect Claworld inbox state or decide one pending chat request. Default action=list is query-only and returns current or recent chats plus local session references for internal tracking; action=accept or action=reject is the canonical request-decision surface. Do not use this tool to send a live message to the peer.',
1023
+ description: 'Use in the main session to inspect Claworld inbox state or decide one pending chat request. Default action=list is query-only and returns pending requests, recent terminal requests, plus current or recent chats with local session references for internal tracking; action=accept or action=reject is the canonical pending-request decision surface. Do not use this tool to send a live message to the peer.',
1022
1024
  metadata: buildToolMetadata({
1023
1025
  category: 'chat_request',
1024
1026
  usageNotes: [
1025
1027
  'Primary actor/session: main session. Default action=list is a status and query surface across inbound and outbound items.',
1026
- 'action=accept and action=reject are request-decision actions for pending requests. They do not send a freeform peer message.',
1028
+ 'list returns actionable pending requests, recent terminal requests such as expired/rejected, and current or recent chats.',
1029
+ 'action=accept and action=reject are request-decision actions for pending requests only. They do not send a freeform peer message.',
1027
1030
  'Use this tool to locate the relevant Claworld chat and the localSessionKey tied to it for internal tracking, summaries, orchestration, or follow-up against the host local session tools.',
1028
1031
  'localSessionKey is a local runtime reference only, not a transport address for sending a user message directly to the peer.',
1029
1032
  'Optional filters can narrow by direction, mode, status, worldId, chatRequestId, conversationKey, localSessionKey, or counterpartyAgentId.',
@@ -1040,7 +1043,7 @@ function buildRegisteredTools(api, plugin) {
1040
1043
  accountId: 'claworld',
1041
1044
  action: 'list',
1042
1045
  },
1043
- outcome: 'Returns all pending requests plus related chats for the current account.',
1046
+ outcome: 'Returns pending requests, recent terminal requests, and related chats for the current account.',
1044
1047
  },
1045
1048
  {
1046
1049
  title: 'Filter to active world chats',
@@ -1067,7 +1070,7 @@ function buildRegisteredTools(api, plugin) {
1067
1070
  ],
1068
1071
  }),
1069
1072
  parameters: objectParam({
1070
- description: 'In the main session, list Claworld inbox state or accept/reject one pending request for the current account. list is query-only; accept/reject are decision-only. Do not use this tool to send a live peer message.',
1073
+ description: 'In the main session, list Claworld inbox state or accept/reject one pending request for the current account. list is query-only and can include pending requests, recent terminal requests, and chats; accept/reject are decision-only for pending requests. Do not use this tool to send a live peer message.',
1071
1074
  required: ['accountId'],
1072
1075
  properties: {
1073
1076
  accountId: accountIdProperty,
@@ -1090,7 +1093,7 @@ function buildRegisteredTools(api, plugin) {
1090
1093
  examples: ['world'],
1091
1094
  }),
1092
1095
  status: stringParam({
1093
- description: 'Filter to pending requests or chats by current status.',
1096
+ description: 'Filter to pending or terminal requests, or to chats by current status.',
1094
1097
  enumValues: CHAT_INBOX_FILTER_STATUSES,
1095
1098
  examples: ['active'],
1096
1099
  }),
@@ -0,0 +1,190 @@
1
+ import { detectOpenclawHost } from '../host-version.js';
2
+ import { CLAWORLD_PLUGIN_CURRENT_VERSION } from '../plugin-version.js';
3
+ import { getClaworldRuntime } from '../plugin/runtime.js';
4
+
5
+ function normalizeText(value, fallback = null) {
6
+ if (value == null) return fallback;
7
+ const normalized = String(value).trim();
8
+ return normalized || fallback;
9
+ }
10
+
11
+ function normalizeObject(value) {
12
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
13
+ return value;
14
+ }
15
+
16
+ function readPath(source, path) {
17
+ let current = source;
18
+ for (const segment of path) {
19
+ if (!current || typeof current !== 'object' || Array.isArray(current)) {
20
+ return undefined;
21
+ }
22
+ current = current[segment];
23
+ }
24
+ return current;
25
+ }
26
+
27
+ function normalizeModelProvider(value, fallback = null) {
28
+ if (value == null) return fallback;
29
+ if (typeof value === 'string') return normalizeText(value, fallback);
30
+ if (typeof value === 'object' && !Array.isArray(value)) {
31
+ return normalizeText(value.provider, fallback);
32
+ }
33
+ return fallback;
34
+ }
35
+
36
+ function normalizeModelId(value, fallback = null) {
37
+ if (value == null) return fallback;
38
+ if (typeof value === 'string') return normalizeText(value, fallback);
39
+ if (typeof value === 'object' && !Array.isArray(value)) {
40
+ return normalizeText(value.id, normalizeText(value.modelId, normalizeText(value.name, fallback)));
41
+ }
42
+ return fallback;
43
+ }
44
+
45
+ function normalizeOsCategory(platform = process.platform) {
46
+ switch (platform) {
47
+ case 'darwin':
48
+ return 'macos';
49
+ case 'win32':
50
+ return 'windows';
51
+ case 'linux':
52
+ return 'linux';
53
+ default:
54
+ return 'other';
55
+ }
56
+ }
57
+
58
+ function resolveRuntimeCandidate(contextRuntime = null) {
59
+ const normalizedContextRuntime = normalizeObject(contextRuntime);
60
+ if (normalizedContextRuntime) return normalizedContextRuntime;
61
+ try {
62
+ return normalizeObject(getClaworldRuntime());
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ async function loadRuntimeConfig(runtime = null) {
69
+ if (!runtime?.config || typeof runtime.config.loadConfig !== 'function') {
70
+ return null;
71
+ }
72
+ try {
73
+ return normalizeObject(await runtime.config.loadConfig());
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ const MODEL_PROVIDER_PATHS = [
80
+ ['agent', 'defaults', 'provider'],
81
+ ['agent', 'defaults', 'modelProvider'],
82
+ ['agent', 'defaults', 'model', 'provider'],
83
+ ['defaults', 'provider'],
84
+ ['defaults', 'modelProvider'],
85
+ ['defaults', 'model', 'provider'],
86
+ ['runtime', 'agent', 'defaults', 'provider'],
87
+ ['runtime', 'agent', 'defaults', 'modelProvider'],
88
+ ['runtime', 'agent', 'defaults', 'model', 'provider'],
89
+ ['runtime', 'defaults', 'provider'],
90
+ ['runtime', 'defaults', 'modelProvider'],
91
+ ['runtime', 'defaults', 'model', 'provider'],
92
+ ['model', 'provider'],
93
+ ['provider'],
94
+ ['modelProvider'],
95
+ ];
96
+
97
+ const MODEL_ID_PATHS = [
98
+ ['agent', 'defaults', 'modelId'],
99
+ ['agent', 'defaults', 'model', 'id'],
100
+ ['agent', 'defaults', 'model', 'modelId'],
101
+ ['agent', 'defaults', 'model', 'name'],
102
+ ['agent', 'defaults', 'model'],
103
+ ['defaults', 'modelId'],
104
+ ['defaults', 'model', 'id'],
105
+ ['defaults', 'model', 'modelId'],
106
+ ['defaults', 'model', 'name'],
107
+ ['defaults', 'model'],
108
+ ['runtime', 'agent', 'defaults', 'modelId'],
109
+ ['runtime', 'agent', 'defaults', 'model', 'id'],
110
+ ['runtime', 'agent', 'defaults', 'model', 'modelId'],
111
+ ['runtime', 'agent', 'defaults', 'model', 'name'],
112
+ ['runtime', 'agent', 'defaults', 'model'],
113
+ ['runtime', 'defaults', 'modelId'],
114
+ ['runtime', 'defaults', 'model', 'id'],
115
+ ['runtime', 'defaults', 'model', 'modelId'],
116
+ ['runtime', 'defaults', 'model', 'name'],
117
+ ['runtime', 'defaults', 'model'],
118
+ ['modelId'],
119
+ ['model', 'id'],
120
+ ['model', 'modelId'],
121
+ ['model', 'name'],
122
+ ['model'],
123
+ ];
124
+
125
+ const OPENCLAW_VERSION_PATHS = [
126
+ ['host', 'version'],
127
+ ['openclaw', 'version'],
128
+ ['meta', 'hostVersion'],
129
+ ['meta', 'openclawVersion'],
130
+ ['hostVersion'],
131
+ ];
132
+
133
+ function pickDiagnosticValue(sources, paths, normalizer) {
134
+ for (const source of sources) {
135
+ const normalizedSource = normalizeObject(source);
136
+ if (!normalizedSource) continue;
137
+ for (const path of paths) {
138
+ const value = readPath(normalizedSource, path);
139
+ const normalizedValue = normalizer(value, null);
140
+ if (normalizedValue) return normalizedValue;
141
+ }
142
+ }
143
+ return null;
144
+ }
145
+
146
+ function resolveModelDiagnostics(sources = []) {
147
+ return {
148
+ modelProvider: pickDiagnosticValue(sources, MODEL_PROVIDER_PATHS, normalizeModelProvider),
149
+ modelId: pickDiagnosticValue(sources, MODEL_ID_PATHS, normalizeModelId),
150
+ };
151
+ }
152
+
153
+ let cachedOpenclawVersionPromise = null;
154
+
155
+ async function detectOpenclawVersion(runtime = null) {
156
+ const exposedVersion = pickDiagnosticValue([runtime], OPENCLAW_VERSION_PATHS, normalizeText);
157
+ if (exposedVersion) return exposedVersion;
158
+ if (!runtime) return null;
159
+ if (!cachedOpenclawVersionPromise) {
160
+ cachedOpenclawVersionPromise = detectOpenclawHost()
161
+ .then((result) => normalizeText(result?.version, null))
162
+ .catch(() => null);
163
+ }
164
+ return cachedOpenclawVersionPromise;
165
+ }
166
+
167
+ export async function collectFeedbackDiagnostics({
168
+ cfg = {},
169
+ runtime = null,
170
+ pluginVersion = null,
171
+ } = {}) {
172
+ const resolvedRuntime = resolveRuntimeCandidate(runtime);
173
+ const [loadedConfig, openclawVersion] = await Promise.all([
174
+ loadRuntimeConfig(resolvedRuntime),
175
+ detectOpenclawVersion(resolvedRuntime),
176
+ ]);
177
+ const modelDiagnostics = resolveModelDiagnostics([
178
+ resolvedRuntime,
179
+ loadedConfig,
180
+ cfg,
181
+ ]);
182
+
183
+ return {
184
+ openclawVersion: normalizeText(openclawVersion, null),
185
+ pluginVersion: normalizeText(pluginVersion, CLAWORLD_PLUGIN_CURRENT_VERSION),
186
+ modelProvider: normalizeText(modelDiagnostics.modelProvider, null),
187
+ modelId: normalizeText(modelDiagnostics.modelId, null),
188
+ osCategory: normalizeOsCategory(process.platform),
189
+ };
190
+ }
@@ -1,6 +1,7 @@
1
1
  import { resolveClaworldRuntimeConfig } from '../plugin/config-schema.js';
2
2
  import { buildRuntimeAuthHeaders } from '../plugin/account-identity.js';
3
3
  import { createRuntimeBoundaryError } from '../../lib/runtime-errors.js';
4
+ import { collectFeedbackDiagnostics } from './feedback-diagnostics.js';
4
5
 
5
6
  function normalizeText(value, fallback = null) {
6
7
  if (value == null) return fallback;
@@ -62,6 +63,7 @@ export async function submitFeedbackReport({
62
63
  cfg = {},
63
64
  accountId = null,
64
65
  runtimeConfig = null,
66
+ runtime = null,
65
67
  agentId = null,
66
68
  category = null,
67
69
  title = null,
@@ -89,6 +91,11 @@ export async function submitFeedbackReport({
89
91
 
90
92
  const normalizedContext = normalizeObject(context);
91
93
  const resolvedRuntimeConfig = runtimeConfig || resolveClaworldRuntimeConfig(cfg, accountId);
94
+ const diagnostics = await collectFeedbackDiagnostics({
95
+ cfg,
96
+ runtime,
97
+ pluginVersion,
98
+ });
92
99
  const baseUrl = normalizeRelayHttpBaseUrl(resolvedRuntimeConfig.serverUrl);
93
100
  const result = await fetchJson(fetchImpl, `${baseUrl}/v1/feedback`, {
94
101
  method: 'POST',
@@ -122,7 +129,7 @@ export async function submitFeedbackReport({
122
129
  channelId: 'claworld',
123
130
  toolName: 'claworld_submit_feedback',
124
131
  toolCallId: normalizeText(toolCallId, null),
125
- pluginVersion: normalizeText(pluginVersion, null),
132
+ ...diagnostics,
126
133
  toolContractVersion: normalizeText(toolContractVersion, null),
127
134
  accountId: normalizeText(resolvedRuntimeConfig.accountId, normalizeText(accountId, null)),
128
135
  serverUrl: baseUrl,
@@ -702,7 +702,11 @@ export function projectToolFeedbackSubmissionResponse(result = {}) {
702
702
  channelId: normalizeText(runtimeContext.channelId, null),
703
703
  toolName: normalizeText(runtimeContext.toolName, null),
704
704
  toolCallId: normalizeText(runtimeContext.toolCallId, null),
705
+ openclawVersion: normalizeText(runtimeContext.openclawVersion, null),
705
706
  pluginVersion: normalizeText(runtimeContext.pluginVersion, null),
707
+ modelProvider: normalizeText(runtimeContext.modelProvider, null),
708
+ modelId: normalizeText(runtimeContext.modelId, null),
709
+ osCategory: normalizeText(runtimeContext.osCategory, null),
706
710
  },
707
711
  nextAction: 'keep_feedback_id_for_follow_up',
708
712
  };
@@ -854,6 +858,18 @@ function projectChatInboxFilters(filters = {}) {
854
858
  function projectChatInboxCountBlock(counts = {}, fallback = {}) {
855
859
  return {
856
860
  pendingRequestCount: normalizeInteger(counts.pendingRequestCount, fallback.pendingRequestCount ?? 0),
861
+ recentRequestCount: normalizeInteger(counts.recentRequestCount, fallback.recentRequestCount ?? 0),
862
+ recentRequestStatusCounts: counts.recentRequestStatusCounts
863
+ && typeof counts.recentRequestStatusCounts === 'object'
864
+ && !Array.isArray(counts.recentRequestStatusCounts)
865
+ ? {
866
+ expired: normalizeInteger(counts.recentRequestStatusCounts.expired, 0),
867
+ rejected: normalizeInteger(counts.recentRequestStatusCounts.rejected, 0),
868
+ }
869
+ : {
870
+ expired: 0,
871
+ rejected: 0,
872
+ },
857
873
  chatCount: normalizeInteger(counts.chatCount, fallback.chatCount ?? 0),
858
874
  chatStatusCounts: counts.chatStatusCounts && typeof counts.chatStatusCounts === 'object' && !Array.isArray(counts.chatStatusCounts)
859
875
  ? {
@@ -914,16 +930,21 @@ export function projectToolChatInboxResponse(result = {}, { accountId = null } =
914
930
  const pendingRequests = Array.isArray(result.pendingRequests)
915
931
  ? result.pendingRequests.map((request) => projectChatRequestItem(request)).filter(Boolean)
916
932
  : [];
933
+ const recentRequests = Array.isArray(result.recentRequests)
934
+ ? result.recentRequests.map((request) => projectChatRequestItem(request)).filter(Boolean)
935
+ : [];
917
936
  const chats = Array.isArray(result.chats)
918
937
  ? result.chats.map((chat) => projectChatInboxChatItem(chat)).filter(Boolean)
919
938
  : [];
920
939
  const projectedFilters = projectChatInboxFilters(result.filters);
921
940
  const globalCounts = projectChatInboxCountBlock(result.counts?.global, {
922
941
  pendingRequestCount: pendingRequests.length,
942
+ recentRequestCount: recentRequests.length,
923
943
  chatCount: chats.length,
924
944
  });
925
945
  const filteredCounts = projectChatInboxCountBlock(result.counts?.filtered, {
926
946
  pendingRequestCount: pendingRequests.length,
947
+ recentRequestCount: recentRequests.length,
927
948
  chatCount: chats.length,
928
949
  });
929
950
  return {
@@ -934,6 +955,7 @@ export function projectToolChatInboxResponse(result = {}, { accountId = null } =
934
955
  filtered: filteredCounts,
935
956
  },
936
957
  pendingRequests,
958
+ recentRequests,
937
959
  chats,
938
960
  };
939
961
  }