@omnicrawl/cli-win32-x64 0.4.2 → 0.4.3
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/bin/omnicrawl.exe +0 -0
- package/host/omnicrawl/templates/plan.md +1 -1
- package/host/omnicrawl/templates/subagents/explore.md +5 -1
- package/host/omnicrawl/templates/subagents/general-purpose.md +6 -2
- package/host/omnicrawl/templates/subagents/plan.md +5 -1
- package/host/omnicrawl/templates/subagents/review.md +5 -1
- package/host/omnicrawl/templates/subagents/verify.md +5 -1
- package/host/omnicrawl-api.exe +0 -0
- package/host/omnicrawl-decision.exe +0 -0
- package/host/omnicrawl-fetch.exe +0 -0
- package/host/omnicrawl-host.exe +0 -0
- package/host/omnicrawl-mcp-server.exe +0 -0
- package/host/omnicrawl-tui.exe +0 -0
- package/host/omnicrawl.exe +0 -0
- package/host/rust/assets/templates/plan.md +1 -1
- package/host/rust/assets/templates/subagents/explore.md +5 -1
- package/host/rust/assets/templates/subagents/general-purpose.md +6 -2
- package/host/rust/assets/templates/subagents/plan.md +5 -1
- package/host/rust/assets/templates/subagents/review.md +5 -1
- package/host/rust/assets/templates/subagents/verify.md +5 -1
- package/package.json +1 -1
- package/version.json +4 -4
- package/host/omnicrawl/extensions/node_runner.mjs +0 -402
- package/host/rust/assets/extensions/node_runner.mjs +0 -402
package/bin/omnicrawl.exe
CHANGED
|
Binary file
|
|
@@ -15,7 +15,11 @@ tools:
|
|
|
15
15
|
- bash
|
|
16
16
|
- powershell
|
|
17
17
|
disallowedTools:
|
|
18
|
-
-
|
|
18
|
+
- subagent_run
|
|
19
|
+
- subagent_list
|
|
20
|
+
- subagent_cancel
|
|
21
|
+
- subagent_apply_worktree
|
|
22
|
+
- subagent_discard_worktree
|
|
19
23
|
- switch_model
|
|
20
24
|
- switch_skill
|
|
21
25
|
- load_skill
|
|
@@ -36,7 +40,7 @@ background: false
|
|
|
36
40
|
5. 完成后用简洁中文总结:改了什么、为什么、如何验证、残留风险。
|
|
37
41
|
|
|
38
42
|
约束:
|
|
39
|
-
- 禁止调用
|
|
43
|
+
- 禁止调用 subagent_run、subagent_list、subagent_cancel、subagent_apply_worktree、subagent_discard_worktree、switch_model、switch_skill、load_skill、search_skills。
|
|
40
44
|
- 不要提交 git commit、push 或改远程。
|
|
41
45
|
- 不要删除用户未明确要求删除的文件。
|
|
42
46
|
- isolation=worktree 时,结果会以分支/diff 形式返回父 Agent,由父决定是否应用。
|
package/host/omnicrawl-api.exe
CHANGED
|
Binary file
|
|
Binary file
|
package/host/omnicrawl-fetch.exe
CHANGED
|
Binary file
|
package/host/omnicrawl-host.exe
CHANGED
|
Binary file
|
|
Binary file
|
package/host/omnicrawl-tui.exe
CHANGED
|
Binary file
|
package/host/omnicrawl.exe
CHANGED
|
Binary file
|
|
@@ -15,7 +15,11 @@ tools:
|
|
|
15
15
|
- bash
|
|
16
16
|
- powershell
|
|
17
17
|
disallowedTools:
|
|
18
|
-
-
|
|
18
|
+
- subagent_run
|
|
19
|
+
- subagent_list
|
|
20
|
+
- subagent_cancel
|
|
21
|
+
- subagent_apply_worktree
|
|
22
|
+
- subagent_discard_worktree
|
|
19
23
|
- switch_model
|
|
20
24
|
- switch_skill
|
|
21
25
|
- load_skill
|
|
@@ -36,7 +40,7 @@ background: false
|
|
|
36
40
|
5. 完成后用简洁中文总结:改了什么、为什么、如何验证、残留风险。
|
|
37
41
|
|
|
38
42
|
约束:
|
|
39
|
-
- 禁止调用
|
|
43
|
+
- 禁止调用 subagent_run、subagent_list、subagent_cancel、subagent_apply_worktree、subagent_discard_worktree、switch_model、switch_skill、load_skill、search_skills。
|
|
40
44
|
- 不要提交 git commit、push 或改远程。
|
|
41
45
|
- 不要删除用户未明确要求删除的文件。
|
|
42
46
|
- isolation=worktree 时,结果会以分支/diff 形式返回父 Agent,由父决定是否应用。
|
package/package.json
CHANGED
package/version.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omnicrawl/cli-win32-x64",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"os": "win32",
|
|
5
5
|
"cpu": "x64",
|
|
6
|
-
"kernelVersion": "0.4.
|
|
7
|
-
"hostVersion": "0.4.
|
|
6
|
+
"kernelVersion": "0.4.3",
|
|
7
|
+
"hostVersion": "0.4.3",
|
|
8
8
|
"hostPlatform": "win32-x64",
|
|
9
|
-
"builtAt": "2026-10-
|
|
9
|
+
"builtAt": "2026-10-30T18:37:35.069Z"
|
|
10
10
|
}
|
|
@@ -1,402 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* OmniCrawl 单插件 Node runner。
|
|
4
|
-
*
|
|
5
|
-
* Host 固定调用:
|
|
6
|
-
* node node_runner.mjs --plugin-root <canonical-path>
|
|
7
|
-
*
|
|
8
|
-
* 协议:stdin/stdout NDJSON JSON-RPC 2.0。
|
|
9
|
-
* 插件日志经 SDK logger 走 stderr 结构化行;普通 console 也被重定向到 stderr。
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { createRequire } from "node:module";
|
|
13
|
-
import path from "node:path";
|
|
14
|
-
import process from "node:process";
|
|
15
|
-
import { pathToFileURL } from "node:url";
|
|
16
|
-
import readline from "node:readline";
|
|
17
|
-
import fs from "node:fs";
|
|
18
|
-
|
|
19
|
-
const require = createRequire(import.meta.url);
|
|
20
|
-
|
|
21
|
-
function parseArgs(argv) {
|
|
22
|
-
const args = { pluginRoot: "" };
|
|
23
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
24
|
-
const item = argv[i];
|
|
25
|
-
if (item === "--plugin-root") {
|
|
26
|
-
args.pluginRoot = argv[i + 1] || "";
|
|
27
|
-
i += 1;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return args;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function writeStdout(message) {
|
|
34
|
-
process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function writeStderrLine(level, message, fields = {}) {
|
|
38
|
-
const payload = {
|
|
39
|
-
level,
|
|
40
|
-
message: String(message),
|
|
41
|
-
fields,
|
|
42
|
-
ts: new Date().toISOString(),
|
|
43
|
-
};
|
|
44
|
-
process.stderr.write(`${JSON.stringify(payload)}\n`);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// 强制把 console 导向 stderr,避免污染协议 stdout。
|
|
48
|
-
const originalConsole = { ...console };
|
|
49
|
-
for (const level of ["log", "info", "warn", "error", "debug"]) {
|
|
50
|
-
console[level] = (...parts) => {
|
|
51
|
-
writeStderrLine(level === "log" ? "info" : level, parts.map(String).join(" "));
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function createLogger() {
|
|
56
|
-
return {
|
|
57
|
-
debug(message, fields) {
|
|
58
|
-
writeStderrLine("debug", message, fields || {});
|
|
59
|
-
},
|
|
60
|
-
info(message, fields) {
|
|
61
|
-
writeStderrLine("info", message, fields || {});
|
|
62
|
-
},
|
|
63
|
-
warn(message, fields) {
|
|
64
|
-
writeStderrLine("warn", message, fields || {});
|
|
65
|
-
},
|
|
66
|
-
error(message, fields) {
|
|
67
|
-
writeStderrLine("error", message, fields || {});
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function definePlugin(plugin) {
|
|
73
|
-
return plugin;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function loadPackageJson(pluginRoot) {
|
|
77
|
-
const packagePath = path.join(pluginRoot, "package.json");
|
|
78
|
-
const raw = fs.readFileSync(packagePath, "utf8");
|
|
79
|
-
return JSON.parse(raw);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async function importPlugin(entryPath) {
|
|
83
|
-
const url = pathToFileURL(entryPath).href;
|
|
84
|
-
const mod = await import(url);
|
|
85
|
-
return mod.default || mod.plugin || mod;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
class PluginRuntime {
|
|
89
|
-
constructor(pluginRoot) {
|
|
90
|
-
this.pluginRoot = path.resolve(pluginRoot);
|
|
91
|
-
this.packageJson = loadPackageJson(this.pluginRoot);
|
|
92
|
-
this.manifest = this.packageJson.omnicrawl || {};
|
|
93
|
-
this.handlers = new Map();
|
|
94
|
-
this.plugin = null;
|
|
95
|
-
this.initialized = false;
|
|
96
|
-
this.activeCancellations = new Set();
|
|
97
|
-
this.logger = createLogger();
|
|
98
|
-
this._hostRequestId = 1;
|
|
99
|
-
this._pendingHostRequests = new Map();
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** Host 对 Worker 请求的响应入口(custom.emit 等)。 */
|
|
103
|
-
resolveHostResponse(message) {
|
|
104
|
-
if (!message || message.id == null) return false;
|
|
105
|
-
const pending = this._pendingHostRequests.get(message.id);
|
|
106
|
-
if (!pending) return false;
|
|
107
|
-
this._pendingHostRequests.delete(message.id);
|
|
108
|
-
if (message.error) {
|
|
109
|
-
pending.reject(new Error(message.error.message || String(message.error)));
|
|
110
|
-
} else {
|
|
111
|
-
pending.resolve(message.result || { ok: true });
|
|
112
|
-
}
|
|
113
|
-
return true;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
_requestHost(method, params = {}) {
|
|
117
|
-
const id = this._hostRequestId;
|
|
118
|
-
this._hostRequestId += 1;
|
|
119
|
-
return new Promise((resolve, reject) => {
|
|
120
|
-
this._pendingHostRequests.set(id, { resolve, reject });
|
|
121
|
-
writeStdout({
|
|
122
|
-
jsonrpc: "2.0",
|
|
123
|
-
id,
|
|
124
|
-
method,
|
|
125
|
-
params,
|
|
126
|
-
});
|
|
127
|
-
// Host 超时由 Python 侧控制;Worker 侧给一个安全兜底。
|
|
128
|
-
setTimeout(() => {
|
|
129
|
-
if (this._pendingHostRequests.has(id)) {
|
|
130
|
-
this._pendingHostRequests.delete(id);
|
|
131
|
-
reject(new Error(`Host 请求超时:${method}`));
|
|
132
|
-
}
|
|
133
|
-
}, 10000);
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async initialize(params = {}) {
|
|
138
|
-
if (this.initialized) {
|
|
139
|
-
return this._registrationSnapshot();
|
|
140
|
-
}
|
|
141
|
-
const entryRel = this.manifest.entry || this.packageJson.main;
|
|
142
|
-
if (!entryRel) {
|
|
143
|
-
throw new Error("package.json 缺少 omnicrawl.entry / main");
|
|
144
|
-
}
|
|
145
|
-
const entryPath = path.resolve(this.pluginRoot, entryRel);
|
|
146
|
-
const rootResolved = path.resolve(this.pluginRoot);
|
|
147
|
-
if (!entryPath.startsWith(rootResolved + path.sep) && entryPath !== rootResolved) {
|
|
148
|
-
throw new Error(`entry 逃逸包根目录:${entryRel}`);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const imported = await importPlugin(entryPath);
|
|
152
|
-
const definition = typeof imported === "function" ? imported({ definePlugin }) : imported;
|
|
153
|
-
if (!definition || typeof definition.activate !== "function") {
|
|
154
|
-
throw new Error("插件必须导出 activate(context) 的 PluginDefinition");
|
|
155
|
-
}
|
|
156
|
-
this.plugin = definition;
|
|
157
|
-
|
|
158
|
-
const declared = Array.isArray(this.manifest.hooks) ? this.manifest.hooks : [];
|
|
159
|
-
const declaredIds = new Set(declared.map((item) => item.id));
|
|
160
|
-
|
|
161
|
-
const context = {
|
|
162
|
-
plugin: {
|
|
163
|
-
name: this.packageJson.name,
|
|
164
|
-
version: this.packageJson.version,
|
|
165
|
-
apiVersion: String(this.manifest.apiVersion || "1"),
|
|
166
|
-
},
|
|
167
|
-
runtime: {
|
|
168
|
-
omnicrawlVersion: params.omnicrawlVersion || process.env.OMNICRAWL_VERSION || "0.1.5",
|
|
169
|
-
nodeVersion: process.version,
|
|
170
|
-
},
|
|
171
|
-
hooks: {
|
|
172
|
-
on: (registration) => {
|
|
173
|
-
if (!registration || typeof registration !== "object") {
|
|
174
|
-
throw new Error("hooks.on(registration) 需要对象");
|
|
175
|
-
}
|
|
176
|
-
const id = String(registration.id || "");
|
|
177
|
-
const hook = String(registration.hook || "");
|
|
178
|
-
const mode = String(registration.mode || "observe");
|
|
179
|
-
const handler = registration.handler;
|
|
180
|
-
if (!declaredIds.has(id)) {
|
|
181
|
-
throw new Error(`运行期不能注册 manifest 未声明的 Handler:${id}`);
|
|
182
|
-
}
|
|
183
|
-
if (typeof handler !== "function") {
|
|
184
|
-
throw new Error(`Handler ${id} 缺少 handler 函数`);
|
|
185
|
-
}
|
|
186
|
-
this.handlers.set(id, { id, hook, mode, handler });
|
|
187
|
-
return () => {
|
|
188
|
-
this.handlers.delete(id);
|
|
189
|
-
};
|
|
190
|
-
},
|
|
191
|
-
emitCustom: async (event, payload = {}, options = {}) => {
|
|
192
|
-
if (!this.initialized) {
|
|
193
|
-
throw new Error("initialize 完成前不能 emitCustom");
|
|
194
|
-
}
|
|
195
|
-
const permissions = Array.isArray(this.manifest.permissions)
|
|
196
|
-
? this.manifest.permissions
|
|
197
|
-
: [];
|
|
198
|
-
if (!permissions.includes("hook:custom-emit")) {
|
|
199
|
-
throw new Error("缺少 hook:custom-emit 权限");
|
|
200
|
-
}
|
|
201
|
-
const version =
|
|
202
|
-
options.version != null
|
|
203
|
-
? Number(options.version)
|
|
204
|
-
: Number(
|
|
205
|
-
(Array.isArray(this.manifest.customEvents)
|
|
206
|
-
? this.manifest.customEvents.find((item) => item.name === event)
|
|
207
|
-
: null)?.version || 1
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
// 同插件订阅必须本地投递:Host 若在 hook.invoke 中再回调本 Worker
|
|
211
|
-
// 会与“等待 custom.emit 响应”形成死锁。
|
|
212
|
-
let localDelivered = 0;
|
|
213
|
-
const localEvent = {
|
|
214
|
-
apiVersion: String(this.manifest.apiVersion || "1"),
|
|
215
|
-
hook: event,
|
|
216
|
-
payload: payload || {},
|
|
217
|
-
timestamp: new Date().toISOString(),
|
|
218
|
-
};
|
|
219
|
-
for (const binding of this.handlers.values()) {
|
|
220
|
-
if (binding.hook !== event) continue;
|
|
221
|
-
await binding.handler(localEvent);
|
|
222
|
-
localDelivered += 1;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const result = await this._requestHost("custom.emit", {
|
|
226
|
-
event,
|
|
227
|
-
version,
|
|
228
|
-
payload: payload || {},
|
|
229
|
-
localDelivered,
|
|
230
|
-
});
|
|
231
|
-
return {
|
|
232
|
-
...(result || { ok: true }),
|
|
233
|
-
localDelivered,
|
|
234
|
-
delivered: Number(result?.delivered || 0) + localDelivered,
|
|
235
|
-
};
|
|
236
|
-
},
|
|
237
|
-
},
|
|
238
|
-
logger: this.logger,
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
await definition.activate(context);
|
|
242
|
-
|
|
243
|
-
// 若插件未在 activate 中显式 on(),但 manifest 声明了 hooks,则保持空绑定;
|
|
244
|
-
// Host 会在二次校验时发现实际注册子集。
|
|
245
|
-
this.initialized = true;
|
|
246
|
-
return this._registrationSnapshot();
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
_registrationSnapshot() {
|
|
250
|
-
const registrations = [];
|
|
251
|
-
for (const item of this.handlers.values()) {
|
|
252
|
-
registrations.push({
|
|
253
|
-
id: item.id,
|
|
254
|
-
hook: item.hook,
|
|
255
|
-
mode: item.mode,
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
// 若 activate 没手动 on,回落为 manifest 声明(开发期便利);
|
|
259
|
-
// Host 仍要求它们是 manifest 子集。
|
|
260
|
-
if (registrations.length === 0 && Array.isArray(this.manifest.hooks)) {
|
|
261
|
-
for (const item of this.manifest.hooks) {
|
|
262
|
-
registrations.push({
|
|
263
|
-
id: item.id,
|
|
264
|
-
hook: item.hook,
|
|
265
|
-
mode: item.mode,
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
return {
|
|
270
|
-
plugin: {
|
|
271
|
-
name: this.packageJson.name,
|
|
272
|
-
version: this.packageJson.version,
|
|
273
|
-
apiVersion: String(this.manifest.apiVersion || "1"),
|
|
274
|
-
},
|
|
275
|
-
handlers: registrations,
|
|
276
|
-
capabilities: {
|
|
277
|
-
customEmit: Boolean(
|
|
278
|
-
Array.isArray(this.manifest.permissions) &&
|
|
279
|
-
this.manifest.permissions.includes("hook:custom-emit")
|
|
280
|
-
),
|
|
281
|
-
},
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
async invoke(handlerId, event) {
|
|
286
|
-
const binding = this.handlers.get(handlerId);
|
|
287
|
-
if (!binding) {
|
|
288
|
-
// 允许仅声明未绑定:默认 continue,便于 observe 空实现。
|
|
289
|
-
return { action: "continue" };
|
|
290
|
-
}
|
|
291
|
-
const result = await binding.handler(event);
|
|
292
|
-
if (result == null) {
|
|
293
|
-
return { action: "continue" };
|
|
294
|
-
}
|
|
295
|
-
if (typeof result !== "object") {
|
|
296
|
-
throw new Error(`Handler ${handlerId} 必须返回对象`);
|
|
297
|
-
}
|
|
298
|
-
return result;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
async shutdown() {
|
|
302
|
-
if (this.plugin && typeof this.plugin.deactivate === "function") {
|
|
303
|
-
await this.plugin.deactivate();
|
|
304
|
-
}
|
|
305
|
-
this.handlers.clear();
|
|
306
|
-
this.initialized = false;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
async function main() {
|
|
311
|
-
const args = parseArgs(process.argv.slice(2));
|
|
312
|
-
if (!args.pluginRoot) {
|
|
313
|
-
writeStderrLine("error", "missing --plugin-root");
|
|
314
|
-
process.exit(2);
|
|
315
|
-
}
|
|
316
|
-
const runtime = new PluginRuntime(args.pluginRoot);
|
|
317
|
-
const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
318
|
-
|
|
319
|
-
const sendResult = (id, result) => {
|
|
320
|
-
writeStdout({ jsonrpc: "2.0", id, result });
|
|
321
|
-
};
|
|
322
|
-
const sendError = (id, code, message) => {
|
|
323
|
-
writeStdout({ jsonrpc: "2.0", id, error: { code, message } });
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
// 必须用事件驱动读取 stdin:handler 内 await custom.emit 时 Host 响应
|
|
327
|
-
// 也要从同一 stdin 进来。若 for-await 串行阻塞,会形成死锁超时。
|
|
328
|
-
let shuttingDown = false;
|
|
329
|
-
const inflight = new Set();
|
|
330
|
-
|
|
331
|
-
const handleMessage = async (message) => {
|
|
332
|
-
// Host 对 Worker 请求的响应:无 method,带 id+result/error。
|
|
333
|
-
if (
|
|
334
|
-
message.id != null &&
|
|
335
|
-
(Object.prototype.hasOwnProperty.call(message, "result") ||
|
|
336
|
-
Object.prototype.hasOwnProperty.call(message, "error")) &&
|
|
337
|
-
!message.method
|
|
338
|
-
) {
|
|
339
|
-
runtime.resolveHostResponse(message);
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const { id, method, params } = message;
|
|
344
|
-
try {
|
|
345
|
-
if (method === "initialize") {
|
|
346
|
-
const result = await runtime.initialize(params || {});
|
|
347
|
-
if (id !== undefined) sendResult(id, result);
|
|
348
|
-
} else if (method === "hook.invoke") {
|
|
349
|
-
const handlerId = params?.handlerId;
|
|
350
|
-
const event = params?.event || {};
|
|
351
|
-
const result = await runtime.invoke(handlerId, event);
|
|
352
|
-
if (id !== undefined) sendResult(id, result);
|
|
353
|
-
} else if (method === "hook.cancel") {
|
|
354
|
-
const requestId = params?.requestId;
|
|
355
|
-
if (requestId != null) runtime.activeCancellations.add(requestId);
|
|
356
|
-
if (id !== undefined) sendResult(id, { ok: true });
|
|
357
|
-
} else if (method === "ping") {
|
|
358
|
-
if (id !== undefined) sendResult(id, { ok: true, ts: Date.now() });
|
|
359
|
-
} else if (method === "shutdown") {
|
|
360
|
-
shuttingDown = true;
|
|
361
|
-
await runtime.shutdown();
|
|
362
|
-
if (id !== undefined) sendResult(id, { ok: true });
|
|
363
|
-
process.exit(0);
|
|
364
|
-
} else if (method) {
|
|
365
|
-
if (id !== undefined) sendError(id, -32601, `Method not found: ${method}`);
|
|
366
|
-
}
|
|
367
|
-
} catch (error) {
|
|
368
|
-
const text = error instanceof Error ? error.message : String(error);
|
|
369
|
-
writeStderrLine("error", text);
|
|
370
|
-
if (id !== undefined) sendError(id, -32000, text);
|
|
371
|
-
}
|
|
372
|
-
};
|
|
373
|
-
|
|
374
|
-
rl.on("line", (line) => {
|
|
375
|
-
if (shuttingDown) return;
|
|
376
|
-
const trimmed = String(line || "").trim();
|
|
377
|
-
if (!trimmed) return;
|
|
378
|
-
let message;
|
|
379
|
-
try {
|
|
380
|
-
message = JSON.parse(trimmed);
|
|
381
|
-
} catch (error) {
|
|
382
|
-
writeStderrLine("error", "invalid json from host", { error: String(error) });
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
if (!message || typeof message !== "object") return;
|
|
386
|
-
const task = handleMessage(message);
|
|
387
|
-
inflight.add(task);
|
|
388
|
-
task.finally(() => inflight.delete(task));
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
await new Promise((resolve) => {
|
|
392
|
-
rl.on("close", resolve);
|
|
393
|
-
});
|
|
394
|
-
await Promise.allSettled([...inflight]);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
main().catch((error) => {
|
|
398
|
-
writeStderrLine("error", error instanceof Error ? error.message : String(error));
|
|
399
|
-
process.exit(1);
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
export { definePlugin };
|
|
@@ -1,402 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* OmniCrawl 单插件 Node runner。
|
|
4
|
-
*
|
|
5
|
-
* Host 固定调用:
|
|
6
|
-
* node node_runner.mjs --plugin-root <canonical-path>
|
|
7
|
-
*
|
|
8
|
-
* 协议:stdin/stdout NDJSON JSON-RPC 2.0。
|
|
9
|
-
* 插件日志经 SDK logger 走 stderr 结构化行;普通 console 也被重定向到 stderr。
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { createRequire } from "node:module";
|
|
13
|
-
import path from "node:path";
|
|
14
|
-
import process from "node:process";
|
|
15
|
-
import { pathToFileURL } from "node:url";
|
|
16
|
-
import readline from "node:readline";
|
|
17
|
-
import fs from "node:fs";
|
|
18
|
-
|
|
19
|
-
const require = createRequire(import.meta.url);
|
|
20
|
-
|
|
21
|
-
function parseArgs(argv) {
|
|
22
|
-
const args = { pluginRoot: "" };
|
|
23
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
24
|
-
const item = argv[i];
|
|
25
|
-
if (item === "--plugin-root") {
|
|
26
|
-
args.pluginRoot = argv[i + 1] || "";
|
|
27
|
-
i += 1;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return args;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function writeStdout(message) {
|
|
34
|
-
process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function writeStderrLine(level, message, fields = {}) {
|
|
38
|
-
const payload = {
|
|
39
|
-
level,
|
|
40
|
-
message: String(message),
|
|
41
|
-
fields,
|
|
42
|
-
ts: new Date().toISOString(),
|
|
43
|
-
};
|
|
44
|
-
process.stderr.write(`${JSON.stringify(payload)}\n`);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// 强制把 console 导向 stderr,避免污染协议 stdout。
|
|
48
|
-
const originalConsole = { ...console };
|
|
49
|
-
for (const level of ["log", "info", "warn", "error", "debug"]) {
|
|
50
|
-
console[level] = (...parts) => {
|
|
51
|
-
writeStderrLine(level === "log" ? "info" : level, parts.map(String).join(" "));
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function createLogger() {
|
|
56
|
-
return {
|
|
57
|
-
debug(message, fields) {
|
|
58
|
-
writeStderrLine("debug", message, fields || {});
|
|
59
|
-
},
|
|
60
|
-
info(message, fields) {
|
|
61
|
-
writeStderrLine("info", message, fields || {});
|
|
62
|
-
},
|
|
63
|
-
warn(message, fields) {
|
|
64
|
-
writeStderrLine("warn", message, fields || {});
|
|
65
|
-
},
|
|
66
|
-
error(message, fields) {
|
|
67
|
-
writeStderrLine("error", message, fields || {});
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function definePlugin(plugin) {
|
|
73
|
-
return plugin;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function loadPackageJson(pluginRoot) {
|
|
77
|
-
const packagePath = path.join(pluginRoot, "package.json");
|
|
78
|
-
const raw = fs.readFileSync(packagePath, "utf8");
|
|
79
|
-
return JSON.parse(raw);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async function importPlugin(entryPath) {
|
|
83
|
-
const url = pathToFileURL(entryPath).href;
|
|
84
|
-
const mod = await import(url);
|
|
85
|
-
return mod.default || mod.plugin || mod;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
class PluginRuntime {
|
|
89
|
-
constructor(pluginRoot) {
|
|
90
|
-
this.pluginRoot = path.resolve(pluginRoot);
|
|
91
|
-
this.packageJson = loadPackageJson(this.pluginRoot);
|
|
92
|
-
this.manifest = this.packageJson.omnicrawl || {};
|
|
93
|
-
this.handlers = new Map();
|
|
94
|
-
this.plugin = null;
|
|
95
|
-
this.initialized = false;
|
|
96
|
-
this.activeCancellations = new Set();
|
|
97
|
-
this.logger = createLogger();
|
|
98
|
-
this._hostRequestId = 1;
|
|
99
|
-
this._pendingHostRequests = new Map();
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** Host 对 Worker 请求的响应入口(custom.emit 等)。 */
|
|
103
|
-
resolveHostResponse(message) {
|
|
104
|
-
if (!message || message.id == null) return false;
|
|
105
|
-
const pending = this._pendingHostRequests.get(message.id);
|
|
106
|
-
if (!pending) return false;
|
|
107
|
-
this._pendingHostRequests.delete(message.id);
|
|
108
|
-
if (message.error) {
|
|
109
|
-
pending.reject(new Error(message.error.message || String(message.error)));
|
|
110
|
-
} else {
|
|
111
|
-
pending.resolve(message.result || { ok: true });
|
|
112
|
-
}
|
|
113
|
-
return true;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
_requestHost(method, params = {}) {
|
|
117
|
-
const id = this._hostRequestId;
|
|
118
|
-
this._hostRequestId += 1;
|
|
119
|
-
return new Promise((resolve, reject) => {
|
|
120
|
-
this._pendingHostRequests.set(id, { resolve, reject });
|
|
121
|
-
writeStdout({
|
|
122
|
-
jsonrpc: "2.0",
|
|
123
|
-
id,
|
|
124
|
-
method,
|
|
125
|
-
params,
|
|
126
|
-
});
|
|
127
|
-
// Host 超时由 Python 侧控制;Worker 侧给一个安全兜底。
|
|
128
|
-
setTimeout(() => {
|
|
129
|
-
if (this._pendingHostRequests.has(id)) {
|
|
130
|
-
this._pendingHostRequests.delete(id);
|
|
131
|
-
reject(new Error(`Host 请求超时:${method}`));
|
|
132
|
-
}
|
|
133
|
-
}, 10000);
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async initialize(params = {}) {
|
|
138
|
-
if (this.initialized) {
|
|
139
|
-
return this._registrationSnapshot();
|
|
140
|
-
}
|
|
141
|
-
const entryRel = this.manifest.entry || this.packageJson.main;
|
|
142
|
-
if (!entryRel) {
|
|
143
|
-
throw new Error("package.json 缺少 omnicrawl.entry / main");
|
|
144
|
-
}
|
|
145
|
-
const entryPath = path.resolve(this.pluginRoot, entryRel);
|
|
146
|
-
const rootResolved = path.resolve(this.pluginRoot);
|
|
147
|
-
if (!entryPath.startsWith(rootResolved + path.sep) && entryPath !== rootResolved) {
|
|
148
|
-
throw new Error(`entry 逃逸包根目录:${entryRel}`);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const imported = await importPlugin(entryPath);
|
|
152
|
-
const definition = typeof imported === "function" ? imported({ definePlugin }) : imported;
|
|
153
|
-
if (!definition || typeof definition.activate !== "function") {
|
|
154
|
-
throw new Error("插件必须导出 activate(context) 的 PluginDefinition");
|
|
155
|
-
}
|
|
156
|
-
this.plugin = definition;
|
|
157
|
-
|
|
158
|
-
const declared = Array.isArray(this.manifest.hooks) ? this.manifest.hooks : [];
|
|
159
|
-
const declaredIds = new Set(declared.map((item) => item.id));
|
|
160
|
-
|
|
161
|
-
const context = {
|
|
162
|
-
plugin: {
|
|
163
|
-
name: this.packageJson.name,
|
|
164
|
-
version: this.packageJson.version,
|
|
165
|
-
apiVersion: String(this.manifest.apiVersion || "1"),
|
|
166
|
-
},
|
|
167
|
-
runtime: {
|
|
168
|
-
omnicrawlVersion: params.omnicrawlVersion || process.env.OMNICRAWL_VERSION || "0.1.5",
|
|
169
|
-
nodeVersion: process.version,
|
|
170
|
-
},
|
|
171
|
-
hooks: {
|
|
172
|
-
on: (registration) => {
|
|
173
|
-
if (!registration || typeof registration !== "object") {
|
|
174
|
-
throw new Error("hooks.on(registration) 需要对象");
|
|
175
|
-
}
|
|
176
|
-
const id = String(registration.id || "");
|
|
177
|
-
const hook = String(registration.hook || "");
|
|
178
|
-
const mode = String(registration.mode || "observe");
|
|
179
|
-
const handler = registration.handler;
|
|
180
|
-
if (!declaredIds.has(id)) {
|
|
181
|
-
throw new Error(`运行期不能注册 manifest 未声明的 Handler:${id}`);
|
|
182
|
-
}
|
|
183
|
-
if (typeof handler !== "function") {
|
|
184
|
-
throw new Error(`Handler ${id} 缺少 handler 函数`);
|
|
185
|
-
}
|
|
186
|
-
this.handlers.set(id, { id, hook, mode, handler });
|
|
187
|
-
return () => {
|
|
188
|
-
this.handlers.delete(id);
|
|
189
|
-
};
|
|
190
|
-
},
|
|
191
|
-
emitCustom: async (event, payload = {}, options = {}) => {
|
|
192
|
-
if (!this.initialized) {
|
|
193
|
-
throw new Error("initialize 完成前不能 emitCustom");
|
|
194
|
-
}
|
|
195
|
-
const permissions = Array.isArray(this.manifest.permissions)
|
|
196
|
-
? this.manifest.permissions
|
|
197
|
-
: [];
|
|
198
|
-
if (!permissions.includes("hook:custom-emit")) {
|
|
199
|
-
throw new Error("缺少 hook:custom-emit 权限");
|
|
200
|
-
}
|
|
201
|
-
const version =
|
|
202
|
-
options.version != null
|
|
203
|
-
? Number(options.version)
|
|
204
|
-
: Number(
|
|
205
|
-
(Array.isArray(this.manifest.customEvents)
|
|
206
|
-
? this.manifest.customEvents.find((item) => item.name === event)
|
|
207
|
-
: null)?.version || 1
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
// 同插件订阅必须本地投递:Host 若在 hook.invoke 中再回调本 Worker
|
|
211
|
-
// 会与“等待 custom.emit 响应”形成死锁。
|
|
212
|
-
let localDelivered = 0;
|
|
213
|
-
const localEvent = {
|
|
214
|
-
apiVersion: String(this.manifest.apiVersion || "1"),
|
|
215
|
-
hook: event,
|
|
216
|
-
payload: payload || {},
|
|
217
|
-
timestamp: new Date().toISOString(),
|
|
218
|
-
};
|
|
219
|
-
for (const binding of this.handlers.values()) {
|
|
220
|
-
if (binding.hook !== event) continue;
|
|
221
|
-
await binding.handler(localEvent);
|
|
222
|
-
localDelivered += 1;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const result = await this._requestHost("custom.emit", {
|
|
226
|
-
event,
|
|
227
|
-
version,
|
|
228
|
-
payload: payload || {},
|
|
229
|
-
localDelivered,
|
|
230
|
-
});
|
|
231
|
-
return {
|
|
232
|
-
...(result || { ok: true }),
|
|
233
|
-
localDelivered,
|
|
234
|
-
delivered: Number(result?.delivered || 0) + localDelivered,
|
|
235
|
-
};
|
|
236
|
-
},
|
|
237
|
-
},
|
|
238
|
-
logger: this.logger,
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
await definition.activate(context);
|
|
242
|
-
|
|
243
|
-
// 若插件未在 activate 中显式 on(),但 manifest 声明了 hooks,则保持空绑定;
|
|
244
|
-
// Host 会在二次校验时发现实际注册子集。
|
|
245
|
-
this.initialized = true;
|
|
246
|
-
return this._registrationSnapshot();
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
_registrationSnapshot() {
|
|
250
|
-
const registrations = [];
|
|
251
|
-
for (const item of this.handlers.values()) {
|
|
252
|
-
registrations.push({
|
|
253
|
-
id: item.id,
|
|
254
|
-
hook: item.hook,
|
|
255
|
-
mode: item.mode,
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
// 若 activate 没手动 on,回落为 manifest 声明(开发期便利);
|
|
259
|
-
// Host 仍要求它们是 manifest 子集。
|
|
260
|
-
if (registrations.length === 0 && Array.isArray(this.manifest.hooks)) {
|
|
261
|
-
for (const item of this.manifest.hooks) {
|
|
262
|
-
registrations.push({
|
|
263
|
-
id: item.id,
|
|
264
|
-
hook: item.hook,
|
|
265
|
-
mode: item.mode,
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
return {
|
|
270
|
-
plugin: {
|
|
271
|
-
name: this.packageJson.name,
|
|
272
|
-
version: this.packageJson.version,
|
|
273
|
-
apiVersion: String(this.manifest.apiVersion || "1"),
|
|
274
|
-
},
|
|
275
|
-
handlers: registrations,
|
|
276
|
-
capabilities: {
|
|
277
|
-
customEmit: Boolean(
|
|
278
|
-
Array.isArray(this.manifest.permissions) &&
|
|
279
|
-
this.manifest.permissions.includes("hook:custom-emit")
|
|
280
|
-
),
|
|
281
|
-
},
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
async invoke(handlerId, event) {
|
|
286
|
-
const binding = this.handlers.get(handlerId);
|
|
287
|
-
if (!binding) {
|
|
288
|
-
// 允许仅声明未绑定:默认 continue,便于 observe 空实现。
|
|
289
|
-
return { action: "continue" };
|
|
290
|
-
}
|
|
291
|
-
const result = await binding.handler(event);
|
|
292
|
-
if (result == null) {
|
|
293
|
-
return { action: "continue" };
|
|
294
|
-
}
|
|
295
|
-
if (typeof result !== "object") {
|
|
296
|
-
throw new Error(`Handler ${handlerId} 必须返回对象`);
|
|
297
|
-
}
|
|
298
|
-
return result;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
async shutdown() {
|
|
302
|
-
if (this.plugin && typeof this.plugin.deactivate === "function") {
|
|
303
|
-
await this.plugin.deactivate();
|
|
304
|
-
}
|
|
305
|
-
this.handlers.clear();
|
|
306
|
-
this.initialized = false;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
async function main() {
|
|
311
|
-
const args = parseArgs(process.argv.slice(2));
|
|
312
|
-
if (!args.pluginRoot) {
|
|
313
|
-
writeStderrLine("error", "missing --plugin-root");
|
|
314
|
-
process.exit(2);
|
|
315
|
-
}
|
|
316
|
-
const runtime = new PluginRuntime(args.pluginRoot);
|
|
317
|
-
const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
318
|
-
|
|
319
|
-
const sendResult = (id, result) => {
|
|
320
|
-
writeStdout({ jsonrpc: "2.0", id, result });
|
|
321
|
-
};
|
|
322
|
-
const sendError = (id, code, message) => {
|
|
323
|
-
writeStdout({ jsonrpc: "2.0", id, error: { code, message } });
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
// 必须用事件驱动读取 stdin:handler 内 await custom.emit 时 Host 响应
|
|
327
|
-
// 也要从同一 stdin 进来。若 for-await 串行阻塞,会形成死锁超时。
|
|
328
|
-
let shuttingDown = false;
|
|
329
|
-
const inflight = new Set();
|
|
330
|
-
|
|
331
|
-
const handleMessage = async (message) => {
|
|
332
|
-
// Host 对 Worker 请求的响应:无 method,带 id+result/error。
|
|
333
|
-
if (
|
|
334
|
-
message.id != null &&
|
|
335
|
-
(Object.prototype.hasOwnProperty.call(message, "result") ||
|
|
336
|
-
Object.prototype.hasOwnProperty.call(message, "error")) &&
|
|
337
|
-
!message.method
|
|
338
|
-
) {
|
|
339
|
-
runtime.resolveHostResponse(message);
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const { id, method, params } = message;
|
|
344
|
-
try {
|
|
345
|
-
if (method === "initialize") {
|
|
346
|
-
const result = await runtime.initialize(params || {});
|
|
347
|
-
if (id !== undefined) sendResult(id, result);
|
|
348
|
-
} else if (method === "hook.invoke") {
|
|
349
|
-
const handlerId = params?.handlerId;
|
|
350
|
-
const event = params?.event || {};
|
|
351
|
-
const result = await runtime.invoke(handlerId, event);
|
|
352
|
-
if (id !== undefined) sendResult(id, result);
|
|
353
|
-
} else if (method === "hook.cancel") {
|
|
354
|
-
const requestId = params?.requestId;
|
|
355
|
-
if (requestId != null) runtime.activeCancellations.add(requestId);
|
|
356
|
-
if (id !== undefined) sendResult(id, { ok: true });
|
|
357
|
-
} else if (method === "ping") {
|
|
358
|
-
if (id !== undefined) sendResult(id, { ok: true, ts: Date.now() });
|
|
359
|
-
} else if (method === "shutdown") {
|
|
360
|
-
shuttingDown = true;
|
|
361
|
-
await runtime.shutdown();
|
|
362
|
-
if (id !== undefined) sendResult(id, { ok: true });
|
|
363
|
-
process.exit(0);
|
|
364
|
-
} else if (method) {
|
|
365
|
-
if (id !== undefined) sendError(id, -32601, `Method not found: ${method}`);
|
|
366
|
-
}
|
|
367
|
-
} catch (error) {
|
|
368
|
-
const text = error instanceof Error ? error.message : String(error);
|
|
369
|
-
writeStderrLine("error", text);
|
|
370
|
-
if (id !== undefined) sendError(id, -32000, text);
|
|
371
|
-
}
|
|
372
|
-
};
|
|
373
|
-
|
|
374
|
-
rl.on("line", (line) => {
|
|
375
|
-
if (shuttingDown) return;
|
|
376
|
-
const trimmed = String(line || "").trim();
|
|
377
|
-
if (!trimmed) return;
|
|
378
|
-
let message;
|
|
379
|
-
try {
|
|
380
|
-
message = JSON.parse(trimmed);
|
|
381
|
-
} catch (error) {
|
|
382
|
-
writeStderrLine("error", "invalid json from host", { error: String(error) });
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
if (!message || typeof message !== "object") return;
|
|
386
|
-
const task = handleMessage(message);
|
|
387
|
-
inflight.add(task);
|
|
388
|
-
task.finally(() => inflight.delete(task));
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
await new Promise((resolve) => {
|
|
392
|
-
rl.on("close", resolve);
|
|
393
|
-
});
|
|
394
|
-
await Promise.allSettled([...inflight]);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
main().catch((error) => {
|
|
398
|
-
writeStderrLine("error", error instanceof Error ? error.message : String(error));
|
|
399
|
-
process.exit(1);
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
export { definePlugin };
|