@omnicrawl/cli-linux-arm-musl 0.2.6 → 0.3.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 (28) hide show
  1. package/README.md +1 -1
  2. package/bin/omnicrawl +0 -0
  3. package/host/omnicrawl +0 -0
  4. package/host/omnicrawl-api +0 -0
  5. package/host/omnicrawl-decision +0 -0
  6. package/host/omnicrawl-host +0 -0
  7. package/host/omnicrawl-mcp-server +0 -0
  8. package/host/omnicrawl-tui +0 -0
  9. package/host/rust/assets/config-templates/config.example.toml +325 -0
  10. package/host/rust/assets/config-templates/decision_models.example.toml +162 -0
  11. package/host/rust/assets/config-templates/models.example.toml +71 -0
  12. package/host/rust/assets/config-templates/subagents.example.toml +38 -0
  13. package/host/rust/assets/extensions/node_runner.mjs +402 -0
  14. package/host/rust/assets/extensions/onejev_worker.py +293 -0
  15. package/host/rust/assets/templates/advisor_system.md +22 -0
  16. package/host/rust/assets/templates/model_reply_compaction_system.md +28 -0
  17. package/host/rust/assets/templates/plan.md +110 -0
  18. package/host/rust/assets/templates/subagents/explore.md +20 -0
  19. package/host/rust/assets/templates/subagents/general-purpose.md +42 -0
  20. package/host/rust/assets/templates/subagents/plan.md +20 -0
  21. package/host/rust/assets/templates/subagents/review.md +130 -0
  22. package/host/rust/assets/templates/subagents/verify.md +35 -0
  23. package/host/rust/assets/templates/summary_prompt.md +77 -0
  24. package/host/rust/assets/templates/system_prompt.md +76 -0
  25. package/host/rust/assets/templates/tool_call_summary_system.md +30 -0
  26. package/host/rust/assets/templates/tool_output_compression_system.md +28 -0
  27. package/package.json +10 -3
  28. package/version.json +4 -4
