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.
Files changed (58) hide show
  1. package/MQTT-API.md +967 -0
  2. package/dist/index.js +430 -0
  3. package/dist/message-handler.js +327 -0
  4. package/dist/src/admin/cli.js +43 -0
  5. package/dist/src/admin/jsonrpc.js +60 -0
  6. package/dist/src/admin/lib/fs.js +30 -0
  7. package/dist/src/admin/lib/paths.js +46 -0
  8. package/dist/src/admin/methods/admin.js +60 -0
  9. package/dist/src/admin/methods/agents-extended.js +235 -0
  10. package/dist/src/admin/methods/index.js +69 -0
  11. package/dist/src/admin/methods/memory.js +360 -0
  12. package/dist/src/admin/methods/models-extended.js +107 -0
  13. package/dist/src/admin/methods/models.js +39 -0
  14. package/dist/src/admin/methods/sessions-extended.js +207 -0
  15. package/dist/src/admin/methods/sessions.js +64 -0
  16. package/dist/src/admin/methods/skills-extended.js +157 -0
  17. package/dist/src/admin/methods/skills-toggle.js +182 -0
  18. package/dist/src/admin/methods/skills.js +384 -0
  19. package/dist/src/admin/methods/system.js +178 -0
  20. package/dist/src/admin/methods/usage.js +1170 -0
  21. package/dist/src/admin/types.js +1 -0
  22. package/dist/src/mqtt/connection-manager.js +155 -0
  23. package/dist/src/mqtt/index.js +5 -0
  24. package/dist/src/mqtt/mqtt-client.js +86 -0
  25. package/dist/src/mqtt/types.js +2 -0
  26. package/dist/src/shared/context.js +24 -0
  27. package/dist/src/shared/wrapper.js +23 -0
  28. package/index.ts +514 -0
  29. package/message-handler.ts +415 -0
  30. package/openclaw.plugin.json +84 -0
  31. package/package.json +35 -0
  32. package/readme.md +32 -0
  33. package/src/admin/cli.ts +60 -0
  34. package/src/admin/jsonrpc.ts +88 -0
  35. package/src/admin/lib/fs.ts +35 -0
  36. package/src/admin/lib/paths.ts +61 -0
  37. package/src/admin/methods/admin.ts +95 -0
  38. package/src/admin/methods/agents-extended.ts +310 -0
  39. package/src/admin/methods/index.ts +103 -0
  40. package/src/admin/methods/memory.ts +546 -0
  41. package/src/admin/methods/models-extended.ts +191 -0
  42. package/src/admin/methods/models.ts +103 -0
  43. package/src/admin/methods/sessions-extended.ts +313 -0
  44. package/src/admin/methods/sessions.ts +122 -0
  45. package/src/admin/methods/skills-extended.ts +249 -0
  46. package/src/admin/methods/skills-toggle.ts +235 -0
  47. package/src/admin/methods/skills.ts +651 -0
  48. package/src/admin/methods/system.ts +203 -0
  49. package/src/admin/methods/usage.ts +1491 -0
  50. package/src/admin/types.ts +46 -0
  51. package/src/mqtt/connection-manager.ts +188 -0
  52. package/src/mqtt/index.ts +6 -0
  53. package/src/mqtt/mqtt-client.ts +119 -0
  54. package/src/mqtt/types.ts +36 -0
  55. package/src/shared/context.ts +33 -0
  56. package/src/shared/wrapper.ts +35 -0
  57. package/tsconfig.json +16 -0
  58. package/types/openclaw.d.ts +74 -0
