@teamclaws/teamclaw 2026.3.21
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 +37 -0
- package/api.ts +10 -0
- package/index.ts +246 -0
- package/openclaw.plugin.json +41 -0
- package/package.json +63 -0
- package/src/config.ts +297 -0
- package/src/controller/controller-service.ts +197 -0
- package/src/controller/controller-tools.ts +224 -0
- package/src/controller/http-server.ts +1946 -0
- package/src/controller/local-worker-manager.ts +531 -0
- package/src/controller/message-router.ts +62 -0
- package/src/controller/prompt-injector.ts +116 -0
- package/src/controller/task-router.ts +97 -0
- package/src/controller/websocket.ts +63 -0
- package/src/controller/worker-provisioning.ts +1286 -0
- package/src/discovery.ts +101 -0
- package/src/git-collaboration.ts +690 -0
- package/src/identity.ts +149 -0
- package/src/openclaw-workspace.ts +101 -0
- package/src/protocol.ts +88 -0
- package/src/roles.ts +275 -0
- package/src/state.ts +118 -0
- package/src/task-executor.ts +478 -0
- package/src/types.ts +469 -0
- package/src/ui/app.js +1400 -0
- package/src/ui/index.html +207 -0
- package/src/ui/style.css +1281 -0
- package/src/worker/http-handler.ts +136 -0
- package/src/worker/message-queue.ts +31 -0
- package/src/worker/prompt-injector.ts +72 -0
- package/src/worker/tools.ts +318 -0
- package/src/worker/worker-service.ts +194 -0
- package/src/workspace-browser.ts +312 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import type { OpenClawPluginService, OpenClawPluginServiceContext, PluginLogger } from "../../api.js";
|
|
3
|
+
import type { PluginConfig, TaskAssignmentPayload, WorkerIdentity } from "../types.js";
|
|
4
|
+
import { createHeartbeatPayload } from "../protocol.js";
|
|
5
|
+
import { IdentityManager } from "../identity.js";
|
|
6
|
+
import { MessageQueue } from "./message-queue.js";
|
|
7
|
+
import { createWorkerHttpHandler } from "./http-handler.js";
|
|
8
|
+
import { ensureOpenClawWorkspaceMemoryDir } from "../openclaw-workspace.js";
|
|
9
|
+
|
|
10
|
+
export type WorkerServiceDeps = {
|
|
11
|
+
config: PluginConfig;
|
|
12
|
+
logger: PluginLogger;
|
|
13
|
+
onIdentityEstablished: (identity: WorkerIdentity) => void;
|
|
14
|
+
taskExecutor?: (taskDescription: string, taskId: string) => Promise<string>;
|
|
15
|
+
prepareTaskAssignment?: (assignment: TaskAssignmentPayload) => Promise<void> | void;
|
|
16
|
+
publishTaskAssignment?: (assignment: TaskAssignmentPayload, result: string) => Promise<void> | void;
|
|
17
|
+
cancelTaskExecution?: (taskId: string) => Promise<boolean> | boolean;
|
|
18
|
+
messageQueue?: MessageQueue;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function createWorkerService(deps: WorkerServiceDeps): OpenClawPluginService {
|
|
22
|
+
const { config, logger, onIdentityEstablished, taskExecutor: externalTaskExecutor } = deps;
|
|
23
|
+
let identityManager: IdentityManager;
|
|
24
|
+
let messageQueue: MessageQueue;
|
|
25
|
+
let server: http.Server | null = null;
|
|
26
|
+
let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
27
|
+
let controllerUrl: string | null = null;
|
|
28
|
+
let workerId: string | null = null;
|
|
29
|
+
let activeTaskId: string | undefined;
|
|
30
|
+
const cancelledTaskIds = new Set<string>();
|
|
31
|
+
|
|
32
|
+
const taskExecutor = externalTaskExecutor
|
|
33
|
+
? async (assignment: TaskAssignmentPayload): Promise<string> => {
|
|
34
|
+
const taskId = assignment.taskId;
|
|
35
|
+
cancelledTaskIds.delete(taskId);
|
|
36
|
+
activeTaskId = taskId;
|
|
37
|
+
try {
|
|
38
|
+
await deps.prepareTaskAssignment?.(assignment);
|
|
39
|
+
const taskPrompt = [assignment.title.trim(), assignment.description.trim()].filter(Boolean).join("\n\n");
|
|
40
|
+
const result = await externalTaskExecutor(taskPrompt, taskId);
|
|
41
|
+
if (cancelledTaskIds.has(taskId)) {
|
|
42
|
+
throw new Error("Task execution cancelled by controller");
|
|
43
|
+
}
|
|
44
|
+
await deps.publishTaskAssignment?.(assignment, result);
|
|
45
|
+
return result;
|
|
46
|
+
} finally {
|
|
47
|
+
activeTaskId = undefined;
|
|
48
|
+
if (!cancelledTaskIds.has(taskId)) {
|
|
49
|
+
cancelledTaskIds.delete(taskId);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
: undefined;
|
|
54
|
+
|
|
55
|
+
function reportTaskResult(taskId: string, result: string, error: string | null): void {
|
|
56
|
+
if (cancelledTaskIds.has(taskId)) {
|
|
57
|
+
logger.info(`Worker: suppressing result report for cancelled task ${taskId}`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (!controllerUrl) return;
|
|
61
|
+
fetch(`${controllerUrl}/api/v1/tasks/${taskId}/result`, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: { "Content-Type": "application/json" },
|
|
64
|
+
body: JSON.stringify({ result, error, workerId }),
|
|
65
|
+
}).catch((err) => {
|
|
66
|
+
logger.error(`Worker: failed to report task result: ${String(err)}`);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function cancelAssignedTask(taskId: string): Promise<boolean> {
|
|
71
|
+
if (activeTaskId !== taskId) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
cancelledTaskIds.add(taskId);
|
|
76
|
+
try {
|
|
77
|
+
const cancelled = await deps.cancelTaskExecution?.(taskId);
|
|
78
|
+
return cancelled ?? true;
|
|
79
|
+
} catch (err) {
|
|
80
|
+
logger.warn(`Worker: failed to cancel task ${taskId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isTaskCancelled(taskId: string): boolean {
|
|
86
|
+
return cancelledTaskIds.has(taskId);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function startServer(): Promise<void> {
|
|
90
|
+
const handler = createWorkerHttpHandler(
|
|
91
|
+
{ role: config.role, port: config.port },
|
|
92
|
+
logger,
|
|
93
|
+
messageQueue,
|
|
94
|
+
workerId ?? "",
|
|
95
|
+
taskExecutor,
|
|
96
|
+
reportTaskResult,
|
|
97
|
+
cancelAssignedTask,
|
|
98
|
+
isTaskCancelled,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (server) {
|
|
102
|
+
await new Promise<void>((resolve) => server!.close(() => resolve()));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
server = http.createServer(handler);
|
|
106
|
+
await new Promise<void>((resolve, reject) => {
|
|
107
|
+
server!.listen(config.port, () => {
|
|
108
|
+
logger.info(`Worker: HTTP server listening on port ${config.port}`);
|
|
109
|
+
resolve();
|
|
110
|
+
});
|
|
111
|
+
server!.on("error", reject);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
id: "teamclaw-worker",
|
|
117
|
+
async start(_ctx: OpenClawPluginServiceContext) {
|
|
118
|
+
await ensureOpenClawWorkspaceMemoryDir(logger);
|
|
119
|
+
|
|
120
|
+
messageQueue = deps.messageQueue ?? new MessageQueue();
|
|
121
|
+
identityManager = new IdentityManager(config, logger);
|
|
122
|
+
|
|
123
|
+
// Start HTTP server initially (without identity)
|
|
124
|
+
await startServer();
|
|
125
|
+
|
|
126
|
+
// Register with controller
|
|
127
|
+
const identity = await identityManager.register();
|
|
128
|
+
if (!identity) {
|
|
129
|
+
logger.warn("Worker: could not register with controller, will retry on next heartbeat");
|
|
130
|
+
} else {
|
|
131
|
+
controllerUrl = identity.controllerUrl;
|
|
132
|
+
workerId = identity.workerId;
|
|
133
|
+
// Restart server with worker ID and task executor
|
|
134
|
+
await startServer();
|
|
135
|
+
onIdentityEstablished(identity);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Start heartbeat
|
|
139
|
+
heartbeatTimer = setInterval(async () => {
|
|
140
|
+
if (!identityManager.hasIdentity()) {
|
|
141
|
+
const newIdentity = await identityManager.register();
|
|
142
|
+
if (newIdentity && !controllerUrl) {
|
|
143
|
+
controllerUrl = newIdentity.controllerUrl;
|
|
144
|
+
workerId = newIdentity.workerId;
|
|
145
|
+
await startServer();
|
|
146
|
+
onIdentityEstablished(newIdentity);
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const id = identityManager.getIdentity();
|
|
152
|
+
if (!id || !controllerUrl) return;
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const heartbeat = createHeartbeatPayload(
|
|
156
|
+
id.workerId,
|
|
157
|
+
activeTaskId ? "busy" : "idle",
|
|
158
|
+
activeTaskId,
|
|
159
|
+
);
|
|
160
|
+
const res = await fetch(`${controllerUrl}/api/v1/workers/${id.workerId}/heartbeat`, {
|
|
161
|
+
method: "POST",
|
|
162
|
+
headers: { "Content-Type": "application/json" },
|
|
163
|
+
body: JSON.stringify(heartbeat),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (!res.ok) {
|
|
167
|
+
logger.warn(`Worker: heartbeat failed (${res.status})`);
|
|
168
|
+
}
|
|
169
|
+
} catch (err) {
|
|
170
|
+
logger.warn(`Worker: heartbeat error: ${err instanceof Error ? err.message : String(err)}`);
|
|
171
|
+
}
|
|
172
|
+
}, config.heartbeatIntervalMs);
|
|
173
|
+
|
|
174
|
+
if (heartbeatTimer) {
|
|
175
|
+
const timer = heartbeatTimer as unknown as { unref?: () => void };
|
|
176
|
+
timer.unref?.();
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
async stop() {
|
|
180
|
+
if (heartbeatTimer) {
|
|
181
|
+
clearInterval(heartbeatTimer);
|
|
182
|
+
heartbeatTimer = null;
|
|
183
|
+
}
|
|
184
|
+
if (server) {
|
|
185
|
+
await new Promise<void>((resolve) => server!.close(() => resolve()));
|
|
186
|
+
server = null;
|
|
187
|
+
}
|
|
188
|
+
if (identityManager) {
|
|
189
|
+
await identityManager.clear();
|
|
190
|
+
}
|
|
191
|
+
logger.info("Worker: stopped");
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveDefaultOpenClawWorkspaceDir } from "./openclaw-workspace.js";
|
|
4
|
+
|
|
5
|
+
const MAX_PREVIEW_BYTES = 256 * 1024;
|
|
6
|
+
const MAX_TREE_DEPTH = 8;
|
|
7
|
+
const HIDDEN_WORKSPACE_NAMES = new Set([
|
|
8
|
+
".git",
|
|
9
|
+
".openclaw",
|
|
10
|
+
"node_modules",
|
|
11
|
+
"memory",
|
|
12
|
+
"AGENTS.md",
|
|
13
|
+
"BOOTSTRAP.md",
|
|
14
|
+
"HEARTBEAT.md",
|
|
15
|
+
"IDENTITY.md",
|
|
16
|
+
"SOUL.md",
|
|
17
|
+
"TOOLS.md",
|
|
18
|
+
"USER.md",
|
|
19
|
+
]);
|
|
20
|
+
const MARKDOWN_EXTENSIONS = new Set([".md", ".markdown", ".mdown"]);
|
|
21
|
+
const HTML_EXTENSIONS = new Set([".html", ".htm"]);
|
|
22
|
+
const TEXT_EXTENSIONS = new Set([
|
|
23
|
+
".txt",
|
|
24
|
+
".js",
|
|
25
|
+
".mjs",
|
|
26
|
+
".cjs",
|
|
27
|
+
".jsx",
|
|
28
|
+
".ts",
|
|
29
|
+
".tsx",
|
|
30
|
+
".css",
|
|
31
|
+
".scss",
|
|
32
|
+
".sass",
|
|
33
|
+
".less",
|
|
34
|
+
".json",
|
|
35
|
+
".yml",
|
|
36
|
+
".yaml",
|
|
37
|
+
".xml",
|
|
38
|
+
".svg",
|
|
39
|
+
".sh",
|
|
40
|
+
".bash",
|
|
41
|
+
".zsh",
|
|
42
|
+
".py",
|
|
43
|
+
".go",
|
|
44
|
+
".rs",
|
|
45
|
+
".java",
|
|
46
|
+
".kt",
|
|
47
|
+
".swift",
|
|
48
|
+
".rb",
|
|
49
|
+
".php",
|
|
50
|
+
".sql",
|
|
51
|
+
".env",
|
|
52
|
+
".gitignore",
|
|
53
|
+
".gitattributes",
|
|
54
|
+
".npmrc",
|
|
55
|
+
".editorconfig",
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
export type WorkspaceTreeNode = {
|
|
59
|
+
name: string;
|
|
60
|
+
path: string;
|
|
61
|
+
type: "directory" | "file";
|
|
62
|
+
size?: number;
|
|
63
|
+
previewType?: "source" | "markdown" | "html" | "binary";
|
|
64
|
+
children?: WorkspaceTreeNode[];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type WorkspaceTreePayload = {
|
|
68
|
+
root: string;
|
|
69
|
+
entries: WorkspaceTreeNode[];
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type WorkspaceFilePayload = {
|
|
73
|
+
name: string;
|
|
74
|
+
path: string;
|
|
75
|
+
size: number;
|
|
76
|
+
extension: string;
|
|
77
|
+
previewType: "source" | "markdown" | "html" | "binary";
|
|
78
|
+
truncated: boolean;
|
|
79
|
+
content?: string;
|
|
80
|
+
rawUrl: string;
|
|
81
|
+
contentType: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export async function listWorkspaceTree(): Promise<WorkspaceTreePayload> {
|
|
85
|
+
const workspaceDir = await ensureWorkspaceDir();
|
|
86
|
+
const entries = await readTree(workspaceDir, "", 0);
|
|
87
|
+
return {
|
|
88
|
+
root: "/",
|
|
89
|
+
entries,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function readWorkspaceFile(relativePath: string): Promise<WorkspaceFilePayload> {
|
|
94
|
+
const { normalizedPath, absolutePath } = await resolveWorkspacePath(relativePath);
|
|
95
|
+
const stat = await fs.stat(absolutePath);
|
|
96
|
+
if (!stat.isFile()) {
|
|
97
|
+
throw new Error("Workspace path is not a file");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const extension = path.extname(normalizedPath).toLowerCase();
|
|
101
|
+
const contentType = getContentType(normalizedPath);
|
|
102
|
+
const handle = await fs.open(absolutePath, "r");
|
|
103
|
+
try {
|
|
104
|
+
const length = Math.min(stat.size, MAX_PREVIEW_BYTES + 1);
|
|
105
|
+
const buffer = Buffer.alloc(length);
|
|
106
|
+
const { bytesRead } = await handle.read(buffer, 0, length, 0);
|
|
107
|
+
const slice = buffer.subarray(0, bytesRead);
|
|
108
|
+
const truncated = stat.size > MAX_PREVIEW_BYTES;
|
|
109
|
+
const previewType = detectPreviewType(normalizedPath, slice);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
name: path.basename(normalizedPath),
|
|
113
|
+
path: normalizedPath,
|
|
114
|
+
size: stat.size,
|
|
115
|
+
extension,
|
|
116
|
+
previewType,
|
|
117
|
+
truncated,
|
|
118
|
+
content: previewType === "binary" ? undefined : slice.subarray(0, Math.min(slice.length, MAX_PREVIEW_BYTES)).toString("utf8"),
|
|
119
|
+
rawUrl: buildWorkspaceRawUrl(normalizedPath),
|
|
120
|
+
contentType,
|
|
121
|
+
};
|
|
122
|
+
} finally {
|
|
123
|
+
await handle.close();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function readWorkspaceRawFile(relativePath: string): Promise<{
|
|
128
|
+
content: Buffer;
|
|
129
|
+
contentType: string;
|
|
130
|
+
}> {
|
|
131
|
+
const { absolutePath } = await resolveWorkspacePath(relativePath);
|
|
132
|
+
const stat = await fs.stat(absolutePath);
|
|
133
|
+
if (!stat.isFile()) {
|
|
134
|
+
throw new Error("Workspace path is not a file");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
content: await fs.readFile(absolutePath),
|
|
139
|
+
contentType: getContentType(absolutePath),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function buildWorkspaceRawUrl(relativePath: string): string {
|
|
144
|
+
const normalizedPath = normalizeWorkspacePath(relativePath);
|
|
145
|
+
const encodedPath = normalizedPath.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
146
|
+
return `/api/v1/workspace/raw/${encodedPath}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function ensureWorkspaceDir(): Promise<string> {
|
|
150
|
+
const workspaceDir = resolveDefaultOpenClawWorkspaceDir();
|
|
151
|
+
await fs.mkdir(workspaceDir, { recursive: true });
|
|
152
|
+
return workspaceDir;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function resolveWorkspacePath(relativePath: string): Promise<{
|
|
156
|
+
workspaceDir: string;
|
|
157
|
+
normalizedPath: string;
|
|
158
|
+
absolutePath: string;
|
|
159
|
+
}> {
|
|
160
|
+
const workspaceDir = await ensureWorkspaceDir();
|
|
161
|
+
const normalizedPath = normalizeWorkspacePath(relativePath);
|
|
162
|
+
if (!normalizedPath) {
|
|
163
|
+
throw new Error("workspace path is required");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const absolutePath = path.resolve(workspaceDir, normalizedPath);
|
|
167
|
+
const relativeFromWorkspace = path.relative(workspaceDir, absolutePath).replace(/\\/g, "/");
|
|
168
|
+
if (!relativeFromWorkspace || relativeFromWorkspace.startsWith("..") || path.isAbsolute(relativeFromWorkspace)) {
|
|
169
|
+
throw new Error("workspace path must stay inside the workspace");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
workspaceDir,
|
|
174
|
+
normalizedPath: relativeFromWorkspace,
|
|
175
|
+
absolutePath,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function normalizeWorkspacePath(relativePath: string): string {
|
|
180
|
+
const value = String(relativePath || "").trim().replace(/\\/g, "/").replace(/^\/+/, "");
|
|
181
|
+
if (!value) {
|
|
182
|
+
return "";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const normalized = path.posix.normalize(value);
|
|
186
|
+
if (normalized === "." || normalized === ".." || normalized.startsWith("../")) {
|
|
187
|
+
throw new Error("Invalid workspace path");
|
|
188
|
+
}
|
|
189
|
+
return normalized;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function readTree(dirPath: string, relativeDir: string, depth: number): Promise<WorkspaceTreeNode[]> {
|
|
193
|
+
if (depth > MAX_TREE_DEPTH) {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const dirents = await fs.readdir(dirPath, { withFileTypes: true });
|
|
198
|
+
const visibleDirents = dirents
|
|
199
|
+
.filter((dirent) => !HIDDEN_WORKSPACE_NAMES.has(dirent.name))
|
|
200
|
+
.sort((left, right) => {
|
|
201
|
+
if (left.isDirectory() && !right.isDirectory()) return -1;
|
|
202
|
+
if (!left.isDirectory() && right.isDirectory()) return 1;
|
|
203
|
+
return left.name.localeCompare(right.name);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const nodes: WorkspaceTreeNode[] = [];
|
|
207
|
+
for (const dirent of visibleDirents) {
|
|
208
|
+
const childRelativePath = relativeDir ? `${relativeDir}/${dirent.name}` : dirent.name;
|
|
209
|
+
const childAbsolutePath = path.join(dirPath, dirent.name);
|
|
210
|
+
|
|
211
|
+
if (dirent.isDirectory()) {
|
|
212
|
+
nodes.push({
|
|
213
|
+
name: dirent.name,
|
|
214
|
+
path: childRelativePath,
|
|
215
|
+
type: "directory",
|
|
216
|
+
children: await readTree(childAbsolutePath, childRelativePath, depth + 1),
|
|
217
|
+
});
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!dirent.isFile()) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const stat = await fs.stat(childAbsolutePath);
|
|
226
|
+
nodes.push({
|
|
227
|
+
name: dirent.name,
|
|
228
|
+
path: childRelativePath,
|
|
229
|
+
type: "file",
|
|
230
|
+
size: stat.size,
|
|
231
|
+
previewType: classifyPreviewType(childRelativePath),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return nodes;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function classifyPreviewType(filePath: string): "source" | "markdown" | "html" | "binary" {
|
|
239
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
240
|
+
if (MARKDOWN_EXTENSIONS.has(extension)) {
|
|
241
|
+
return "markdown";
|
|
242
|
+
}
|
|
243
|
+
if (HTML_EXTENSIONS.has(extension)) {
|
|
244
|
+
return "html";
|
|
245
|
+
}
|
|
246
|
+
return TEXT_EXTENSIONS.has(extension) ? "source" : "binary";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function detectPreviewType(filePath: string, content: Buffer): "source" | "markdown" | "html" | "binary" {
|
|
250
|
+
const classified = classifyPreviewType(filePath);
|
|
251
|
+
if (classified === "markdown" || classified === "html") {
|
|
252
|
+
return classified;
|
|
253
|
+
}
|
|
254
|
+
if (classified === "source") {
|
|
255
|
+
return "source";
|
|
256
|
+
}
|
|
257
|
+
return isLikelyBinary(content) ? "binary" : "source";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function isLikelyBinary(content: Buffer): boolean {
|
|
261
|
+
const sample = content.subarray(0, Math.min(content.length, 4096));
|
|
262
|
+
for (const byte of sample) {
|
|
263
|
+
if (byte === 0) {
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function getContentType(filePath: string): string {
|
|
271
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
272
|
+
switch (extension) {
|
|
273
|
+
case ".html":
|
|
274
|
+
case ".htm":
|
|
275
|
+
return "text/html; charset=utf-8";
|
|
276
|
+
case ".md":
|
|
277
|
+
case ".markdown":
|
|
278
|
+
case ".mdown":
|
|
279
|
+
return "text/markdown; charset=utf-8";
|
|
280
|
+
case ".css":
|
|
281
|
+
return "text/css; charset=utf-8";
|
|
282
|
+
case ".js":
|
|
283
|
+
case ".mjs":
|
|
284
|
+
case ".cjs":
|
|
285
|
+
return "application/javascript; charset=utf-8";
|
|
286
|
+
case ".json":
|
|
287
|
+
return "application/json; charset=utf-8";
|
|
288
|
+
case ".svg":
|
|
289
|
+
return "image/svg+xml";
|
|
290
|
+
case ".txt":
|
|
291
|
+
case ".sh":
|
|
292
|
+
case ".bash":
|
|
293
|
+
case ".zsh":
|
|
294
|
+
case ".yml":
|
|
295
|
+
case ".yaml":
|
|
296
|
+
case ".ts":
|
|
297
|
+
case ".tsx":
|
|
298
|
+
case ".jsx":
|
|
299
|
+
case ".py":
|
|
300
|
+
case ".go":
|
|
301
|
+
case ".rs":
|
|
302
|
+
case ".java":
|
|
303
|
+
case ".kt":
|
|
304
|
+
case ".swift":
|
|
305
|
+
case ".rb":
|
|
306
|
+
case ".php":
|
|
307
|
+
case ".sql":
|
|
308
|
+
return "text/plain; charset=utf-8";
|
|
309
|
+
default:
|
|
310
|
+
return "application/octet-stream";
|
|
311
|
+
}
|
|
312
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"allowImportingTsExtensions": true,
|
|
4
|
+
"allowSyntheticDefaultImports": true,
|
|
5
|
+
"esModuleInterop": true,
|
|
6
|
+
"forceConsistentCasingInFileNames": true,
|
|
7
|
+
"lib": ["DOM", "DOM.Iterable", "ES2023"],
|
|
8
|
+
"module": "NodeNext",
|
|
9
|
+
"moduleResolution": "NodeNext",
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"target": "es2023",
|
|
15
|
+
"paths": {
|
|
16
|
+
"openclaw/plugin-sdk": ["../openclaw/src/plugin-sdk/index.ts"],
|
|
17
|
+
"openclaw/plugin-sdk/*": ["../openclaw/src/plugin-sdk/*"]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"include": ["./**/*.ts", "./*.ts"],
|
|
21
|
+
"exclude": ["node_modules", "dist"]
|
|
22
|
+
}
|