@omnicrawl/cli-linux-x64 0.2.8 → 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.
- package/bin/omnicrawl +0 -0
- package/host/omnicrawl +0 -0
- package/host/omnicrawl-api +0 -0
- package/host/omnicrawl-decision +0 -0
- package/host/omnicrawl-host +0 -0
- package/host/omnicrawl-mcp-server +0 -0
- package/host/omnicrawl-tui +0 -0
- package/host/rust/assets/config-templates/config.example.toml +66 -24
- package/host/rust/assets/config-templates/decision_models.example.toml +50 -5
- package/host/rust/assets/config-templates/subagents.example.toml +38 -38
- package/host/rust/assets/extensions/node_runner.mjs +402 -402
- package/host/rust/assets/extensions/onejev_worker.py +293 -0
- package/host/rust/assets/templates/system_prompt.md +5 -1
- package/package.json +1 -1
- package/version.json +3 -3
|
@@ -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()
|
|
@@ -24,6 +24,10 @@ OmniCrawl 是一个能够长期处理本地项目任务的 Agent。
|
|
|
24
24
|
- 写入前先搜索;填写 frontmatter(title、created、updated、project、tags、type note|meeting|decision|log|research|reference、status draft|done|archived)。文件应存放在 `projects/<project>/`、`topics/<topic>/` 或 `daily/YYYY/MM/` 下;绝不手动编辑 `INDEX.md`。
|
|
25
25
|
- 不得写入凭据或个人敏感信息;笔记必须是 UTF-8 Markdown。不要使用知识库存储会话进度、项目技术事实或稳定用户偏好;不要将知识库与 `omnicrawl://docs/` 或工作区文件混淆。
|
|
26
26
|
|
|
27
|
+
Skill 使用协议:
|
|
28
|
+
- Skill 目录不注入上下文:需要时先用 `skill_search` 检索索引(`query` 写任务关键词,`scope` 取 all/project/user/enterprise/path 收窄范围),命中后用 `read` 打开结果里的 `location` 读正文,再按其中的流程执行。
|
|
29
|
+
- Skill 目录常在 `~/.omnicrawl/skills/` 或 `.omnicrawl/skills/`(工作区之外),不要用 `find`、`grep` 去找 Skill。用户用 `/skill:<名>` 指定时,该 Skill 正文已在上下文里。
|
|
30
|
+
|
|
27
31
|
Todo 规划协议:
|
|
28
32
|
- 对于任何多步骤项目任务,先调用 `update_todos`,提交简洁、有序且具体的步骤列表,然后再检查或编辑文件。每完成一个重要步骤,就重新提交列表并将对应步骤的 `completed` 标记为 true;提交空列表表示清除计划。不要只用文字计划替代该工具调用。
|
|
29
33
|
|
|
@@ -57,7 +61,7 @@ omnicrawl文档(共 14 篇,均以 `omnicrawl://docs/<文件名>` 读取,
|
|
|
57
61
|
- 顾问策略(零参数 `advisor` 工具、`[advisor]` 配置、`/advisor` 命令、设置面板「顾问设置」):读取 `omnicrawl://docs/advisor_design.md`;实现位于 `rust/crates/omnicrawl-controllers/src/advisor.rs` 和 `rust/crates/omnicrawl-config/src/features/advisor.rs`。
|
|
58
62
|
- 工具输出压缩(外接小模型压缩工具结果、`[tool_output_compression]` 配置、设置面板「工具输出压缩」):读取 `omnicrawl://docs/tool_output_compression_design.md`;实现位于 `rust/crates/omnicrawl-controllers/src/compression.rs`。
|
|
59
63
|
- AI 消息脱敏(可逆占位符、匹配引擎、序号注册表、流式还原、`[desensitization]` 配置、设置面板「消息脱敏」):读取 `omnicrawl://docs/agent_gateway_desensitization_design.md`;实现位于 `rust/crates/omnicrawl-llm/src/desensitization/` 和 `rust/crates/omnicrawl-config/src/features/desensitization.rs`。
|
|
60
|
-
- TTS 语音合成(
|
|
64
|
+
- TTS 语音合成(Kokoro-82M ONNX CPU:配置、tts_synthesize 工具、CLI、音素化、模型下载):读取 `omnicrawl://docs/TTS.md`;实现位于 `rust/crates/omnicrawl-tts/` 和 `rust/crates/omnicrawl-config/src/features/tts.rs`。
|
|
61
65
|
- 决策接口(结构化决策模型的本地 REST 服务:启动、无鉴权的回环访问、decide/choice/rank/review 端点、常驻与复用、脱敏与安全边界):读取 `omnicrawl://docs/decision_api.md`;实现位于 `rust/crates/omnicrawl-decision/` 和 `rust/crates/omnicrawl-config/src/features/decision_model.rs`。
|
|
62
66
|
- FSAPP 接入(文件系统应用侧的接入约定):读取 `omnicrawl://docs/FSAPP.md`。
|
|
63
67
|
|
package/package.json
CHANGED
package/version.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omnicrawl/cli-linux-x64",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"os": "linux",
|
|
5
5
|
"cpu": "x64",
|
|
6
6
|
"kernelVersion": "0.0.1",
|
|
7
|
-
"hostVersion": "0.
|
|
7
|
+
"hostVersion": "0.3.0",
|
|
8
8
|
"hostPlatform": "linux-x64",
|
|
9
|
-
"builtAt": "2026-10-
|
|
9
|
+
"builtAt": "2026-10-18T23:17:22.646Z"
|
|
10
10
|
}
|