@@ -0,0 +1,122 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { pathExists, readJsonFile } from '../lib/fs.ts';
5
+ import type { JsonValue, MethodContext, MethodHandler } from '../types.ts';
6
+
7
+ interface SessionsIndexEntry {
8
+ sessionId?: string;
9
+ updatedAt?: number;
10
+ status?: string;
11
+ sessionFile?: string;
12
+ model?: string;
13
+ modelProvider?: string;
14
+ systemPromptReport?: {
15
+ provider?: string;
16
+ model?: string;
17
+ workspaceDir?: string;
18
+ };
19
+ origin?: {
20
+ label?: string;
21
+ provider?: string;
22
+ chatType?: string;
23
+ };
24
+ }
25
+
26
+ type SessionsIndex = Record<string, SessionsIndexEntry>;
27
+
28
+ export interface AgentSessionRecord {
29
+ agentName: string;
30
+ sessionKey: string;
31
+ sessionId: string | null;
32
+ updatedAt: number | null;
33
+ status: string | null;
34
+ provider: string | null;
35
+ model: string | null;
36
+ workspaceDir: string | null;
37
+ originLabel: string | null;
38
+ chatType: string | null;
39
+ sessionFileName: string | null;
40
+ sessionFilePath: string | null;
41
+ }
42
+
43
+ export const listSessions: MethodHandler = async (
44
+ _params,
45
+ context
46
+ ): Promise<JsonValue> => {
47
+ const items = await listAllAgentSessions(context);
48
+ return {
49
+ count: items.length,
50
+ items
51
+ };
52
+ };
53
+
54
+ export async function listAllAgentSessions(context: MethodContext): Promise<AgentSessionRecord[]> {
55
+ const agentsRoot = path.join(context.openclawRoot, 'agents');
56
+ if (!(await pathExists(agentsRoot))) {
57
+ return [];
58
+ }
59
+
60
+ const agentEntries = await fs.readdir(agentsRoot, { withFileTypes: true });
61
+ const items: AgentSessionRecord[] = [];
62
+
63
+ for (const agentEntry of agentEntries) {
64
+ if (!agentEntry.isDirectory()) {
65
+ continue;
66
+ }
67
+
68
+ const agentName = agentEntry.name;
69
+ const sessionsPath = path.join(agentsRoot, agentName, 'sessions', 'sessions.json');
70
+ if (!(await pathExists(sessionsPath))) {
71
+ continue;
72
+ }
73
+
74
+ const sessions = await readJsonFile<SessionsIndex>(sessionsPath);
75
+ for (const [sessionKey, entry] of Object.entries(sessions)) {
76
+ const sessionId = entry.sessionId ?? extractSessionIdFromFile(entry.sessionFile) ?? null;
77
+ items.push({
78
+ agentName,
79
+ sessionKey,
80
+ sessionId,
81
+ updatedAt: entry.updatedAt ?? null,
82
+ status: entry.status ?? null,
83
+ provider: entry.systemPromptReport?.provider ?? entry.modelProvider ?? null,
84
+ model: entry.systemPromptReport?.model ?? entry.model ?? null,
85
+ workspaceDir: entry.systemPromptReport?.workspaceDir ?? null,
86
+ originLabel: entry.origin?.label ?? entry.origin?.provider ?? null,
87
+ chatType: entry.origin?.chatType ?? null,
88
+ sessionFileName: entry.sessionFile ? path.basename(entry.sessionFile) : (sessionId ? `${sessionId}.jsonl` : null),
89
+ sessionFilePath: sessionId
90
+ ? path.join(agentsRoot, agentName, 'sessions', `${sessionId}.jsonl`)
91
+ : null
92
+ });
93
+ }
94
+ }
95
+
96
+ return items.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
97
+ }
98
+
99
+ export async function findSessionRecord(
100
+ context: MethodContext,
101
+ sessionIdOrKey: string
102
+ ): Promise<AgentSessionRecord | null> {
103
+ const sessions = await listAllAgentSessions(context);
104
+ return sessions.find((item) => item.sessionId === sessionIdOrKey || item.sessionKey === sessionIdOrKey) ?? null;
105
+ }
106
+
107
+ export async function resolveSessionFile(
108
+ context: MethodContext,
109
+ sessionIdOrKey: string
110
+ ): Promise<string | null> {
111
+ const session = await findSessionRecord(context, sessionIdOrKey);
112
+ return session?.sessionFilePath ?? null;
113
+ }
114
+
115
+ function extractSessionIdFromFile(sessionFile: string | undefined): string | null {
116
+ if (!sessionFile) {
117
+ return null;
118
+ }
119
+
120
+ const base = path.basename(sessionFile);
121
+ return base.endsWith('.jsonl') ? base.slice(0, -'.jsonl'.length) : base;
122
+ }
@@ -0,0 +1,249 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import { pathExists, readJsonFile, writeJsonFile } from '../lib/fs.ts';
5
+ import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.ts';
6
+ import type { JsonValue, MethodContext, MethodHandler } from '../types.ts';
7
+ import { getInstalledSkillsFromCli } from './skills.ts';
8
+
9
+ interface OpenClawConfig {
10
+ agents?: {
11
+ defaults?: {
12
+ skills?: string[];
13
+ [key: string]: unknown;
14
+ };
15
+ [key: string]: unknown;
16
+ };
17
+ [key: string]: unknown;
18
+ }
19
+
20
+ interface InstalledSkillRecord {
21
+ slug: string;
22
+ name: string;
23
+ version: string;
24
+ description?: string;
25
+ author?: string;
26
+ tags?: string[];
27
+ entryFile?: string;
28
+ npmPackage?: string;
29
+ installPath: string;
30
+ scope: 'global' | 'workspace';
31
+ enabled: boolean;
32
+ }
33
+
34
+ export const getInstalledSkill: MethodHandler = async (
35
+ params,
36
+ context
37
+ ): Promise<JsonValue> => {
38
+ const objectParams = isObject(params) ? params : {};
39
+ const slug = typeof objectParams.slug === 'string' ? objectParams.slug : null;
40
+
41
+ if (!slug) {
42
+ throw new JsonRpcException(
43
+ JSON_RPC_ERRORS.invalidParams,
44
+ 'Missing required parameter: slug'
45
+ );
46
+ }
47
+
48
+ const skills = await getInstalledSkillsFromCli(context);
49
+ const skill = skills.find((item) => isObject(item) && item.slug === slug);
50
+
51
+ if (!skill) {
52
+ throw new JsonRpcException(
53
+ JSON_RPC_ERRORS.invalidParams,
54
+ `Skill not installed: ${slug}`
55
+ );
56
+ }
57
+
58
+ return skill;
59
+ };
60
+
61
+ export const uninstallSkill: MethodHandler = async (
62
+ params,
63
+ context
64
+ ): Promise<JsonValue> => {
65
+ const objectParams = isObject(params) ? params : {};
66
+ const slug = typeof objectParams.slug === 'string' ? objectParams.slug : null;
67
+ const scope = typeof objectParams.scope === 'string' ? objectParams.scope : 'global';
68
+
69
+ if (!slug) {
70
+ throw new JsonRpcException(
71
+ JSON_RPC_ERRORS.invalidParams,
72
+ 'Missing required parameter: slug'
73
+ );
74
+ }
75
+
76
+ if (slug.includes('..')) {
77
+ throw new JsonRpcException(
78
+ JSON_RPC_ERRORS.invalidParams,
79
+ 'Invalid skill slug: must not contain ..'
80
+ );
81
+ }
82
+
83
+ const customSkill = await findCustomInstalledSkill(context, slug);
84
+ const skills = await getInstalledSkillsFromCli(context);
85
+ const skill = customSkill ?? skills.find((item) => isObject(item) && item.slug === slug);
86
+ if (!skill || !isObject(skill)) {
87
+ throw new JsonRpcException(
88
+ JSON_RPC_ERRORS.invalidParams,
89
+ `Skill not installed: ${slug}`
90
+ );
91
+ }
92
+
93
+ const skillPathValue =
94
+ typeof skill.customInstallPath === 'string' ? skill.customInstallPath :
95
+ typeof skill.installPath === 'string' ? skill.installPath :
96
+ null;
97
+
98
+ if (!skillPathValue) {
99
+ throw new JsonRpcException(
100
+ JSON_RPC_ERRORS.invalidParams,
101
+ `Skill cannot be uninstalled: ${slug}`
102
+ );
103
+ }
104
+
105
+ const normalizedSkillPath = path.normalize(skillPathValue);
106
+ const globalSkillsRoot = path.normalize(path.join(context.openclawRoot, 'skills'));
107
+ const workspaceSkillsRoot = path.normalize(path.join(context.openclawRoot, 'workspace', '.openclaw', 'skills'));
108
+
109
+ const insideGlobal = normalizedSkillPath === globalSkillsRoot || normalizedSkillPath.startsWith(`${globalSkillsRoot}${path.sep}`);
110
+ const insideWorkspace = normalizedSkillPath === workspaceSkillsRoot || normalizedSkillPath.startsWith(`${workspaceSkillsRoot}${path.sep}`);
111
+
112
+ if (!insideGlobal && !insideWorkspace) {
113
+ throw new JsonRpcException(
114
+ JSON_RPC_ERRORS.invalidParams,
115
+ 'Invalid skill path: must be within skills directory'
116
+ );
117
+ }
118
+
119
+ if (!(await pathExists(skillPathValue))) {
120
+ throw new JsonRpcException(
121
+ JSON_RPC_ERRORS.invalidParams,
122
+ `Skill not installed: ${slug} (scope: ${scope})`
123
+ );
124
+ }
125
+
126
+ await fs.promises.rm(skillPathValue, { recursive: true, force: true });
127
+ await removeSkillFromDefaults(context, slug);
128
+
129
+ return {
130
+ success: true,
131
+ slug,
132
+ scope,
133
+ removedPath: skillPathValue,
134
+ message: `Skill ${slug} uninstalled successfully`
135
+ };
136
+ };
137
+
138
+ function isObject(value: JsonValue | undefined): value is Record<string, JsonValue> {
139
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
140
+ }
141
+
142
+ async function findCustomInstalledSkill(
143
+ context: MethodContext,
144
+ requestedSlug: string
145
+ ): Promise<Record<string, JsonValue> | null> {
146
+ const roots = [
147
+ path.join(context.openclawRoot, 'skills'),
148
+ path.join(context.openclawRoot, 'workspace', '.openclaw', 'skills')
149
+ ];
150
+
151
+ for (const root of roots) {
152
+ if (!(await pathExists(root))) {
153
+ continue;
154
+ }
155
+
156
+ const entries = await fs.promises.readdir(root, { withFileTypes: true });
157
+ for (const entry of entries) {
158
+ if (!entry.isDirectory()) {
159
+ continue;
160
+ }
161
+
162
+ const installPath = path.join(root, entry.name);
163
+ const aliases = await collectCustomSkillAliases(installPath, entry.name);
164
+ if (!aliases.has(requestedSlug)) {
165
+ continue;
166
+ }
167
+
168
+ return {
169
+ slug: aliases.has(requestedSlug) ? requestedSlug : entry.name,
170
+ custom: true,
171
+ installPath
172
+ };
173
+ }
174
+ }
175
+
176
+ return null;
177
+ }
178
+
179
+ async function collectCustomSkillAliases(installPath: string, dirName: string): Promise<Set<string>> {
180
+ const aliases = new Set<string>([dirName, sanitizeAlias(dirName)]);
181
+ const skillJsonPath = path.join(installPath, 'skill.json');
182
+ const packageJsonPath = path.join(installPath, 'package.json');
183
+ const installMetaPath = path.join(installPath, '.openclaw-admin-bridge-install.json');
184
+
185
+ if (await pathExists(skillJsonPath)) {
186
+ const skillJson = await readJsonFile<Record<string, unknown>>(skillJsonPath);
187
+ if (typeof skillJson.slug === 'string') aliases.add(skillJson.slug);
188
+ if (typeof skillJson.name === 'string') aliases.add(skillJson.name);
189
+ }
190
+
191
+ if (await pathExists(packageJsonPath)) {
192
+ const packageJson = await readJsonFile<Record<string, unknown>>(packageJsonPath);
193
+ if (typeof packageJson.name === 'string') {
194
+ aliases.add(packageJson.name);
195
+ const lastSegment = packageJson.name.includes('/') ? (packageJson.name.split('/').at(-1) ?? packageJson.name) : packageJson.name;
196
+ aliases.add(lastSegment);
197
+ aliases.add(sanitizeAlias(lastSegment));
198
+ }
199
+ }
200
+
201
+ if (await pathExists(installMetaPath)) {
202
+ const installMeta = await readJsonFile<Record<string, unknown>>(installMetaPath);
203
+ if (typeof installMeta.package === 'string') {
204
+ aliases.add(installMeta.package);
205
+ const lastSegment = installMeta.package.includes('/') ? (installMeta.package.split('/').at(-1) ?? installMeta.package) : installMeta.package;
206
+ aliases.add(lastSegment);
207
+ aliases.add(sanitizeAlias(lastSegment));
208
+ }
209
+ }
210
+
211
+ return aliases;
212
+ }
213
+
214
+ function sanitizeAlias(value: string): string {
215
+ return value
216
+ .replace(/^@/, '')
217
+ .replace(/[\\/]/g, '-')
218
+ .replace(/[^a-zA-Z0-9._-]+/g, '-')
219
+ .replace(/-+/g, '-')
220
+ .replace(/^-|-$/g, '')
221
+ .toLowerCase();
222
+ }
223
+
224
+ async function removeSkillFromDefaults(context: MethodContext, slug: string): Promise<void> {
225
+ const configPath = path.join(context.openclawRoot, 'openclaw.json');
226
+ if (!(await pathExists(configPath))) {
227
+ return;
228
+ }
229
+
230
+ const config = await readJsonFile<OpenClawConfig>(configPath);
231
+ const currentSkills = config.agents?.defaults?.skills;
232
+ if (!Array.isArray(currentSkills)) {
233
+ return;
234
+ }
235
+
236
+ const nextSkills = currentSkills.filter((item) => item !== slug);
237
+ if (nextSkills.length === currentSkills.length) {
238
+ return;
239
+ }
240
+
241
+ if (!config.agents) config.agents = {};
242
+ if (!config.agents.defaults) config.agents.defaults = {};
243
+ config.agents.defaults.skills = nextSkills;
244
+ await writeJsonFile(configPath, config);
245
+ }
246
+
247
+ function pickString(value: unknown): string | undefined {
248
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
249
+ }
@@ -0,0 +1,235 @@
1
+ import { readJsonFile, writeJsonFile } from '../lib/fs.ts';
2
+ import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.ts';
3
+ import type { JsonValue, MethodHandler } from '../types.ts';
4
+ import { getInstalledSkillsFromCli } from './skills.ts';
5
+ import path from 'node:path';
6
+
7
+ interface OpenClawConfig {
8
+ agents?: {
9
+ defaults?: {
10
+ skills?: string[];
11
+ [key: string]: any;
12
+ };
13
+ [key: string]: any;
14
+ };
15
+ skills?: {
16
+ allowBundled?: string[];
17
+ entries?: Record<string, { enabled?: boolean;[key: string]: any }>;
18
+ [key: string]: any;
19
+ };
20
+ [key: string]: any;
21
+ }
22
+
23
+ interface ToggleSkillParams {
24
+ slug: string;
25
+ enabled: boolean;
26
+ agentName?: string;
27
+ }
28
+
29
+ /**
30
+ * 启用或停用 skill
31
+ * 通过修改 agents.defaults.skills 数组来控制
32
+ */
33
+ export const toggleSkill: MethodHandler = async (
34
+ params,
35
+ context
36
+ ): Promise<JsonValue> => {
37
+ const objectParams = expectObject(params);
38
+ const slug = expectString(objectParams.slug, 'slug');
39
+ const enabled = expectBoolean(objectParams.enabled, 'enabled');
40
+ const agentName = typeof objectParams.agentName === 'string' ? objectParams.agentName : 'defaults';
41
+
42
+ if (agentName !== 'defaults') {
43
+ throw new JsonRpcException(
44
+ JSON_RPC_ERRORS.invalidParams,
45
+ 'Currently only "defaults" agent is supported'
46
+ );
47
+ }
48
+
49
+ const allSkills = await getInstalledSkillsFromCli(context);
50
+ const runtimeSkill = allSkills.find(
51
+ (item) => isObject(item) && typeof item.slug === 'string' && item.slug === slug
52
+ );
53
+ if (!runtimeSkill || !isObject(runtimeSkill)) {
54
+ throw new JsonRpcException(
55
+ JSON_RPC_ERRORS.invalidParams,
56
+ `Skill not installed: ${slug}`
57
+ );
58
+ }
59
+
60
+ const bundled = runtimeSkill.bundled === true;
61
+
62
+ const configPath = path.join(context.openclawRoot, 'openclaw.json');
63
+ const config = await readJsonFile<OpenClawConfig>(configPath);
64
+
65
+ if (!config.skills) config.skills = {};
66
+ if (!config.skills.entries) config.skills.entries = {};
67
+ if (!config.skills.entries[slug] || typeof config.skills.entries[slug] !== 'object') {
68
+ config.skills.entries[slug] = {};
69
+ }
70
+ config.skills.entries[slug].enabled = enabled;
71
+
72
+ if (bundled) {
73
+ if (!config.skills) config.skills = {};
74
+ const currentAllowBundled = Array.isArray(config.skills.allowBundled)
75
+ ? config.skills.allowBundled
76
+ : [];
77
+ const skillIndex = currentAllowBundled.indexOf(slug);
78
+
79
+ if (enabled) {
80
+ if (skillIndex === -1) {
81
+ currentAllowBundled.push(slug);
82
+ config.skills.allowBundled = currentAllowBundled;
83
+ await writeJsonFile(configPath, config);
84
+ return {
85
+ success: true,
86
+ action: 'enabled',
87
+ slug,
88
+ bundled: true,
89
+ skillEntry: config.skills.entries[slug],
90
+ allowBundled: config.skills.allowBundled
91
+ };
92
+ }
93
+
94
+ await writeJsonFile(configPath, config);
95
+ return {
96
+ success: true,
97
+ action: 'already_enabled',
98
+ slug,
99
+ bundled: true,
100
+ skillEntry: config.skills.entries[slug],
101
+ allowBundled: currentAllowBundled
102
+ };
103
+ }
104
+
105
+ if (skillIndex !== -1) {
106
+ currentAllowBundled.splice(skillIndex, 1);
107
+ config.skills.allowBundled = currentAllowBundled;
108
+ await writeJsonFile(configPath, config);
109
+ return {
110
+ success: true,
111
+ action: 'disabled',
112
+ slug,
113
+ bundled: true,
114
+ skillEntry: config.skills.entries[slug],
115
+ allowBundled: config.skills.allowBundled
116
+ };
117
+ }
118
+
119
+ await writeJsonFile(configPath, config);
120
+ return {
121
+ success: true,
122
+ action: 'already_disabled',
123
+ slug,
124
+ bundled: true,
125
+ skillEntry: config.skills.entries[slug],
126
+ allowBundled: currentAllowBundled
127
+ };
128
+ }
129
+
130
+ const installPath: string | null = (() => {
131
+ if (!isObject(runtimeSkill)) return null;
132
+ const p = runtimeSkill.installPath ?? runtimeSkill.customInstallPath;
133
+ return typeof p === 'string' && p.length > 0 ? p : null;
134
+ })();
135
+
136
+ if (!installPath) {
137
+ const globalSkillPath = path.join(context.openclawRoot, 'skills', slug);
138
+ const workspaceSkillPath = path.join(context.openclawRoot, 'workspace', '.openclaw', 'skills', slug);
139
+ const { pathExists } = await import('../lib/fs.ts');
140
+ if (!(await pathExists(globalSkillPath)) && !(await pathExists(workspaceSkillPath))) {
141
+ throw new JsonRpcException(
142
+ JSON_RPC_ERRORS.invalidParams,
143
+ `Skill not installed: ${slug}`
144
+ );
145
+ }
146
+ }
147
+
148
+ if (!config.agents) config.agents = {};
149
+ if (!config.agents.defaults) config.agents.defaults = {};
150
+ if (!config.agents.defaults.skills) config.agents.defaults.skills = [];
151
+
152
+ const currentSkills = config.agents.defaults.skills;
153
+ const skillIndex = currentSkills.indexOf(slug);
154
+
155
+ if (enabled) {
156
+ // 启用:如果不在列表中,添加
157
+ if (skillIndex === -1) {
158
+ currentSkills.push(slug);
159
+ await writeJsonFile(configPath, config);
160
+ return {
161
+ success: true,
162
+ action: 'enabled',
163
+ slug,
164
+ agentName,
165
+ skillEntry: config.skills.entries[slug],
166
+ currentSkills: config.agents.defaults.skills
167
+ };
168
+ } else {
169
+ await writeJsonFile(configPath, config);
170
+ return {
171
+ success: true,
172
+ action: 'already_enabled',
173
+ slug,
174
+ agentName,
175
+ skillEntry: config.skills.entries[slug],
176
+ currentSkills: config.agents.defaults.skills
177
+ };
178
+ }
179
+ } else {
180
+ // 停用:如果在列表中,移除
181
+ if (skillIndex !== -1) {
182
+ currentSkills.splice(skillIndex, 1);
183
+ await writeJsonFile(configPath, config);
184
+ return {
185
+ success: true,
186
+ action: 'disabled',
187
+ slug,
188
+ agentName,
189
+ skillEntry: config.skills.entries[slug],
190
+ currentSkills: config.agents.defaults.skills
191
+ };
192
+ } else {
193
+ await writeJsonFile(configPath, config);
194
+ return {
195
+ success: true,
196
+ action: 'already_disabled',
197
+ slug,
198
+ agentName,
199
+ skillEntry: config.skills.entries[slug],
200
+ currentSkills: config.agents.defaults.skills
201
+ };
202
+ }
203
+ }
204
+ };
205
+
206
+ function expectObject(value: JsonValue | undefined): Record<string, JsonValue> {
207
+ if (!value || Array.isArray(value) || typeof value !== 'object') {
208
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, 'Params must be an object');
209
+ }
210
+ return value as Record<string, JsonValue>;
211
+ }
212
+
213
+ function expectString(value: JsonValue | undefined, fieldName: string): string {
214
+ if (typeof value !== 'string' || value.trim().length === 0) {
215
+ throw new JsonRpcException(
216
+ JSON_RPC_ERRORS.invalidParams,
217
+ `Field '${fieldName}' must be a non-empty string`
218
+ );
219
+ }
220
+ return value.trim();
221
+ }
222
+
223
+ function expectBoolean(value: JsonValue | undefined, fieldName: string): boolean {
224
+ if (typeof value !== 'boolean') {
225
+ throw new JsonRpcException(
226
+ JSON_RPC_ERRORS.invalidParams,
227
+ `Field '${fieldName}' must be a boolean`
228
+ );
229
+ }
230
+ return value;
231
+ }
232
+
233
+ function isObject(value: JsonValue | undefined): value is Record<string, JsonValue> {
234
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
235
+ }