rol-websocket-channel 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MQTT-API.md +967 -0
- package/dist/index.js +430 -0
- package/dist/message-handler.js +327 -0
- package/dist/src/admin/cli.js +43 -0
- package/dist/src/admin/jsonrpc.js +60 -0
- package/dist/src/admin/lib/fs.js +30 -0
- package/dist/src/admin/lib/paths.js +46 -0
- package/dist/src/admin/methods/admin.js +60 -0
- package/dist/src/admin/methods/agents-extended.js +235 -0
- package/dist/src/admin/methods/index.js +69 -0
- package/dist/src/admin/methods/memory.js +360 -0
- package/dist/src/admin/methods/models-extended.js +107 -0
- package/dist/src/admin/methods/models.js +39 -0
- package/dist/src/admin/methods/sessions-extended.js +207 -0
- package/dist/src/admin/methods/sessions.js +64 -0
- package/dist/src/admin/methods/skills-extended.js +157 -0
- package/dist/src/admin/methods/skills-toggle.js +182 -0
- package/dist/src/admin/methods/skills.js +384 -0
- package/dist/src/admin/methods/system.js +178 -0
- package/dist/src/admin/methods/usage.js +1170 -0
- package/dist/src/admin/types.js +1 -0
- package/dist/src/mqtt/connection-manager.js +155 -0
- package/dist/src/mqtt/index.js +5 -0
- package/dist/src/mqtt/mqtt-client.js +86 -0
- package/dist/src/mqtt/types.js +2 -0
- package/dist/src/shared/context.js +24 -0
- package/dist/src/shared/wrapper.js +23 -0
- package/index.ts +514 -0
- package/message-handler.ts +415 -0
- package/openclaw.plugin.json +84 -0
- package/package.json +35 -0
- package/readme.md +32 -0
- package/src/admin/cli.ts +60 -0
- package/src/admin/jsonrpc.ts +88 -0
- package/src/admin/lib/fs.ts +35 -0
- package/src/admin/lib/paths.ts +61 -0
- package/src/admin/methods/admin.ts +95 -0
- package/src/admin/methods/agents-extended.ts +310 -0
- package/src/admin/methods/index.ts +103 -0
- package/src/admin/methods/memory.ts +546 -0
- package/src/admin/methods/models-extended.ts +191 -0
- package/src/admin/methods/models.ts +103 -0
- package/src/admin/methods/sessions-extended.ts +313 -0
- package/src/admin/methods/sessions.ts +122 -0
- package/src/admin/methods/skills-extended.ts +249 -0
- package/src/admin/methods/skills-toggle.ts +235 -0
- package/src/admin/methods/skills.ts +651 -0
- package/src/admin/methods/system.ts +203 -0
- package/src/admin/methods/usage.ts +1491 -0
- package/src/admin/types.ts +46 -0
- package/src/mqtt/connection-manager.ts +188 -0
- package/src/mqtt/index.ts +6 -0
- package/src/mqtt/mqtt-client.ts +119 -0
- package/src/mqtt/types.ts +36 -0
- package/src/shared/context.ts +33 -0
- package/src/shared/wrapper.ts +35 -0
- package/tsconfig.json +16 -0
- package/types/openclaw.d.ts +74 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { readJsonFile, writeJsonFile } from '../lib/fs.ts';
|
|
4
|
+
import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.ts';
|
|
5
|
+
import type { JsonValue, MethodHandler } from '../types.ts';
|
|
6
|
+
|
|
7
|
+
interface OpenClawConfig {
|
|
8
|
+
models?: {
|
|
9
|
+
mode?: string;
|
|
10
|
+
providers?: Record<string, any>;
|
|
11
|
+
};
|
|
12
|
+
agents?: {
|
|
13
|
+
defaults?: {
|
|
14
|
+
model?: {
|
|
15
|
+
primary?: string;
|
|
16
|
+
provider?: string;
|
|
17
|
+
[key: string]: any;
|
|
18
|
+
};
|
|
19
|
+
[key: string]: any;
|
|
20
|
+
};
|
|
21
|
+
[key: string]: any;
|
|
22
|
+
};
|
|
23
|
+
[key: string]: any;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 允许修改的 provider 配置字段白名单
|
|
27
|
+
const ALLOWED_PROVIDER_FIELDS: Record<string, boolean> = {
|
|
28
|
+
'apiKey': true,
|
|
29
|
+
'baseUrl': true,
|
|
30
|
+
'model': true,
|
|
31
|
+
'temperature': true,
|
|
32
|
+
'maxTokens': true,
|
|
33
|
+
'topP': true,
|
|
34
|
+
'frequencyPenalty': true,
|
|
35
|
+
'presencePenalty': true,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 更新模型配置
|
|
40
|
+
* 支持修改 primary model 和 provider 配置
|
|
41
|
+
*/
|
|
42
|
+
export const updateModels: MethodHandler = async (
|
|
43
|
+
params,
|
|
44
|
+
context
|
|
45
|
+
): Promise<JsonValue> => {
|
|
46
|
+
const objectParams = isObject(params) ? params : {};
|
|
47
|
+
const primaryModel = pickString(objectParams.primaryModel);
|
|
48
|
+
const modelProvider = pickString(objectParams.modelProvider);
|
|
49
|
+
const provider = pickString(objectParams.provider);
|
|
50
|
+
const providerConfig = isObject(objectParams.providerConfig) ? objectParams.providerConfig : null;
|
|
51
|
+
|
|
52
|
+
const configPath = path.join(context.openclawRoot, 'openclaw.json');
|
|
53
|
+
const config = await readJsonFile<OpenClawConfig>(configPath);
|
|
54
|
+
|
|
55
|
+
let updated = false;
|
|
56
|
+
const changes: string[] = [];
|
|
57
|
+
|
|
58
|
+
// 更新 primary model
|
|
59
|
+
if (primaryModel) {
|
|
60
|
+
if (!config.agents) config.agents = {};
|
|
61
|
+
if (!config.agents.defaults) config.agents.defaults = {};
|
|
62
|
+
if (!config.agents.defaults.model) config.agents.defaults.model = {};
|
|
63
|
+
|
|
64
|
+
config.agents.defaults.model.primary = primaryModel;
|
|
65
|
+
const inferredProvider = modelProvider ?? inferProviderFromPrimaryModel(primaryModel);
|
|
66
|
+
if (inferredProvider) {
|
|
67
|
+
config.agents.defaults.model.provider = inferredProvider;
|
|
68
|
+
changes.push(`Updated default model provider to: ${inferredProvider}`);
|
|
69
|
+
}
|
|
70
|
+
updated = true;
|
|
71
|
+
changes.push(`Updated primary model to: ${primaryModel}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (modelProvider && !primaryModel) {
|
|
75
|
+
if (!config.agents) config.agents = {};
|
|
76
|
+
if (!config.agents.defaults) config.agents.defaults = {};
|
|
77
|
+
if (!config.agents.defaults.model) config.agents.defaults.model = {};
|
|
78
|
+
|
|
79
|
+
config.agents.defaults.model.provider = modelProvider;
|
|
80
|
+
updated = true;
|
|
81
|
+
changes.push(`Updated default model provider to: ${modelProvider}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 更新 provider 配置
|
|
85
|
+
if (provider && providerConfig) {
|
|
86
|
+
if (!config.models) config.models = {};
|
|
87
|
+
if (!config.models.providers) config.models.providers = {};
|
|
88
|
+
|
|
89
|
+
// 验证字段白名单
|
|
90
|
+
for (const field of Object.keys(providerConfig)) {
|
|
91
|
+
if (!ALLOWED_PROVIDER_FIELDS[field]) {
|
|
92
|
+
throw new JsonRpcException(
|
|
93
|
+
JSON_RPC_ERRORS.invalidParams,
|
|
94
|
+
`Field not allowed for provider config: ${field}. Allowed fields: ${Object.keys(ALLOWED_PROVIDER_FIELDS).join(', ')}`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 合并配置
|
|
100
|
+
if (!config.models.providers[provider]) {
|
|
101
|
+
config.models.providers[provider] = {};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
config.models.providers[provider] = {
|
|
105
|
+
...config.models.providers[provider],
|
|
106
|
+
...providerConfig
|
|
107
|
+
};
|
|
108
|
+
updated = true;
|
|
109
|
+
changes.push(`Updated provider config for: ${provider}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!updated) {
|
|
113
|
+
throw new JsonRpcException(
|
|
114
|
+
JSON_RPC_ERRORS.invalidParams,
|
|
115
|
+
'No valid update parameters provided. Use primaryModel, modelProvider, or provider+providerConfig.'
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 写回配置文件
|
|
120
|
+
await writeJsonFile(configPath, config);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
success: true,
|
|
124
|
+
configFile: configPath,
|
|
125
|
+
changes,
|
|
126
|
+
updatedConfig: {
|
|
127
|
+
primaryModel: config.agents?.defaults?.model?.primary ?? null,
|
|
128
|
+
modelProvider: config.agents?.defaults?.model?.provider ?? null,
|
|
129
|
+
defaultModel: config.agents?.defaults?.model ?? {},
|
|
130
|
+
providers: redactSecrets(config.models?.providers ?? {})
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
function inferProviderFromPrimaryModel(primaryModel: string): string | null {
|
|
136
|
+
const slashIndex = primaryModel.indexOf('/');
|
|
137
|
+
if (slashIndex <= 0) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return primaryModel.slice(0, slashIndex);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function pickString(value: JsonValue | undefined): string | null {
|
|
145
|
+
if (typeof value !== 'string') {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const trimmed = value.trim();
|
|
150
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isObject(value: JsonValue | undefined): value is Record<string, JsonValue> {
|
|
154
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function redactSecrets(value: any): any {
|
|
158
|
+
if (Array.isArray(value)) {
|
|
159
|
+
return value.map(redactSecrets);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!value || typeof value !== 'object') {
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const result: Record<string, any> = {};
|
|
167
|
+
|
|
168
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
169
|
+
if (isSecretKey(key) && typeof nestedValue === 'string') {
|
|
170
|
+
result[key] = redactString(nestedValue);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
result[key] = redactSecrets(nestedValue);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isSecretKey(key: string): boolean {
|
|
181
|
+
const normalized = key.toLowerCase();
|
|
182
|
+
return normalized.includes('apikey') || normalized.includes('token') || normalized.includes('secret');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function redactString(value: string): string {
|
|
186
|
+
if (value.length <= 8) {
|
|
187
|
+
return '********';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return `${value.slice(0, 4)}***${value.slice(-4)}`;
|
|
191
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { readJsonFile } from '../lib/fs.ts';
|
|
4
|
+
import type { JsonValue, MethodHandler } from '../types.ts';
|
|
5
|
+
|
|
6
|
+
interface OpenClawConfig {
|
|
7
|
+
models?: {
|
|
8
|
+
mode?: string;
|
|
9
|
+
providers?: JsonValue;
|
|
10
|
+
};
|
|
11
|
+
agents?: {
|
|
12
|
+
defaults?: {
|
|
13
|
+
model?: {
|
|
14
|
+
primary?: string;
|
|
15
|
+
provider?: string;
|
|
16
|
+
[key: string]: any;
|
|
17
|
+
};
|
|
18
|
+
[key: string]: any;
|
|
19
|
+
};
|
|
20
|
+
list?: Array<{
|
|
21
|
+
id?: string;
|
|
22
|
+
name?: string;
|
|
23
|
+
model?: {
|
|
24
|
+
primary?: string;
|
|
25
|
+
provider?: string;
|
|
26
|
+
[key: string]: any;
|
|
27
|
+
};
|
|
28
|
+
[key: string]: any;
|
|
29
|
+
}>;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const getModels: MethodHandler = async (_params, context): Promise<JsonValue> => {
|
|
34
|
+
const configPath = path.join(context.openclawRoot, 'openclaw.json');
|
|
35
|
+
const config = await readJsonFile<OpenClawConfig>(configPath);
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
sourceConfigFile: configPath,
|
|
39
|
+
defaults: {
|
|
40
|
+
model: config.agents?.defaults?.model ?? {}
|
|
41
|
+
},
|
|
42
|
+
agents: normalizeAgentModels(config),
|
|
43
|
+
modelConfigMode: config.models?.mode ?? null,
|
|
44
|
+
configuredProviders: redactSecrets(config.models?.providers ?? {})
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function normalizeAgentModels(config: OpenClawConfig): JsonValue[] {
|
|
49
|
+
const items: JsonValue[] = [
|
|
50
|
+
{
|
|
51
|
+
id: 'defaults',
|
|
52
|
+
name: 'defaults',
|
|
53
|
+
model: config.agents?.defaults?.model ?? {}
|
|
54
|
+
}
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const agents = Array.isArray(config.agents?.list) ? config.agents.list : [];
|
|
58
|
+
for (const agent of agents) {
|
|
59
|
+
items.push({
|
|
60
|
+
id: agent.id ?? null,
|
|
61
|
+
name: agent.name ?? agent.id ?? null,
|
|
62
|
+
model: agent.model ?? {}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return items;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function redactSecrets(value: JsonValue): JsonValue {
|
|
70
|
+
if (Array.isArray(value)) {
|
|
71
|
+
return value.map(redactSecrets);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!value || typeof value !== 'object') {
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const result: Record<string, JsonValue> = {};
|
|
79
|
+
|
|
80
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
81
|
+
if (isSecretKey(key) && typeof nestedValue === 'string') {
|
|
82
|
+
result[key] = redactString(nestedValue);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
result[key] = redactSecrets(nestedValue);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isSecretKey(key: string): boolean {
|
|
93
|
+
const normalized = key.toLowerCase();
|
|
94
|
+
return normalized.includes('apikey') || normalized.includes('token') || normalized.includes('secret');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function redactString(value: string): string {
|
|
98
|
+
if (value.length <= 8) {
|
|
99
|
+
return '********';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return `${value.slice(0, 4)}***${value.slice(-4)}`;
|
|
103
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import readline from 'node:readline';
|
|
4
|
+
|
|
5
|
+
import { pathExists, readJsonFile } from '../lib/fs.ts';
|
|
6
|
+
import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.ts';
|
|
7
|
+
import type { JsonValue, MethodContext, MethodHandler } from '../types.ts';
|
|
8
|
+
import { findSessionRecord } from './sessions.ts';
|
|
9
|
+
|
|
10
|
+
interface SessionMessage {
|
|
11
|
+
role: string;
|
|
12
|
+
content: string;
|
|
13
|
+
timestamp?: number;
|
|
14
|
+
[key: string]: any;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const MAX_SESSION_MESSAGES = 100;
|
|
18
|
+
|
|
19
|
+
export const getSession: MethodHandler = async (
|
|
20
|
+
params,
|
|
21
|
+
context
|
|
22
|
+
): Promise<JsonValue> => {
|
|
23
|
+
const objectParams = isObject(params) ? params : {};
|
|
24
|
+
const sessionId = typeof objectParams.sessionId === 'string' ? objectParams.sessionId : null;
|
|
25
|
+
const requestedLimit = typeof objectParams.limit === 'number' ? objectParams.limit : MAX_SESSION_MESSAGES;
|
|
26
|
+
const requestedOffset = typeof objectParams.offset === 'number' ? objectParams.offset : 0;
|
|
27
|
+
const limit = normalizePageSize(requestedLimit, MAX_SESSION_MESSAGES);
|
|
28
|
+
const offset = normalizeOffset(requestedOffset);
|
|
29
|
+
|
|
30
|
+
if (!sessionId) {
|
|
31
|
+
throw new JsonRpcException(
|
|
32
|
+
JSON_RPC_ERRORS.invalidParams,
|
|
33
|
+
'Missing required parameter: sessionId'
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const session = await findSessionRecord(context, sessionId);
|
|
38
|
+
if (!session) {
|
|
39
|
+
throw new JsonRpcException(
|
|
40
|
+
JSON_RPC_ERRORS.invalidParams,
|
|
41
|
+
`Session not found: ${sessionId}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const sessionFile = session.sessionFilePath ?? path.join(
|
|
46
|
+
context.openclawRoot,
|
|
47
|
+
'agents',
|
|
48
|
+
session.agentName,
|
|
49
|
+
'sessions',
|
|
50
|
+
`${session.sessionId ?? sessionId}.jsonl`
|
|
51
|
+
);
|
|
52
|
+
const messages = await readSessionMessages(sessionFile, limit, offset);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
agentName: session.agentName,
|
|
56
|
+
sessionId: session.sessionId ?? sessionId,
|
|
57
|
+
sessionKey: session.sessionKey,
|
|
58
|
+
updatedAt: session.updatedAt,
|
|
59
|
+
status: session.status,
|
|
60
|
+
provider: session.provider,
|
|
61
|
+
model: session.model,
|
|
62
|
+
workspaceDir: session.workspaceDir,
|
|
63
|
+
originLabel: session.originLabel,
|
|
64
|
+
chatType: session.chatType,
|
|
65
|
+
messages: {
|
|
66
|
+
total: messages.total,
|
|
67
|
+
limit,
|
|
68
|
+
offset,
|
|
69
|
+
items: messages.items
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const prepareMessage: MethodHandler = async (
|
|
75
|
+
params,
|
|
76
|
+
context
|
|
77
|
+
): Promise<JsonValue> => {
|
|
78
|
+
const objectParams = isObject(params) ? params : {};
|
|
79
|
+
const sessionId = typeof objectParams.sessionId === 'string' ? objectParams.sessionId : null;
|
|
80
|
+
const message = typeof objectParams.message === 'string' ? objectParams.message : null;
|
|
81
|
+
const attachedSkills = Array.isArray(objectParams.attachedSkills) ? objectParams.attachedSkills : [];
|
|
82
|
+
|
|
83
|
+
if (!sessionId) {
|
|
84
|
+
throw new JsonRpcException(
|
|
85
|
+
JSON_RPC_ERRORS.invalidParams,
|
|
86
|
+
'Missing required parameter: sessionId'
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!message) {
|
|
91
|
+
throw new JsonRpcException(
|
|
92
|
+
JSON_RPC_ERRORS.invalidParams,
|
|
93
|
+
'Missing required parameter: message'
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const session = await findSessionRecord(context, sessionId);
|
|
98
|
+
if (!session) {
|
|
99
|
+
throw new JsonRpcException(
|
|
100
|
+
JSON_RPC_ERRORS.invalidParams,
|
|
101
|
+
`Session not found: ${sessionId}`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const messageData: any = {
|
|
106
|
+
agentName: session.agentName,
|
|
107
|
+
sessionId: session.sessionId ?? sessionId,
|
|
108
|
+
sessionKey: session.sessionKey,
|
|
109
|
+
message,
|
|
110
|
+
timestamp: Date.now()
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (attachedSkills.length > 0) {
|
|
114
|
+
messageData.attachedSkills = attachedSkills;
|
|
115
|
+
const skillContexts = await loadSkillContexts(context, attachedSkills);
|
|
116
|
+
if (skillContexts.length > 0) {
|
|
117
|
+
messageData.skillContexts = skillContexts;
|
|
118
|
+
messageData.enhancedMessage = buildEnhancedMessage(message, skillContexts);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
success: true,
|
|
124
|
+
messageData,
|
|
125
|
+
note: 'Message data prepared. To actually send, use MQTT sender message or OpenClaw runtime API.'
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const attachSkill: MethodHandler = async (
|
|
130
|
+
params,
|
|
131
|
+
context
|
|
132
|
+
): Promise<JsonValue> => {
|
|
133
|
+
const objectParams = isObject(params) ? params : {};
|
|
134
|
+
const sessionId = typeof objectParams.sessionId === 'string' ? objectParams.sessionId : null;
|
|
135
|
+
const skillSlug = typeof objectParams.skillSlug === 'string' ? objectParams.skillSlug : null;
|
|
136
|
+
const message = typeof objectParams.message === 'string' ? objectParams.message : '';
|
|
137
|
+
|
|
138
|
+
if (!sessionId) {
|
|
139
|
+
throw new JsonRpcException(
|
|
140
|
+
JSON_RPC_ERRORS.invalidParams,
|
|
141
|
+
'Missing required parameter: sessionId'
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!skillSlug) {
|
|
146
|
+
throw new JsonRpcException(
|
|
147
|
+
JSON_RPC_ERRORS.invalidParams,
|
|
148
|
+
'Missing required parameter: skillSlug'
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const skillPath = await findInstalledSkill(context, skillSlug);
|
|
153
|
+
if (!skillPath) {
|
|
154
|
+
throw new JsonRpcException(
|
|
155
|
+
JSON_RPC_ERRORS.invalidParams,
|
|
156
|
+
`Skill not installed: ${skillSlug}`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const skillContext = await loadSkillContext(context, skillSlug, skillPath);
|
|
161
|
+
const enhancedMessage = message
|
|
162
|
+
? `${message}\n\n[Attached Skill: ${skillSlug}]`
|
|
163
|
+
: `[Using Skill: ${skillSlug}]`;
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
success: true,
|
|
167
|
+
sessionId,
|
|
168
|
+
skillSlug,
|
|
169
|
+
skillContext,
|
|
170
|
+
enhancedMessage,
|
|
171
|
+
messageData: {
|
|
172
|
+
sessionId,
|
|
173
|
+
message: enhancedMessage,
|
|
174
|
+
attachedSkills: [{ slug: skillSlug }],
|
|
175
|
+
skillContexts: [skillContext],
|
|
176
|
+
timestamp: Date.now()
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
function isObject(value: JsonValue | undefined): value is Record<string, JsonValue> {
|
|
182
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizePageSize(value: number, max: number): number {
|
|
186
|
+
if (!Number.isFinite(value)) {
|
|
187
|
+
return max;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const normalized = Math.floor(value);
|
|
191
|
+
if (normalized <= 0) {
|
|
192
|
+
return max;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return Math.min(normalized, max);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function normalizeOffset(value: number): number {
|
|
199
|
+
if (!Number.isFinite(value)) {
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return Math.max(0, Math.floor(value));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function readSessionMessages(
|
|
207
|
+
filePath: string,
|
|
208
|
+
limit: number,
|
|
209
|
+
offset: number
|
|
210
|
+
): Promise<{ total: number; items: SessionMessage[] }> {
|
|
211
|
+
if (!(await pathExists(filePath))) {
|
|
212
|
+
return { total: 0, items: [] };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const messages: SessionMessage[] = [];
|
|
216
|
+
const fileStream = fs.createReadStream(filePath);
|
|
217
|
+
const rl = readline.createInterface({
|
|
218
|
+
input: fileStream,
|
|
219
|
+
crlfDelay: Infinity
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
for await (const line of rl) {
|
|
223
|
+
if (!line.trim()) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
messages.push(JSON.parse(line));
|
|
229
|
+
} catch {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
total: messages.length,
|
|
236
|
+
items: messages.slice(offset, offset + limit)
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function findInstalledSkill(
|
|
241
|
+
context: MethodContext,
|
|
242
|
+
skillSlug: string
|
|
243
|
+
): Promise<string | null> {
|
|
244
|
+
const globalPath = path.join(context.openclawRoot, 'skills', skillSlug);
|
|
245
|
+
if (await pathExists(globalPath)) {
|
|
246
|
+
return globalPath;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const workspacePath = path.join(context.openclawRoot, 'workspace', '.openclaw', 'skills', skillSlug);
|
|
250
|
+
if (await pathExists(workspacePath)) {
|
|
251
|
+
return workspacePath;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function loadSkillContext(
|
|
258
|
+
context: MethodContext,
|
|
259
|
+
skillSlug: string,
|
|
260
|
+
skillPath: string
|
|
261
|
+
): Promise<any> {
|
|
262
|
+
const skillMdPath = path.join(skillPath, 'SKILL.md');
|
|
263
|
+
let content = '';
|
|
264
|
+
|
|
265
|
+
if (await pathExists(skillMdPath)) {
|
|
266
|
+
content = await fs.promises.readFile(skillMdPath, 'utf-8');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const skillJsonPath = path.join(skillPath, 'skill.json');
|
|
270
|
+
let metadata: any = { slug: skillSlug };
|
|
271
|
+
|
|
272
|
+
if (await pathExists(skillJsonPath)) {
|
|
273
|
+
metadata = await readJsonFile(skillJsonPath);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
slug: skillSlug,
|
|
278
|
+
path: skillPath,
|
|
279
|
+
content,
|
|
280
|
+
metadata
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function loadSkillContexts(
|
|
285
|
+
context: MethodContext,
|
|
286
|
+
skills: any[]
|
|
287
|
+
): Promise<any[]> {
|
|
288
|
+
const contexts: any[] = [];
|
|
289
|
+
|
|
290
|
+
for (const skill of skills) {
|
|
291
|
+
const skillSlug = typeof skill === 'string' ? skill : skill?.slug;
|
|
292
|
+
if (!skillSlug) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const skillPath = await findInstalledSkill(context, skillSlug);
|
|
297
|
+
if (!skillPath) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
contexts.push(await loadSkillContext(context, skillSlug, skillPath));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return contexts;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function buildEnhancedMessage(message: string, skillContexts: any[]): string {
|
|
308
|
+
const skillSections = skillContexts
|
|
309
|
+
.map((ctx) => `\n\n--- Skill: ${ctx.slug} ---\n${ctx.content}`)
|
|
310
|
+
.join('\n');
|
|
311
|
+
|
|
312
|
+
return `${message}${skillSections}`;
|
|
313
|
+
}
|