@workclaw/openclaw-workclaw 1.0.16 → 1.0.18
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/README.md +21 -1
- package/index.ts +210 -210
- package/openclaw.plugin.json +1 -0
- package/package.json +12 -5
- package/setup-entry.ts +6 -0
- package/skills/openclaw-workclaw-cron/SKILL.md +45 -28
- package/src/accounts.ts +62 -37
- package/src/api/accounts-api.ts +88 -89
- package/src/api/prompts-api.ts +70 -77
- package/src/api/session-api.ts +99 -108
- package/src/api/skills-api.ts +35 -37
- package/src/api/workspace.ts +27 -29
- package/src/channel.ts +200 -202
- package/src/config-schema.ts +9 -9
- package/src/connection/workclaw-client.ts +554 -567
- package/src/gateway/agent-handlers.ts +392 -426
- package/src/gateway/config-writer.ts +228 -243
- package/src/gateway/message-context.ts +534 -362
- package/src/gateway/message-dispatcher.ts +529 -489
- package/src/gateway/reconnect.ts +217 -113
- package/src/gateway/skills-handler.ts +408 -472
- package/src/gateway/skills-list-handler.ts +9 -9
- package/src/gateway/tools-list-handler.ts +70 -72
- package/src/gateway/workclaw-gateway.ts +328 -486
- package/src/media/upload.ts +83 -94
- package/src/outbound/index.ts +57 -55
- package/src/outbound/workclaw-sender.ts +134 -133
- package/src/runtime.ts +291 -194
- package/src/send.ts +1 -1
- package/src/tools/openclaw-workclaw-cron/api/index.ts +6 -6
- package/src/tools/openclaw-workclaw-cron/src/add/params.ts +20 -19
- package/src/tools/openclaw-workclaw-cron/src/add/sync.ts +2 -2
- package/src/tools/openclaw-workclaw-cron/src/disable/params.ts +1 -1
- package/src/tools/openclaw-workclaw-cron/src/disable/sync.ts +3 -3
- package/src/tools/openclaw-workclaw-cron/src/enable/params.ts +1 -1
- package/src/tools/openclaw-workclaw-cron/src/enable/sync.ts +3 -3
- package/src/tools/openclaw-workclaw-cron/src/notify/sync.ts +2 -2
- package/src/tools/openclaw-workclaw-cron/src/remove/params.ts +1 -1
- package/src/tools/openclaw-workclaw-cron/src/remove/sync.ts +3 -3
- package/src/tools/openclaw-workclaw-cron/src/update/params.ts +195 -197
- package/src/tools/openclaw-workclaw-cron/src/update/sync.ts +4 -4
- package/src/tools/openclaw-workclaw-system/src/get/index.ts +2 -2
- package/src/tools/openclaw-workclaw-system/src/token/index.ts +4 -4
- package/src/types.ts +38 -40
- package/src/utils/content.ts +16 -21
- package/tests/accounts.test.ts +285 -0
- package/tests/message-context.test.ts +313 -0
- package/tests/reconnect.test.ts +257 -0
- package/tests/workclaw-client.test.ts +112 -0
- package/tsconfig.json +8 -5
- package/vitest.config.ts +8 -0
package/src/api/prompts-api.ts
CHANGED
|
@@ -1,123 +1,116 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import path from 'node:path'
|
|
1
|
+
import { mkdir, readFile, stat, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
3
|
+
import { resolveWorkspaceDir } from "./workspace.js";
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const promptNames = ['SOUL.md', 'IDENTITY.md'] as const
|
|
5
|
+
const promptNames = ["SOUL.md", "USER.md"] as const;
|
|
9
6
|
|
|
10
7
|
function normalizePromptName(raw: string | null | undefined): (typeof promptNames)[number] | null {
|
|
11
|
-
const name = String(raw ??
|
|
12
|
-
if (name ===
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return 'IDENTITY.md'
|
|
16
|
-
return null
|
|
8
|
+
const name = String(raw ?? "").trim().toUpperCase();
|
|
9
|
+
if (name === "SOUL" || name === "SOUL.MD") return "SOUL.md";
|
|
10
|
+
if (name === "USER" || name === "USER.MD") return "USER.md";
|
|
11
|
+
return null;
|
|
17
12
|
}
|
|
18
13
|
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
|
|
19
16
|
function resolvePromptPath(api: OpenClawPluginApi, name: (typeof promptNames)[number]): string {
|
|
20
|
-
return path.join(resolveWorkspaceDir(api), name)
|
|
17
|
+
return path.join(resolveWorkspaceDir(api), name);
|
|
21
18
|
}
|
|
22
19
|
|
|
23
|
-
function sendJson(res: any, statusCode: number, payload: unknown)
|
|
24
|
-
res.statusCode = statusCode
|
|
25
|
-
res.setHeader(
|
|
26
|
-
res.end(JSON.stringify(payload))
|
|
20
|
+
function sendJson(res: any, statusCode: number, payload: unknown) {
|
|
21
|
+
res.statusCode = statusCode;
|
|
22
|
+
res.setHeader("Content-Type", "application/json");
|
|
23
|
+
res.end(JSON.stringify(payload));
|
|
27
24
|
}
|
|
28
25
|
|
|
29
26
|
export function createPromptsApiHandler(api: OpenClawPluginApi) {
|
|
30
27
|
return async (req: any, res: any) => {
|
|
31
|
-
const method = String(req.method ??
|
|
32
|
-
const url = new URL(req.url ??
|
|
33
|
-
const name = normalizePromptName(url.searchParams.get(
|
|
28
|
+
const method = String(req.method ?? "GET").toUpperCase();
|
|
29
|
+
const url = new URL(req.url ?? "", "http://localhost");
|
|
30
|
+
const name = normalizePromptName(url.searchParams.get("name"));
|
|
34
31
|
|
|
35
|
-
if (method ===
|
|
32
|
+
if (method === "GET") {
|
|
36
33
|
if (name) {
|
|
37
|
-
const filePath = resolvePromptPath(api, name)
|
|
34
|
+
const filePath = resolvePromptPath(api, name);
|
|
38
35
|
try {
|
|
39
|
-
const content = await readFile(filePath,
|
|
40
|
-
sendJson(res, 200, { ok: true, name, content })
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return
|
|
36
|
+
const content = await readFile(filePath, "utf-8");
|
|
37
|
+
sendJson(res, 200, { ok: true, name, content });
|
|
38
|
+
return;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
sendJson(res, 404, { ok: false, error: "Not Found" });
|
|
41
|
+
return;
|
|
46
42
|
}
|
|
47
43
|
}
|
|
48
44
|
|
|
49
45
|
const entries = await Promise.all(
|
|
50
46
|
promptNames.map(async (promptName) => {
|
|
51
|
-
const filePath = resolvePromptPath(api, promptName)
|
|
47
|
+
const filePath = resolvePromptPath(api, promptName);
|
|
52
48
|
try {
|
|
53
|
-
const info = await stat(filePath)
|
|
54
|
-
return { name: promptName, exists: true, bytes: info.size }
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return { name: promptName, exists: false, bytes: 0 }
|
|
49
|
+
const info = await stat(filePath);
|
|
50
|
+
return { name: promptName, exists: true, bytes: info.size };
|
|
51
|
+
} catch {
|
|
52
|
+
return { name: promptName, exists: false, bytes: 0 };
|
|
58
53
|
}
|
|
59
54
|
}),
|
|
60
|
-
)
|
|
61
|
-
sendJson(res, 200, { ok: true, entries })
|
|
62
|
-
return
|
|
55
|
+
);
|
|
56
|
+
sendJson(res, 200, { ok: true, entries });
|
|
57
|
+
return;
|
|
63
58
|
}
|
|
64
59
|
|
|
65
|
-
if (method ===
|
|
66
|
-
const raw = await readRequestBody(req)
|
|
67
|
-
let input: any = {}
|
|
60
|
+
if (method === "PUT" || method === "POST") {
|
|
61
|
+
const raw = await readRequestBody(req);
|
|
62
|
+
let input: any = {};
|
|
68
63
|
try {
|
|
69
|
-
input = raw ? JSON.parse(raw) : {}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return
|
|
64
|
+
input = raw ? JSON.parse(raw) : {};
|
|
65
|
+
} catch {
|
|
66
|
+
sendJson(res, 400, { ok: false, error: "Invalid JSON" });
|
|
67
|
+
return;
|
|
74
68
|
}
|
|
75
69
|
|
|
76
|
-
const targetName = normalizePromptName(input?.name)
|
|
77
|
-
const content = typeof input?.content ===
|
|
70
|
+
const targetName = normalizePromptName(input?.name);
|
|
71
|
+
const content = typeof input?.content === "string" ? input.content : null;
|
|
78
72
|
|
|
79
73
|
if (!targetName || content === null) {
|
|
80
|
-
sendJson(res, 400, { ok: false, error:
|
|
81
|
-
return
|
|
74
|
+
sendJson(res, 400, { ok: false, error: "Missing name or content" });
|
|
75
|
+
return;
|
|
82
76
|
}
|
|
83
77
|
|
|
84
|
-
const workspaceDir = resolveWorkspaceDir(api)
|
|
85
|
-
await mkdir(workspaceDir, { recursive: true })
|
|
86
|
-
await writeFile(resolvePromptPath(api, targetName), content, { encoding:
|
|
87
|
-
sendJson(res, 200, { ok: true, name: targetName })
|
|
88
|
-
return
|
|
78
|
+
const workspaceDir = resolveWorkspaceDir(api);
|
|
79
|
+
await mkdir(workspaceDir, { recursive: true });
|
|
80
|
+
await writeFile(resolvePromptPath(api, targetName), content, { encoding: "utf-8" });
|
|
81
|
+
sendJson(res, 200, { ok: true, name: targetName });
|
|
82
|
+
return;
|
|
89
83
|
}
|
|
90
84
|
|
|
91
|
-
if (method ===
|
|
85
|
+
if (method === "DELETE") {
|
|
92
86
|
if (!name) {
|
|
93
|
-
sendJson(res, 400, { ok: false, error:
|
|
94
|
-
return
|
|
87
|
+
sendJson(res, 400, { ok: false, error: "Missing name" });
|
|
88
|
+
return;
|
|
95
89
|
}
|
|
96
90
|
|
|
97
|
-
const filePath = resolvePromptPath(api, name)
|
|
91
|
+
const filePath = resolvePromptPath(api, name);
|
|
98
92
|
try {
|
|
99
|
-
await unlink(filePath)
|
|
100
|
-
sendJson(res, 200, { ok: true, name, deleted: true })
|
|
101
|
-
return
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return
|
|
93
|
+
await unlink(filePath);
|
|
94
|
+
sendJson(res, 200, { ok: true, name, deleted: true });
|
|
95
|
+
return;
|
|
96
|
+
} catch {
|
|
97
|
+
sendJson(res, 200, { ok: true, name, deleted: false });
|
|
98
|
+
return;
|
|
106
99
|
}
|
|
107
100
|
}
|
|
108
101
|
|
|
109
|
-
sendJson(res, 405, { ok: false, error:
|
|
110
|
-
}
|
|
102
|
+
sendJson(res, 405, { ok: false, error: "Method Not Allowed" });
|
|
103
|
+
};
|
|
111
104
|
}
|
|
112
105
|
|
|
113
106
|
async function readRequestBody(req: any): Promise<string> {
|
|
114
|
-
const chunks: Buffer[] = []
|
|
107
|
+
const chunks: Buffer[] = [];
|
|
115
108
|
await new Promise<void>((resolve, reject) => {
|
|
116
|
-
req.on(
|
|
117
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
|
|
118
|
-
})
|
|
119
|
-
req.on(
|
|
120
|
-
req.on(
|
|
121
|
-
})
|
|
122
|
-
return Buffer.concat(chunks).toString(
|
|
109
|
+
req.on("data", (chunk: any) => {
|
|
110
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
111
|
+
});
|
|
112
|
+
req.on("end", () => resolve());
|
|
113
|
+
req.on("error", (err: unknown) => reject(err));
|
|
114
|
+
});
|
|
115
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
123
116
|
}
|
package/src/api/session-api.ts
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
import type { OpenClawPluginApi } from
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
res.
|
|
7
|
-
res.
|
|
8
|
-
res.end(JSON.stringify(payload))
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { getWorkclawRuntime } from "../runtime.js";
|
|
3
|
+
|
|
4
|
+
function sendJson(res: any, statusCode: number, payload: unknown) {
|
|
5
|
+
res.statusCode = statusCode;
|
|
6
|
+
res.setHeader("Content-Type", "application/json");
|
|
7
|
+
res.end(JSON.stringify(payload));
|
|
9
8
|
}
|
|
10
9
|
|
|
11
10
|
async function readRequestBody(req: any): Promise<string> {
|
|
12
|
-
const chunks: Buffer[] = []
|
|
11
|
+
const chunks: Buffer[] = [];
|
|
13
12
|
await new Promise<void>((resolve, reject) => {
|
|
14
|
-
req.on(
|
|
15
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
|
|
16
|
-
})
|
|
17
|
-
req.on(
|
|
18
|
-
req.on(
|
|
19
|
-
})
|
|
20
|
-
return Buffer.concat(chunks).toString(
|
|
13
|
+
req.on("data", (chunk: any) => {
|
|
14
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
15
|
+
});
|
|
16
|
+
req.on("end", () => resolve());
|
|
17
|
+
req.on("error", (err: unknown) => reject(err));
|
|
18
|
+
});
|
|
19
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
/**
|
|
@@ -25,26 +24,26 @@ async function readRequestBody(req: any): Promise<string> {
|
|
|
25
24
|
* 格式: openclaw-workclaw:{accountId}:{userId}
|
|
26
25
|
*/
|
|
27
26
|
function buildSessionKey(accountId: string, userId: string): string {
|
|
28
|
-
return `openclaw-workclaw:${accountId}:${userId}
|
|
27
|
+
return `openclaw-workclaw:${accountId}:${userId}`;
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
/**
|
|
32
31
|
* 解析会话 key
|
|
33
32
|
*/
|
|
34
33
|
function parseSessionKey(sessionKey: string): {
|
|
35
|
-
channel: string
|
|
36
|
-
accountId: string
|
|
37
|
-
userId: string
|
|
34
|
+
channel: string;
|
|
35
|
+
accountId: string;
|
|
36
|
+
userId: string;
|
|
38
37
|
} | null {
|
|
39
|
-
const parts = sessionKey.split(
|
|
40
|
-
if (parts.length !== 3 || parts[0] !==
|
|
41
|
-
return null
|
|
38
|
+
const parts = sessionKey.split(":");
|
|
39
|
+
if (parts.length !== 3 || parts[0] !== "openclaw-workclaw") {
|
|
40
|
+
return null;
|
|
42
41
|
}
|
|
43
42
|
return {
|
|
44
43
|
channel: parts[0],
|
|
45
44
|
accountId: parts[1],
|
|
46
45
|
userId: parts[2],
|
|
47
|
-
}
|
|
46
|
+
};
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
/**
|
|
@@ -53,28 +52,27 @@ function parseSessionKey(sessionKey: string): {
|
|
|
53
52
|
*/
|
|
54
53
|
async function resetSession(
|
|
55
54
|
sessionKey: string,
|
|
56
|
-
log?: { info?: (msg: string) => void
|
|
57
|
-
): Promise<{ success: boolean
|
|
55
|
+
log?: { info?: (msg: string) => void; error?: (msg: string) => void }
|
|
56
|
+
): Promise<{ success: boolean; message: string }> {
|
|
58
57
|
try {
|
|
59
|
-
const runtime =
|
|
58
|
+
const runtime = getWorkclawRuntime();
|
|
60
59
|
|
|
61
60
|
// 使用 system.enqueueSystemEvent 发送 /new 命令
|
|
62
|
-
runtime.system.enqueueSystemEvent(
|
|
61
|
+
runtime.system.enqueueSystemEvent("/new", {
|
|
63
62
|
sessionKey,
|
|
64
63
|
contextKey: null,
|
|
65
|
-
})
|
|
64
|
+
});
|
|
66
65
|
|
|
67
|
-
log?.info?.(`[SessionAPI] Sent /new command to session: ${sessionKey}`)
|
|
66
|
+
log?.info?.(`[SessionAPI] Sent /new command to session: ${sessionKey}`);
|
|
68
67
|
|
|
69
68
|
return {
|
|
70
69
|
success: true,
|
|
71
70
|
message: `Session reset command sent to ${sessionKey}`,
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return { success: false, message: errorMsg }
|
|
71
|
+
};
|
|
72
|
+
} catch (err) {
|
|
73
|
+
const errorMsg = `Failed to reset session: ${String(err)}`;
|
|
74
|
+
log?.error?.(`[SessionAPI] ${errorMsg}`);
|
|
75
|
+
return { success: false, message: errorMsg };
|
|
78
76
|
}
|
|
79
77
|
}
|
|
80
78
|
|
|
@@ -83,18 +81,17 @@ async function resetSession(
|
|
|
83
81
|
*/
|
|
84
82
|
function getSessionStorePath(
|
|
85
83
|
agentId?: string,
|
|
86
|
-
log?: { info?: (msg: string) => void
|
|
84
|
+
log?: { info?: (msg: string) => void; error?: (msg: string) => void }
|
|
87
85
|
): string | null {
|
|
88
86
|
try {
|
|
89
|
-
const runtime =
|
|
87
|
+
const runtime = getWorkclawRuntime();
|
|
90
88
|
const storePath = runtime.channel.session.resolveStorePath(undefined, {
|
|
91
89
|
agentId,
|
|
92
|
-
})
|
|
93
|
-
return storePath
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return null
|
|
90
|
+
});
|
|
91
|
+
return storePath;
|
|
92
|
+
} catch (err) {
|
|
93
|
+
log?.error?.(`[SessionAPI] Failed to resolve store path: ${String(err)}`);
|
|
94
|
+
return null;
|
|
98
95
|
}
|
|
99
96
|
}
|
|
100
97
|
|
|
@@ -103,47 +100,46 @@ function getSessionStorePath(
|
|
|
103
100
|
*/
|
|
104
101
|
async function getSessionLastUpdated(
|
|
105
102
|
sessionKey: string,
|
|
106
|
-
log?: { info?: (msg: string) => void
|
|
103
|
+
log?: { info?: (msg: string) => void; error?: (msg: string) => void }
|
|
107
104
|
): Promise<number | null> {
|
|
108
105
|
try {
|
|
109
|
-
const runtime =
|
|
110
|
-
const storePath = runtime.channel.session.resolveStorePath()
|
|
106
|
+
const runtime = getWorkclawRuntime();
|
|
107
|
+
const storePath = runtime.channel.session.resolveStorePath();
|
|
111
108
|
const updatedAt = runtime.channel.session.readSessionUpdatedAt({
|
|
112
109
|
storePath,
|
|
113
110
|
sessionKey,
|
|
114
|
-
})
|
|
115
|
-
return updatedAt ?? null
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return null
|
|
111
|
+
});
|
|
112
|
+
return updatedAt ?? null;
|
|
113
|
+
} catch (err) {
|
|
114
|
+
log?.error?.(`[SessionAPI] Failed to read session updated at: ${String(err)}`);
|
|
115
|
+
return null;
|
|
120
116
|
}
|
|
121
117
|
}
|
|
122
118
|
|
|
123
119
|
export function createSessionApiHandler(api: OpenClawPluginApi) {
|
|
124
120
|
return async (req: any, res: any) => {
|
|
125
|
-
const method = String(req.method ??
|
|
126
|
-
const url = new URL(req.url ??
|
|
121
|
+
const method = String(req.method ?? "GET").toUpperCase();
|
|
122
|
+
const url = new URL(req.url ?? "", "http://localhost");
|
|
127
123
|
// 移除尾部斜杠并获取路径部分
|
|
128
|
-
const fullPath = url.pathname.replace(/\/+$/,
|
|
124
|
+
const fullPath = url.pathname.replace(/\/+$/, "");
|
|
129
125
|
// 提取子路径(去掉 /openclaw-workclaw/sessions 前缀)
|
|
130
|
-
const subPath = fullPath.replace(/^\/openclaw-workclaw\/sessions/,
|
|
126
|
+
const subPath = fullPath.replace(/^\/openclaw-workclaw\/sessions/, "").replace(/^\//, "") || "/";
|
|
131
127
|
|
|
132
128
|
const log = {
|
|
133
129
|
info: (msg: string) => api.logger?.info?.(`[SessionAPI] ${msg}`),
|
|
134
130
|
error: (msg: string) => api.logger?.error?.(`[SessionAPI] ${msg}`),
|
|
135
|
-
}
|
|
131
|
+
};
|
|
136
132
|
|
|
137
|
-
log?.info?.(`[SessionAPI] ${method} ${fullPath} (subPath: ${subPath})`)
|
|
133
|
+
log?.info?.(`[SessionAPI] ${method} ${fullPath} (subPath: ${subPath})`);
|
|
138
134
|
|
|
139
135
|
// GET /sessions 或 /sessions/ - 列出会话信息
|
|
140
|
-
if (method ===
|
|
141
|
-
const sessionKey = url.searchParams.get(
|
|
136
|
+
if (method === "GET" && (subPath === "/" || subPath === "")) {
|
|
137
|
+
const sessionKey = url.searchParams.get("sessionKey");
|
|
142
138
|
|
|
143
139
|
if (sessionKey) {
|
|
144
140
|
// 获取特定会话信息
|
|
145
|
-
const parsed = parseSessionKey(sessionKey)
|
|
146
|
-
const updatedAt = await getSessionLastUpdated(sessionKey, log)
|
|
141
|
+
const parsed = parseSessionKey(sessionKey);
|
|
142
|
+
const updatedAt = await getSessionLastUpdated(sessionKey, log);
|
|
147
143
|
|
|
148
144
|
sendJson(res, 200, {
|
|
149
145
|
ok: true,
|
|
@@ -155,93 +151,88 @@ export function createSessionApiHandler(api: OpenClawPluginApi) {
|
|
|
155
151
|
? new Date(updatedAt).toISOString()
|
|
156
152
|
: null,
|
|
157
153
|
},
|
|
158
|
-
})
|
|
159
|
-
return
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
160
156
|
}
|
|
161
157
|
|
|
162
158
|
// 返回 API 信息
|
|
163
159
|
sendJson(res, 200, {
|
|
164
160
|
ok: true,
|
|
165
|
-
message:
|
|
161
|
+
message: "Session Management API",
|
|
166
162
|
endpoints: {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
163
|
+
"GET /sessions": "获取会话信息 (可选参数: sessionKey)",
|
|
164
|
+
"POST /sessions/reset": "重置/开启新会话 (参数: accountId + userId 或直接提供 sessionKey)",
|
|
165
|
+
"GET /sessions/store-path": "获取会话存储路径 (可选参数: agentId)",
|
|
170
166
|
},
|
|
171
|
-
})
|
|
172
|
-
return
|
|
167
|
+
});
|
|
168
|
+
return;
|
|
173
169
|
}
|
|
174
170
|
|
|
175
171
|
// POST /sessions/reset - 重置会话
|
|
176
|
-
if (method ===
|
|
177
|
-
const raw = await readRequestBody(req)
|
|
178
|
-
let input: any = {}
|
|
172
|
+
if (method === "POST" && subPath === "reset") {
|
|
173
|
+
const raw = await readRequestBody(req);
|
|
174
|
+
let input: any = {};
|
|
179
175
|
try {
|
|
180
|
-
input = raw ? JSON.parse(raw) : {}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return
|
|
176
|
+
input = raw ? JSON.parse(raw) : {};
|
|
177
|
+
} catch {
|
|
178
|
+
sendJson(res, 400, { ok: false, error: "Invalid JSON" });
|
|
179
|
+
return;
|
|
185
180
|
}
|
|
186
181
|
|
|
187
|
-
const { accountId, userId, sessionKey: directSessionKey } = input
|
|
182
|
+
const { accountId, userId, sessionKey: directSessionKey } = input;
|
|
188
183
|
|
|
189
|
-
let sessionKey: string
|
|
184
|
+
let sessionKey: string;
|
|
190
185
|
if (directSessionKey) {
|
|
191
|
-
sessionKey = directSessionKey
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
else {
|
|
186
|
+
sessionKey = directSessionKey;
|
|
187
|
+
} else if (accountId && userId) {
|
|
188
|
+
sessionKey = buildSessionKey(accountId, userId);
|
|
189
|
+
} else {
|
|
197
190
|
sendJson(res, 400, {
|
|
198
191
|
ok: false,
|
|
199
|
-
error:
|
|
200
|
-
})
|
|
201
|
-
return
|
|
192
|
+
error: "Missing required fields: either provide 'sessionKey' or both 'accountId' and 'userId'",
|
|
193
|
+
});
|
|
194
|
+
return;
|
|
202
195
|
}
|
|
203
196
|
|
|
204
|
-
const result = await resetSession(sessionKey, log)
|
|
197
|
+
const result = await resetSession(sessionKey, log);
|
|
205
198
|
|
|
206
199
|
if (result.success) {
|
|
207
200
|
sendJson(res, 200, {
|
|
208
201
|
ok: true,
|
|
209
202
|
message: result.message,
|
|
210
203
|
sessionKey,
|
|
211
|
-
})
|
|
212
|
-
}
|
|
213
|
-
else {
|
|
204
|
+
});
|
|
205
|
+
} else {
|
|
214
206
|
sendJson(res, 500, {
|
|
215
207
|
ok: false,
|
|
216
208
|
error: result.message,
|
|
217
209
|
sessionKey,
|
|
218
|
-
})
|
|
210
|
+
});
|
|
219
211
|
}
|
|
220
|
-
return
|
|
212
|
+
return;
|
|
221
213
|
}
|
|
222
214
|
|
|
223
215
|
// GET /sessions/store-path - 获取会话存储路径
|
|
224
|
-
if (method ===
|
|
225
|
-
const agentId = url.searchParams.get(
|
|
226
|
-
const storePath = getSessionStorePath(agentId, log)
|
|
216
|
+
if (method === "GET" && subPath === "store-path") {
|
|
217
|
+
const agentId = url.searchParams.get("agentId") ?? undefined;
|
|
218
|
+
const storePath = getSessionStorePath(agentId, log);
|
|
227
219
|
|
|
228
220
|
if (storePath) {
|
|
229
221
|
sendJson(res, 200, {
|
|
230
222
|
ok: true,
|
|
231
223
|
storePath,
|
|
232
|
-
agentId: agentId ??
|
|
233
|
-
})
|
|
234
|
-
}
|
|
235
|
-
else {
|
|
224
|
+
agentId: agentId ?? "default",
|
|
225
|
+
});
|
|
226
|
+
} else {
|
|
236
227
|
sendJson(res, 500, {
|
|
237
228
|
ok: false,
|
|
238
|
-
error:
|
|
239
|
-
})
|
|
229
|
+
error: "Failed to resolve session store path",
|
|
230
|
+
});
|
|
240
231
|
}
|
|
241
|
-
return
|
|
232
|
+
return;
|
|
242
233
|
}
|
|
243
234
|
|
|
244
235
|
// 404
|
|
245
|
-
sendJson(res, 404, { ok: false, error:
|
|
246
|
-
}
|
|
236
|
+
sendJson(res, 404, { ok: false, error: "Not Found" });
|
|
237
|
+
};
|
|
247
238
|
}
|
package/src/api/skills-api.ts
CHANGED
|
@@ -1,74 +1,72 @@
|
|
|
1
|
-
import type { OpenClawPluginApi } from
|
|
2
|
-
import { exec } from
|
|
3
|
-
import { promisify } from
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { exec } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
4
|
|
|
5
|
-
const execAsync = promisify(exec)
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
6
|
|
|
7
|
-
function sendJson(res: any, statusCode: number, payload: unknown)
|
|
8
|
-
res.statusCode = statusCode
|
|
9
|
-
res.setHeader(
|
|
10
|
-
res.end(JSON.stringify(payload))
|
|
7
|
+
function sendJson(res: any, statusCode: number, payload: unknown) {
|
|
8
|
+
res.statusCode = statusCode;
|
|
9
|
+
res.setHeader("Content-Type", "application/json");
|
|
10
|
+
res.end(JSON.stringify(payload));
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function createSkillsApiHandler(api: OpenClawPluginApi) {
|
|
14
14
|
return async (req: any, res: any) => {
|
|
15
|
-
const method = String(req.method ??
|
|
15
|
+
const method = String(req.method ?? "GET").toUpperCase();
|
|
16
16
|
|
|
17
|
-
if (method ===
|
|
17
|
+
if (method === "GET") {
|
|
18
18
|
try {
|
|
19
19
|
// 调用 openclaw skills list 命令
|
|
20
|
-
const { stdout, stderr } = await execAsync(
|
|
20
|
+
const { stdout, stderr } = await execAsync("openclaw skills list --json", {
|
|
21
21
|
timeout: 10000, // 10秒超时
|
|
22
|
-
})
|
|
22
|
+
});
|
|
23
23
|
|
|
24
24
|
if (stderr && !stdout) {
|
|
25
|
-
api.logger.error(`Skills list error: ${stderr}`)
|
|
26
|
-
sendJson(res, 500, { ok: false, error:
|
|
27
|
-
return
|
|
25
|
+
api.logger.error(`Skills list error: ${stderr}`);
|
|
26
|
+
sendJson(res, 500, { ok: false, error: "Failed to list skills" });
|
|
27
|
+
return;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
// 尝试解析JSON输出
|
|
31
|
-
let skillsData: any
|
|
31
|
+
let skillsData: any;
|
|
32
32
|
try {
|
|
33
|
-
skillsData = JSON.parse(stdout)
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
33
|
+
skillsData = JSON.parse(stdout);
|
|
34
|
+
} catch {
|
|
36
35
|
// 如果不支持--json输出,尝试解析文本输出
|
|
37
|
-
const lines = stdout.split('\n')
|
|
36
|
+
const lines = stdout.split('\n');
|
|
38
37
|
const skills = lines
|
|
39
38
|
.filter((line: string) => line.trim() && !line.includes('Skills'))
|
|
40
39
|
.map((line: string) => {
|
|
41
|
-
const parts = line.split(/\s{2,}/).map((p: string) => p.trim())
|
|
40
|
+
const parts = line.split(/\s{2,}/).map((p: string) => p.trim());
|
|
42
41
|
if (parts.length >= 3) {
|
|
43
42
|
return {
|
|
44
43
|
status: parts[0]?.includes('✓') ? 'ready' : 'missing',
|
|
45
44
|
name: parts[1],
|
|
46
45
|
description: parts[2],
|
|
47
|
-
source: parts[3] || 'openclaw-bundled'
|
|
48
|
-
}
|
|
46
|
+
source: parts[3] || 'openclaw-bundled'
|
|
47
|
+
};
|
|
49
48
|
}
|
|
50
|
-
return null
|
|
49
|
+
return null;
|
|
51
50
|
})
|
|
52
|
-
.filter((s: any) => s !== null)
|
|
53
|
-
|
|
51
|
+
.filter((s: any) => s !== null);
|
|
52
|
+
|
|
54
53
|
skillsData = {
|
|
55
54
|
total: skills.length,
|
|
56
55
|
ready: skills.filter((s: any) => s.status === 'ready').length,
|
|
57
56
|
missing: skills.filter((s: any) => s.status === 'missing').length,
|
|
58
|
-
skills
|
|
59
|
-
}
|
|
57
|
+
skills
|
|
58
|
+
};
|
|
60
59
|
}
|
|
61
60
|
|
|
62
|
-
sendJson(res, 200, { ok: true, ...skillsData })
|
|
63
|
-
return
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return
|
|
61
|
+
sendJson(res, 200, { ok: true, ...skillsData });
|
|
62
|
+
return;
|
|
63
|
+
} catch (error: any) {
|
|
64
|
+
api.logger.error(`Skills API error: ${error.message}`);
|
|
65
|
+
sendJson(res, 500, { ok: false, error: error.message });
|
|
66
|
+
return;
|
|
69
67
|
}
|
|
70
68
|
}
|
|
71
69
|
|
|
72
|
-
sendJson(res, 405, { ok: false, error:
|
|
73
|
-
}
|
|
70
|
+
sendJson(res, 405, { ok: false, error: "Method Not Allowed" });
|
|
71
|
+
};
|
|
74
72
|
}
|