@@ -0,0 +1,402 @@
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 };
@@ -0,0 +1,293 @@
1
+ """OneJev 本地决策 worker:在命名管道(Windows)/ Unix socket 上提供 System One 判定。
2
+
3
+ 为什么不用 `qev serve`:官方那个服务经 uvicorn/FastAPI 暴露 HTTP,本机调用方因此要为
4
+ 一次纯内存判定多付一层 TCP + ASGI 中转。这个 worker 直接持有 `qev` 的引擎对象,
5
+ 请求从管道进来、答案从管道出去,中间只有一次 JSON 编解码。
6
+
7
+ 协议(NDJSON,一行一个 JSON 对象):
8
+
9
+ 请求 {"model": "...", "state": {...}, "questions": {...}}
10
+ 成功 {"model": "...", "answers": {...}, "usage": {...}, "qev": {...}}
11
+ 失败 {"error": "可读原因"}
12
+
13
+ 响应形状故意与 HTTP 端点一致,Rust 侧读答案的代码因此不必区分两种传输。
14
+
15
+ 服务是**本机共用**的常驻进程,由 OmniCrawl 宿主以脱离进程的方式拉起(见
16
+ `omnicrawl-onejev` 的 `server` 模块):加载权重、创建端点、打出就绪标记,之后一直服务;
17
+ 停止只能显式做(宿主按 PID 回收整棵进程树)。
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import json
24
+ import logging
25
+ import os
26
+ import socket
27
+ import sys
28
+ import threading
29
+ import time
30
+ from pathlib import Path
31
+
32
+ # 就绪标记:宿主按它认定「服务已经能接请求」(写进服务日志)。
33
+ READY_MARKER = "[onejev] 服务就绪"
34
+ # 单次连接的等待上限(秒):客户端总是连上就发,静默超时说明对端已经不在了。
35
+ CONNECTION_TIMEOUT_SECONDS = 60.0
36
+ # 管道缓冲区:一次判定请求的 state 上限是 64k 字符级别,留一倍余量。
37
+ PIPE_BUFFER_BYTES = 128 * 1024
38
+
39
+ log = logging.getLogger("onejev.worker")
40
+
41
+
42
+ class Decider:
43
+ """引擎 + 判定;串行化——一次前向独占前缀缓存,并发只会互相踩。"""
44
+
45
+ def __init__(self, engine, model_name: str) -> None:
46
+ self.engine = engine
47
+ self.model_name = model_name
48
+ self._lock = threading.Lock()
49
+
50
+ def answer(self, body: dict) -> dict:
51
+ from qev.schema import SystemOneRequest
52
+
53
+ request = SystemOneRequest.model_validate(body)
54
+ with self._lock:
55
+ response, meta = self.engine.decide(request)
56
+ response.model = self.model_name
57
+ payload = response.model_dump()
58
+ payload["qev"] = meta
59
+ return payload
60
+
61
+
62
+ def _handle_connection(decider: Decider, read_line, write_line, close) -> None:
63
+ """一次连接 = 一次判定:读一行请求,回一行响应(成功或错误),然后收线。"""
64
+ try:
65
+ line = read_line()
66
+ if not line:
67
+ return
68
+ payload = decider.answer(json.loads(line))
69
+ except Exception as error: # 请求不合法 / 引擎报错:回一行可读原因,不猜答案
70
+ log.warning("判定失败:%s", error)
71
+ message = str(error) or error.__class__.__name__
72
+ try:
73
+ write_line(json.dumps({"error": message}, ensure_ascii=False))
74
+ except Exception:
75
+ pass
76
+ else:
77
+ write_line(json.dumps(payload, ensure_ascii=False))
78
+ finally:
79
+ close()
80
+
81
+
82
+ # --------------------------------------------------------------------------
83
+ # Windows:命名管道
84
+ # --------------------------------------------------------------------------
85
+
86
+
87
+ def windows_pipe_io(endpoint: str, decider: Decider) -> None:
88
+ import ctypes
89
+ import ctypes.wintypes as wintypes
90
+ import msvcrt
91
+
92
+ kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
93
+
94
+ pipe_access_duplex = 0x00000003
95
+ pipe_type_byte = 0x00000000
96
+ pipe_readmode_byte = 0x00000000
97
+ pipe_wait = 0x00000000
98
+ pipe_unlimited_instances = 255
99
+ invalid_handle = wintypes.HANDLE(-1).value
100
+ error_pipe_connected = 535
101
+
102
+ kernel32.CreateNamedPipeW.restype = wintypes.HANDLE
103
+ kernel32.CreateNamedPipeW.argtypes = [
104
+ wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD, wintypes.DWORD,
105
+ wintypes.DWORD, wintypes.DWORD, wintypes.DWORD, ctypes.c_void_p,
106
+ ]
107
+ kernel32.ConnectNamedPipe.restype = wintypes.BOOL
108
+ kernel32.ConnectNamedPipe.argtypes = [wintypes.HANDLE, ctypes.c_void_p]
109
+ kernel32.PeekNamedPipe.restype = wintypes.BOOL
110
+ kernel32.PeekNamedPipe.argtypes = [
111
+ wintypes.HANDLE, ctypes.c_void_p, wintypes.DWORD,
112
+ ctypes.POINTER(wintypes.DWORD), ctypes.POINTER(wintypes.DWORD), ctypes.POINTER(wintypes.DWORD),
113
+ ]
114
+ kernel32.CloseHandle.restype = wintypes.BOOL
115
+ kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
116
+
117
+ def create_instance() -> int:
118
+ return kernel32.CreateNamedPipeW(
119
+ endpoint,
120
+ pipe_access_duplex,
121
+ pipe_type_byte | pipe_readmode_byte | pipe_wait,
122
+ pipe_unlimited_instances,
123
+ PIPE_BUFFER_BYTES,
124
+ PIPE_BUFFER_BYTES,
125
+ 0,
126
+ None,
127
+ )
128
+
129
+ def serve_instance(handle: int) -> None:
130
+ fd = msvcrt.open_osfhandle(handle, os.O_RDWR | os.O_BINARY)
131
+ stream = os.fdopen(fd, "r+b", buffering=0)
132
+
133
+ def read_line() -> str:
134
+ buffer = bytearray()
135
+ deadline = time.monotonic() + CONNECTION_TIMEOUT_SECONDS
136
+ while b"\n" not in buffer:
137
+ available = wintypes.DWORD(0)
138
+ kernel32.PeekNamedPipe(
139
+ wintypes.HANDLE(handle), None, 0, None, ctypes.byref(available), None
140
+ )
141
+ if available.value:
142
+ chunk = stream.read(available.value)
143
+ if not chunk:
144
+ break
145
+ buffer += chunk
146
+ continue
147
+ if time.monotonic() > deadline:
148
+ break
149
+ time.sleep(0.005)
150
+ return bytes(buffer).split(b"\n", 1)[0].decode("utf-8", "replace")
151
+
152
+ def write_line(text: str) -> None:
153
+ stream.write(text.encode("utf-8") + b"\n")
154
+
155
+ # 关闭 fd 即关闭 handle(`open_osfhandle` 把它交给了 fd)。
156
+ _handle_connection(decider, read_line, write_line, stream.close)
157
+
158
+ # 实例先建出来再报就绪:宿主看到标记时端点必须已经能被连上。
159
+ first = create_instance()
160
+ if first == invalid_handle:
161
+ raise SystemExit(f"创建命名管道失败:{ctypes.get_last_error()}({endpoint})")
162
+ print(f"{READY_MARKER} endpoint={endpoint}", flush=True)
163
+
164
+ pending = first
165
+ while True:
166
+ handle = pending
167
+ if not kernel32.ConnectNamedPipe(wintypes.HANDLE(handle), None):
168
+ error = ctypes.get_last_error()
169
+ # ERROR_PIPE_CONNECTED 说明客户端已经连上了这个实例,照旧交给服务线程。
170
+ if error != error_pipe_connected:
171
+ kernel32.CloseHandle(wintypes.HANDLE(handle))
172
+ pending = create_instance()
173
+ continue
174
+ # 一个实例只服务一个连接;下一个实例提前建好,客户端不会撞到「管道不存在」。
175
+ pending = create_instance()
176
+ threading.Thread(target=serve_instance, args=(handle,), daemon=True).start()
177
+
178
+
179
+ # --------------------------------------------------------------------------
180
+ # Unix:Unix domain socket(没有 TCP 端口,也没有 HTTP 中转)
181
+ # --------------------------------------------------------------------------
182
+
183
+
184
+ def unix_socket_io(endpoint: str, decider: Decider) -> None:
185
+ path = Path(endpoint)
186
+ if path.exists():
187
+ path.unlink()
188
+ server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
189
+ server.bind(endpoint)
190
+ server.listen(64)
191
+ os.chmod(endpoint, 0o600)
192
+ print(f"{READY_MARKER} endpoint={endpoint}", flush=True)
193
+
194
+ def serve_connection(connection: socket.socket) -> None:
195
+ stream = connection.makefile("rwb", buffering=0)
196
+
197
+ def read_line() -> str:
198
+ connection.settimeout(CONNECTION_TIMEOUT_SECONDS)
199
+ line = stream.readline()
200
+ return line.decode("utf-8", "replace").rstrip("\n") if line else ""
201
+
202
+ def write_line(text: str) -> None:
203
+ stream.write(text.encode("utf-8") + b"\n")
204
+
205
+ def close() -> None:
206
+ try:
207
+ stream.close()
208
+ finally:
209
+ connection.close()
210
+
211
+ _handle_connection(decider, read_line, write_line, close)
212
+
213
+ while True:
214
+ connection, _ = server.accept()
215
+ threading.Thread(target=serve_connection, args=(connection,), daemon=True).start()
216
+
217
+
218
+ # --------------------------------------------------------------------------
219
+ # 引擎装配(与 `qev serve` 同口径,但不启 HTTP 服务)
220
+ # --------------------------------------------------------------------------
221
+
222
+
223
+ def build_engine(args: argparse.Namespace):
224
+ from qev.calibrate import Calibration
225
+ from qev.mm_engine import local_model_dir
226
+
227
+ model_path = local_model_dir(args.model)
228
+ local = Path(model_path) / "calibration.json"
229
+ calib_path = args.calibration or (str(local) if local.exists() else None)
230
+ calibration = Calibration.load(calib_path) if calib_path else None
231
+ if calib_path:
232
+ log.info("calibration: %s", calib_path)
233
+
234
+ config = json.loads((Path(model_path) / "config.json").read_text())
235
+ if args.multimodal or config.get("vision_config") is not None:
236
+ from qev.mm_engine import MMDecisionEngine
237
+
238
+ return MMDecisionEngine(
239
+ model_path,
240
+ device=args.device,
241
+ dtype=args.dtype,
242
+ calibration=calibration,
243
+ fork_mode=args.fork_mode,
244
+ head_dtype=args.head_dtype,
245
+ max_branch_tokens=args.max_branch_tokens,
246
+ max_request_tokens=args.max_request_tokens,
247
+ gpu_preprocess=not args.no_gpu_preprocess,
248
+ cuda_graphs=not args.no_cuda_graphs,
249
+ )
250
+ from qev.engine import DecisionEngine
251
+
252
+ return DecisionEngine(
253
+ model_path,
254
+ device=args.device,
255
+ dtype=args.dtype,
256
+ calibration=calibration,
257
+ fork_mode=args.fork_mode,
258
+ head_dtype=args.head_dtype,
259
+ max_branch_tokens=args.max_branch_tokens,
260
+ max_request_tokens=args.max_request_tokens,
261
+ )
262
+
263
+
264
+ def main() -> None:
265
+ parser = argparse.ArgumentParser(prog="onejev_worker")
266
+ parser.add_argument("--model", required=True, help="本地权重目录(或 Hub id)")
267
+ parser.add_argument("--name", default=None, help="对外的模型名")
268
+ parser.add_argument("--endpoint", required=True, help="命名管道名或 Unix socket 路径")
269
+ parser.add_argument("--device", default="cuda")
270
+ parser.add_argument("--dtype", default="bfloat16")
271
+ parser.add_argument("--head-dtype", default="float32")
272
+ parser.add_argument("--fork-mode", default="auto")
273
+ parser.add_argument("--multimodal", action="store_true")
274
+ parser.add_argument("--no-cuda-graphs", action="store_true")
275
+ parser.add_argument("--no-gpu-preprocess", action="store_true")
276
+ parser.add_argument("--calibration", default=None)
277
+ parser.add_argument("--max-branch-tokens", type=int, default=32768)
278
+ parser.add_argument("--max-request-tokens", type=int, default=65536)
279
+ args = parser.parse_args()
280
+
281
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s")
282
+
283
+ engine = build_engine(args)
284
+ decider = Decider(engine, args.name or Path(args.model).name)
285
+
286
+ if sys.platform == "win32":
287
+ windows_pipe_io(args.endpoint, decider)
288
+ else:
289
+ unix_socket_io(args.endpoint, decider)
290
+
291
+
292
+ if __name__ == "__main__":
293
+ main()