dsh-moli 1.1.14 → 1.1.15
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 -4
- package/README.zh.md +52 -8
- package/client.js +347 -0
- package/index.js +212 -10
- package/lib/install.js +37 -3
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -311,10 +311,13 @@ capabilities always visible to the model.
|
|
|
311
311
|
2. **Binary output is copied byte for byte.** PowerShell's `>` operator re-encodes a native
|
|
312
312
|
program's output as UTF-16, inserting a `00` after every byte, which corrupts a screenshot.
|
|
313
313
|
Bytes here are written verbatim, so the PNG signature stays `89504e470d0a1a0a`.
|
|
314
|
-
3. **
|
|
314
|
+
3. **One optional external dependency.** A profile plugin is loaded from its real path on disk (a `link:`
|
|
315
315
|
dependency stays a symlink target), so a bare specifier such as `@deepseek-ai/schemastery`
|
|
316
|
-
resolves against the plugin directory instead of the profile's `node_modules
|
|
317
|
-
|
|
316
|
+
resolves against the plugin directory instead of the profile's `node_modules`. The import is
|
|
317
|
+
therefore guarded: without schemastery (a `link:` install) the settings form does not appear and
|
|
318
|
+
the plugin degrades to plain patch-layer configuration, while `moli_fetch` and `moli_probe` keep
|
|
319
|
+
registering. With it (a normal npm install, which brings the dependency along) the settings page
|
|
320
|
+
is available.
|
|
318
321
|
|
|
319
322
|
## Platform support
|
|
320
323
|
|
|
@@ -358,6 +361,8 @@ exercised the proxy download route as well as the engine paths.
|
|
|
358
361
|
## Configuration
|
|
359
362
|
|
|
360
363
|
Override any field under the plugin's `config` entry in the profile patch layer. All are optional.
|
|
364
|
+
The same fields are also editable from the **Moli 安装与抓取** settings section on the plugin's page in
|
|
365
|
+
the Plugins manager (a `plugins.bundle.config` form registered by the browser half).
|
|
361
366
|
|
|
362
367
|
```yaml
|
|
363
368
|
- insert:
|
|
@@ -379,6 +384,22 @@ Without `exePath`, the plugin probes `exePath` → `MOLI_BIN` → `%LOCALAPPDATA
|
|
|
379
384
|
`%USERPROFILE%\.local\bin\moli.exe`, then scans inside `%LOCALAPPDATA%\Moli` for a `moli.exe`, so an
|
|
380
385
|
installation under a versioned subdirectory is still found.
|
|
381
386
|
|
|
387
|
+
### Engine lifecycle from the settings page
|
|
388
|
+
|
|
389
|
+
The settings section also carries an **engine** row that detects the installed engine and lets you
|
|
390
|
+
install or uninstall it without touching the profile:
|
|
391
|
+
|
|
392
|
+
- **state** — `engineState` is `installed` (with `engineVersion`), `missing`, or a short error text.
|
|
393
|
+
- **install** — the form writes `engineAction: "install"`; the host downloads and runs the official
|
|
394
|
+
installer asynchronously, then reports the outcome back through `engineState` / `engineVersion`.
|
|
395
|
+
- **uninstall** — `engineAction: "uninstall"` removes the engine. On Windows only the installer-owned
|
|
396
|
+
`%LOCALAPPDATA%\Moli` tree is deleted; without one, nothing is touched. On POSIX the located binary
|
|
397
|
+
is removed and the now-empty install directories are pruned.
|
|
398
|
+
|
|
399
|
+
These three fields (`engineAction`, `engineState`, `engineVersion`) are part of the live Config schema;
|
|
400
|
+
`engineAction` is a `none`/`install`/`uninstall` union. They are meant to be driven by the settings
|
|
401
|
+
form, so prefer editing the four fields above for routine configuration.
|
|
402
|
+
|
|
382
403
|
## Known limitations
|
|
383
404
|
|
|
384
405
|
- **macOS is untested, and Linux was tested by the contributor rather than the maintainer.** The
|
|
@@ -416,8 +437,14 @@ installation under a versioned subdirectory is still found.
|
|
|
416
437
|
|
|
417
438
|
## Verification
|
|
418
439
|
|
|
419
|
-
|
|
440
|
+
Eight suites live in [`verify/`](verify/), and all are runnable against a working tree:
|
|
420
441
|
|
|
442
|
+
- `config-verify.mjs` checks the settings surface and engine lifecycle added by the plugin's browser
|
|
443
|
+
half: the volatile `Config` schema (every declared field, `engineAction` as a `none`/`install`/
|
|
444
|
+
`uninstall` union), `apply` reading both a plain config and a volatile `.get()` reference, the
|
|
445
|
+
`settings/document-updated` wiring, `uninstallMoli` (platform-aware: POSIX removes the located
|
|
446
|
+
binary, Windows removes only the installer-owned `%LOCALAPPDATA%\Moli` tree), and the client.js
|
|
447
|
+
shape (`plugins.bundle.config` registration, zh/en dictionaries, raw `true`/`false` select labels).
|
|
421
448
|
- `schema-verify.mjs` checks the tool definitions with the registry's own validator, including
|
|
422
449
|
controls that assert the two invalid schema shapes still fail.
|
|
423
450
|
- `install-verify.mjs` drives the install pipeline against a loopback HTTP server: route ordering,
|
|
@@ -459,6 +486,12 @@ through a proxy, that proxy too. Useful where a network is available, but a suit
|
|
|
459
486
|
because a website changed is worse than no suite, so it stays a local check. If you run it and see
|
|
460
487
|
failures in sections 4–6 while sections 1–3 pass, suspect the network before the code.
|
|
461
488
|
|
|
489
|
+
### Prerequisites for the suites
|
|
490
|
+
|
|
491
|
+
The plugin has one optional runtime dependency — [`@deepseek-ai/schemastery`](https://www.npmjs.com/package/@deepseek-ai/schemastery) — used by the live `Config` schema that registers the settings namespace (optional because the import is guarded, so a `link:` install without it still loads and registers the tools). Suites that import the plugin (`config-verify`, `schema-verify`, `empty-verify`, `report-verify`, `http-verify`) need it resolvable from the working tree, so run `npm install` first.
|
|
492
|
+
|
|
493
|
+
`schema-verify` additionally imports the registry's own validator out of the npm-global `@deepseek-ai/dsh` install (`@deepseek-ai/dsh-tools`); `report-verify` and the engine suites need a real Moli engine, and `empty-verify` needs a C compiler to build its stub engines. On Windows, `@deepseek-ai/schemastery` can be resolved through a directory junction into the fork's `node_modules` from the npm-global DSH tree, the same way the suites resolve `dsh-tools`.
|
|
494
|
+
|
|
462
495
|
Beyond those, `lib/moli.js` and the tool layer were exercised end to end against Moli 1.1.16 on
|
|
463
496
|
Windows: registration, probe, Markdown and JSON dumps, `eval` (including a tight-timeout regression
|
|
464
497
|
guard for the `-t` argument position), PNG and PDF artifacts with valid signatures, truncation, every
|
package/README.zh.md
CHANGED
|
@@ -289,9 +289,10 @@ Moli 仓库自带官方 agent skill:[`moli-webfetch`](https://github.com/lexmo
|
|
|
289
289
|
重定向到临时文件再读回。
|
|
290
290
|
2. **二进制按原始字节落盘。** PowerShell 的 `>` 会把原生程序输出按 UTF-16 重编码(每字节后插
|
|
291
291
|
`00`),截图因此损坏。这里直接写字节,PNG 签名保持 `89504e470d0a1a0a`。
|
|
292
|
-
3.
|
|
293
|
-
导入(例如 `@deepseek-ai/schemastery`)都会解析到插件目录而非 profile 的 `node_modules
|
|
294
|
-
|
|
292
|
+
3. **一个可选的外部依赖。** profile 插件按磁盘真实路径加载(`link:` 依赖仍是符号链接目标),任何裸包名
|
|
293
|
+
导入(例如 `@deepseek-ai/schemastery`)都会解析到插件目录而非 profile 的 `node_modules`。因此该导入
|
|
294
|
+
是**受保护的**:没有 schemastery(`link:` 安装)时设置界面不出现、插件退化为纯补丁层配置,而
|
|
295
|
+
`moli_fetch` 与 `moli_probe` 照常注册;有它(普通 npm 安装会自动带上)时设置页面可用。
|
|
295
296
|
|
|
296
297
|
## 平台支持
|
|
297
298
|
|
|
@@ -328,7 +329,7 @@ cookie 驱动的查询参数。那次实测同时覆盖了代理下载路由与
|
|
|
328
329
|
|
|
329
330
|
## 配置
|
|
330
331
|
|
|
331
|
-
在 profile 的补丁层里覆盖 `config
|
|
332
|
+
在 profile 的补丁层里覆盖 `config`,全部可选。也可以在图 **Moli 设置** 界面里编辑(见下方「设置页入口」)。
|
|
332
333
|
|
|
333
334
|
```yaml
|
|
334
335
|
- insert:
|
|
@@ -349,6 +350,32 @@ profile。无论生效的是哪个代理,loopback 地址一律绕过它。
|
|
|
349
350
|
→ `%USERPROFILE%\.local\bin\moli.exe`,随后还会在 `%LOCALAPPDATA%\Moli` 内递归查找 `moli.exe`,
|
|
350
351
|
所以装在带版本号的子目录里也能找到。
|
|
351
352
|
|
|
353
|
+
### 设置页入口
|
|
354
|
+
|
|
355
|
+
插件自带一个**设置界面**,挂在插件管理页的对应插件条目上(`plugins.bundle.config` 槽位,由浏览器端
|
|
356
|
+
`client.js` 注册)。两种方式进入:
|
|
357
|
+
|
|
358
|
+
1. **插件管理页**:打开 DSH Web 界面的插件管理页,找到 **dsh-moli** 条目,其配置区即是本设置界面;
|
|
359
|
+
引擎状态、安装/卸载按钮、以及下方所有配置字段都在这里编辑。
|
|
360
|
+
2. **直接改 profile**:按上面的 YAML 改 `cordis.patch.yml` 同样生效,两边是同一份 `config`,保存即同步。
|
|
361
|
+
|
|
362
|
+
设置界面跟随 DSH 当前语言(内置 `zh` / `en`,英语兜底),布尔字段(如 `insecureTls`)以下拉选择
|
|
363
|
+
**原始值**(`true` / `false`)显示,不做本地化翻译。
|
|
364
|
+
|
|
365
|
+
### 引擎生命周期(设置页的「引擎」行)
|
|
366
|
+
|
|
367
|
+
设置界面顶部有一条**引擎**状态行,自动检测已安装的引擎,并可一键安装/卸载,无需动 profile:
|
|
368
|
+
|
|
369
|
+
- **状态** — `engineState` 为 `installed`(同时给出 `engineVersion`)、`missing`,或一段简短错误文本。
|
|
370
|
+
- **安装** — 点「安装引擎」后表单写入 `engineAction: "install"`,主机异步下载并运行官方安装器,
|
|
371
|
+
完成后通过 `engineState` / `engineVersion` 回写结果。
|
|
372
|
+
- **卸载** — 「卸载引擎」写入 `engineAction: "uninstall"`。Windows 上只删除安装器所属的
|
|
373
|
+
`%LOCALAPPDATA%\Moli` 目录树;没有该目录树时**不触碰任何文件**。POSIX 上删除定位到的二进制,
|
|
374
|
+
并清理随之变空的安装目录。
|
|
375
|
+
|
|
376
|
+
`engineAction` / `engineState` / `engineVersion` 三个字段属于动态 Config schema(`engineAction` 是
|
|
377
|
+
`none` / `install` / `uninstall` 三选一),设计上由设置界面驱动;日常改配置请优先用上面的四个字段。
|
|
378
|
+
|
|
352
379
|
## 已知限制
|
|
353
380
|
|
|
354
381
|
- **macOS 未经验证,Linux 是由贡献者而非维护者验证的。** 维护者是一名**没有 macOS / Linux 电脑**的
|
|
@@ -376,8 +403,13 @@ profile。无论生效的是哪个代理,loopback 地址一律绕过它。
|
|
|
376
403
|
|
|
377
404
|
## 验证情况
|
|
378
405
|
|
|
379
|
-
[`verify/`](verify/)
|
|
406
|
+
[`verify/`](verify/) 下有八个可直接对工作区运行的测试:
|
|
380
407
|
|
|
408
|
+
- `config-verify.mjs` 检查设置面与引擎生命周期:动态 `Config` schema(每个已声明字段、`engineAction`
|
|
409
|
+
为 `none`/`install`/`uninstall` 三选一)、`apply` 同时接受普通配置与动态 `.get()` 引用、
|
|
410
|
+
`settings/document-updated` 接线、`uninstallMoli`(平台感知:POSIX 删除定位到的二进制,Windows
|
|
411
|
+
只删除安装器所属的 `%LOCALAPPDATA%\Moli` 目录树)、以及 client.js 的形状(`plugins.bundle.config`
|
|
412
|
+
注册、zh/en 词典、`true`/`false` 下拉显示原始值)。
|
|
381
413
|
- `schema-verify.mjs` 用注册表自己的校验器检查工具定义,并带有断言"两种非法 schema 形状仍会被拒"的对照组。
|
|
382
414
|
- `install-verify.mjs` 用本地回环 HTTP 服务器驱动安装链路:路由顺序、被提供字节的 SHA256、校验不匹配时
|
|
383
415
|
拒绝执行、匹配时真正执行、以及全部路径失败的诊断。
|
|
@@ -400,9 +432,21 @@ profile。无论生效的是哪个代理,loopback 地址一律绕过它。
|
|
|
400
432
|
——`curl.exe` 连 `127.0.0.1` 会超时,而 Node 宿主进程可以——所以它用 `example.com` 下的一个路径,网络不可用
|
|
401
433
|
时跳过。
|
|
402
434
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
435
|
+
### 测试前置条件
|
|
436
|
+
|
|
437
|
+
插件有一个可选的运行期依赖——[`@deepseek-ai/schemastery`](https://www.npmjs.com/package/@deepseek-ai/schemastery),
|
|
438
|
+
由动态 `Config` schema(注册设置命名空间的那份)使用。凡是 import 插件本体的套件(`config-verify`、
|
|
439
|
+
`schema-verify`、`empty-verify`、`report-verify`、`http-verify`)都需要它能从工作区解析,所以先跑
|
|
440
|
+
`npm install`。
|
|
441
|
+
|
|
442
|
+
`schema-verify` 额外需要从 npm 全局安装的 `@deepseek-ai/dsh` 里 import 注册表自己的校验器
|
|
443
|
+
(`@deepseek-ai/dsh-tools`);`report-verify` 与引擎类套件需要真实 Moli 引擎,`empty-verify` 需要
|
|
444
|
+
C 编译器来构建它的桩引擎。在 Windows 上,`@deepseek-ai/schemastery` 可以通过目录 junction 指到
|
|
445
|
+
npm 全局 DSH 树来解析(与套件解析 `dsh-tools` 的方式相同)。
|
|
446
|
+
|
|
447
|
+
`.github/workflows/verify.yml` 在 Linux、Windows、macOS 上跑 `schema-verify`、`empty-verify`、
|
|
448
|
+
`scroll-verify` 与 `http-verify`(无需引擎、无需网络),在 Linux 与 Windows 上尝试依赖引擎的几套;
|
|
449
|
+
若 runner 上没有引擎,则报告为跳过而不是失败。
|
|
406
450
|
|
|
407
451
|
`report-verify.mjs` **故意不放进 CI**:其中三项会抓 `example.com`,还有一项抓一个具体的论坛帖子,因此它
|
|
408
452
|
需要能上外网——如果这台机器的流量走代理,还需要那个代理可用。有网络时它是有用的,但**一个因为网站改版就变
|
package/client.js
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-moli — browser half.
|
|
3
|
+
*
|
|
4
|
+
* Settings section (Moli install & fetch): the plugin's configuration —
|
|
5
|
+
* engine lifecycle (install / uninstall), forward proxy, per-call timeout,
|
|
6
|
+
* inline truncation budget, the TLS no-verify switch, and the engine path —
|
|
7
|
+
* edited on the plugin's page in the Plugins manager.
|
|
8
|
+
*
|
|
9
|
+
* Hand-written ModuleLoader bundle — no build step required.
|
|
10
|
+
*/
|
|
11
|
+
window.__ModuleLoader__.load({
|
|
12
|
+
id: "dsh-moli",
|
|
13
|
+
factory: (require) => {
|
|
14
|
+
var module = { exports: {} };
|
|
15
|
+
var exports = module.exports;
|
|
16
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
17
|
+
var react = require("react");
|
|
18
|
+
var h = react.createElement;
|
|
19
|
+
|
|
20
|
+
// ── CSS (theme tokens, same vocabulary as dsh-soul-md) ────────────────
|
|
21
|
+
var CSS = ".__moli_root{max-width:640px;display:flex;flex-direction:column;gap:10px}" +
|
|
22
|
+
".__moli_field{display:flex;flex-direction:column;gap:4px}" +
|
|
23
|
+
".__moli_label{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:6px}" +
|
|
24
|
+
".__moli_hint{font-size:11px;color:var(--dsw-alias-label-tertiary)}" +
|
|
25
|
+
".__moli_input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:6px 10px;font-size:13px;box-sizing:border-box;width:100%}" +
|
|
26
|
+
".__moli_actions{display:flex;gap:8px;align-items:center;margin-top:4px}" +
|
|
27
|
+
".__moli_btn{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:8px;padding:6px 14px;font:inherit;font-size:13px;cursor:pointer}" +
|
|
28
|
+
".__moli_btn:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary)}" +
|
|
29
|
+
".__moli_btn:disabled{opacity:.5;cursor:default}" +
|
|
30
|
+
".__moli_btnPrimary{border-color:var(--dsw-alias-state-business-primary, #3964fe);background:var(--dsw-alias-state-business-primary, #3964fe);color:#fff}" +
|
|
31
|
+
".__moli_btnDanger{border-color:var(--dsw-alias-state-error-primary, #f85149);color:var(--dsw-alias-state-error-primary, #f85149)}" +
|
|
32
|
+
".__moli_status{font-size:12px;color:var(--dsw-alias-label-tertiary)}" +
|
|
33
|
+
".__moli_error{font-size:12px;color:var(--dsw-alias-state-error-primary)}" +
|
|
34
|
+
".__moli_unavailable{font-size:13px;color:var(--dsw-alias-label-tertiary)}" +
|
|
35
|
+
".__moli_group{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:10px 12px;display:flex;flex-direction:column;gap:8px}" +
|
|
36
|
+
".__moli_engineRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap}" +
|
|
37
|
+
".__moli_engineState{font-size:13px;color:var(--dsw-alias-label-primary);font-weight:600}" +
|
|
38
|
+
".__moli_engineDetail{font-size:11px;color:var(--dsw-alias-label-tertiary)}";
|
|
39
|
+
if (typeof document !== "undefined") {
|
|
40
|
+
var style = document.createElement("style");
|
|
41
|
+
style.textContent = CSS;
|
|
42
|
+
style.setAttribute("data-dsh-moli", "");
|
|
43
|
+
if (!document.head.querySelector("style[data-dsh-moli]")) document.head.appendChild(style);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── locale ────────────────────────────────────────────────────────────
|
|
47
|
+
var zh = {
|
|
48
|
+
nav: "Moli 设置",
|
|
49
|
+
sectionTitle: "Moli 安装与抓取",
|
|
50
|
+
unavailable: "配置不可用(可能当前 profile 未启用本插件)。",
|
|
51
|
+
loading: "正在读取配置…",
|
|
52
|
+
saved: "已保存。",
|
|
53
|
+
save: "保存",
|
|
54
|
+
error: "保存失败",
|
|
55
|
+
notApplied: "写入被宿主拒绝,表单已按宿主实际状态刷新。",
|
|
56
|
+
engineSection: "引擎",
|
|
57
|
+
engineInstalled: "已安装",
|
|
58
|
+
engineMissing: "未安装",
|
|
59
|
+
engineUnknown: "检测中…",
|
|
60
|
+
engineWorking: "正在处理…",
|
|
61
|
+
engineActionInstall: "安装引擎",
|
|
62
|
+
engineActionUninstall: "卸载引擎",
|
|
63
|
+
engineVersion: "版本 {version}",
|
|
64
|
+
engineInstallHint: "未检测到 Moli 引擎。点击「安装引擎」下载官方安装器并运行(需联网,可能耗时数分钟)。",
|
|
65
|
+
engineUninstallHint: "点击「卸载引擎」将删除本机安装的 Moli 二进制。",
|
|
66
|
+
fieldProxy: "转发代理",
|
|
67
|
+
hintProxy: '每次调用的代理覆盖项;留空表示直连。例如 "http://127.0.0.1:7890"。回环地址总是绕过代理。',
|
|
68
|
+
fieldDefaultTimeoutMs: "默认超时(毫秒)",
|
|
69
|
+
hintDefaultTimeoutMs: "单次调用默认超时,默认 30000(30 秒)。",
|
|
70
|
+
fieldMaxChars: "内联文本截断(字符)",
|
|
71
|
+
hintMaxChars: "内联文本最多返回的字符数,默认 200000;超出时保留头尾并注明省略量,传 writeTo 可保留全文。",
|
|
72
|
+
fieldInsecureTls: "跳过 TLS 验证(insecureTls)",
|
|
73
|
+
hintInsecureTls: '本网络证书吊销应答不可达时,GitHub 等 HTTPS 主机可能报错;开启后透传 moli 的 no-verify 标志。默认开启。',
|
|
74
|
+
fieldExePath: "引擎路径(exePath)",
|
|
75
|
+
hintExePath: "可选。指定 moli 可执行文件的绝对路径;留空则自动在常见安装位置查找。",
|
|
76
|
+
placeholderProxy: "http://127.0.0.1:7890",
|
|
77
|
+
placeholderExePath: "/home/user/.local/bin/moli",
|
|
78
|
+
};
|
|
79
|
+
var en = {
|
|
80
|
+
nav: "Moli settings",
|
|
81
|
+
sectionTitle: "Moli install & fetch",
|
|
82
|
+
unavailable: "Configuration unavailable (this profile may not enable the plugin).",
|
|
83
|
+
loading: "Loading configuration…",
|
|
84
|
+
saved: "Saved.",
|
|
85
|
+
save: "Save",
|
|
86
|
+
error: "Save failed",
|
|
87
|
+
notApplied: "The write was refused by the host; the form was reloaded to what the host actually holds.",
|
|
88
|
+
engineSection: "Engine",
|
|
89
|
+
engineInstalled: "Installed",
|
|
90
|
+
engineMissing: "Not installed",
|
|
91
|
+
engineUnknown: "Detecting…",
|
|
92
|
+
engineWorking: "Working…",
|
|
93
|
+
engineActionInstall: "Install engine",
|
|
94
|
+
engineActionUninstall: "Uninstall engine",
|
|
95
|
+
engineVersion: "version {version}",
|
|
96
|
+
engineInstallHint: "No Moli engine found. Click “Install engine” to download and run the official installer (needs network; may take a few minutes).",
|
|
97
|
+
engineUninstallHint: "Click “Uninstall engine” to remove the locally installed Moli binary.",
|
|
98
|
+
fieldProxy: "Forward proxy",
|
|
99
|
+
hintProxy: 'Per-call proxy override; leave empty for direct. E.g. "http://127.0.0.1:7890". Loopback URLs always bypass it.',
|
|
100
|
+
fieldDefaultTimeoutMs: "Default timeout (ms)",
|
|
101
|
+
hintDefaultTimeoutMs: "Per-call default timeout; 30000 (30 s) by default.",
|
|
102
|
+
fieldMaxChars: "Inline truncation budget (chars)",
|
|
103
|
+
hintMaxChars: "Max characters returned inline; 200000 by default. Beyond it the head and tail are kept with an omitted-count note; pass writeTo to keep everything.",
|
|
104
|
+
fieldInsecureTls: "Skip TLS verification (insecureTls)",
|
|
105
|
+
hintInsecureTls: "When this network's CRL/OCSP responder is unreachable, HTTPS hosts like GitHub fail; enabling passes moli's no-verify flag. On by default.",
|
|
106
|
+
fieldExePath: "Engine path (exePath)",
|
|
107
|
+
hintExePath: "Optional. Absolute path to the moli executable; leave empty to search the usual install locations.",
|
|
108
|
+
placeholderProxy: "http://127.0.0.1:7890",
|
|
109
|
+
placeholderExePath: "/home/user/.local/bin/moli",
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// ── write helper (same contract as dsh-soul-md) ───────────────────────
|
|
113
|
+
function settingsOpsApplied(snapshot, ops) {
|
|
114
|
+
var v = snapshot && snapshot.status === "ready" ? snapshot.value : void 0;
|
|
115
|
+
if (v === void 0) return false;
|
|
116
|
+
return ops.every(function (op) {
|
|
117
|
+
if (op.op === "unset") return v[op.path[0]] === void 0;
|
|
118
|
+
return v[op.path[0]] === op.value;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
function commitSettingsOps(scope, ops) {
|
|
122
|
+
if (ops.length === 0) return Promise.resolve(true);
|
|
123
|
+
var run;
|
|
124
|
+
if (typeof scope.mutate === "function") {
|
|
125
|
+
run = scope.mutate(ops, scope.getSnapshot().revision);
|
|
126
|
+
} else {
|
|
127
|
+
run = ops.reduce(function (chain, op) {
|
|
128
|
+
return chain.then(function () {
|
|
129
|
+
return op.op === "unset" ? scope.unset(op.path[0]) : scope.set(op.path[0], op.value);
|
|
130
|
+
});
|
|
131
|
+
}, Promise.resolve());
|
|
132
|
+
}
|
|
133
|
+
return Promise.resolve(run).then(function () {
|
|
134
|
+
return settingsOpsApplied(scope.getSnapshot(), ops);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── field descriptors ─────────────────────────────────────────────────
|
|
139
|
+
// `select` fields store booleans (single option pair); the draft holds the
|
|
140
|
+
// string option and the ops map it back to a boolean.
|
|
141
|
+
var FIELDS = [
|
|
142
|
+
{ key: "proxy", label: "fieldProxy", type: "text", hint: "hintProxy", placeholder: "placeholderProxy" },
|
|
143
|
+
{ key: "defaultTimeoutMs", label: "fieldDefaultTimeoutMs", type: "number", min: 1000, hint: "hintDefaultTimeoutMs" },
|
|
144
|
+
{ key: "maxChars", label: "fieldMaxChars", type: "number", min: 100, hint: "hintMaxChars" },
|
|
145
|
+
{ key: "insecureTls", label: "fieldInsecureTls", type: "select", options: [["true", "true"], ["false", "false"]], hint: "hintInsecureTls" },
|
|
146
|
+
{ key: "exePath", label: "fieldExePath", type: "text", hint: "hintExePath", placeholder: "placeholderExePath" },
|
|
147
|
+
];
|
|
148
|
+
function toDraft(value) {
|
|
149
|
+
var out = {};
|
|
150
|
+
for (var i = 0; i < FIELDS.length; i++) {
|
|
151
|
+
var f = FIELDS[i];
|
|
152
|
+
var v = value && value[f.key];
|
|
153
|
+
if (f.type === "number") out[f.key] = typeof v === "number" ? String(v) : "";
|
|
154
|
+
else if (f.type === "select") out[f.key] = v ? "true" : "false";
|
|
155
|
+
else out[f.key] = typeof v === "string" ? v : "";
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
function draftOps(draft) {
|
|
160
|
+
var ops = [];
|
|
161
|
+
for (var i = 0; i < FIELDS.length; i++) {
|
|
162
|
+
var f = FIELDS[i];
|
|
163
|
+
if (f.type === "number") {
|
|
164
|
+
var n = Number(draft[f.key]);
|
|
165
|
+
if (draft[f.key] !== "" && Number.isFinite(n)) ops.push({ op: "set", path: [f.key], value: n });
|
|
166
|
+
} else if (f.type === "select") {
|
|
167
|
+
ops.push({ op: "set", path: [f.key], value: draft[f.key] === "true" });
|
|
168
|
+
} else {
|
|
169
|
+
ops.push({ op: "set", path: [f.key], value: typeof draft[f.key] === "string" ? draft[f.key] : "" });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return ops;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── settings section ──────────────────────────────────────────────────
|
|
176
|
+
function MoliConfigSection(props) {
|
|
177
|
+
useLocale(props.locale);
|
|
178
|
+
var t = props.t;
|
|
179
|
+
var scope = props.scope;
|
|
180
|
+
var [snapshot, setSnapshot] = react.useState(function () { return scope.getSnapshot(); });
|
|
181
|
+
var ready = snapshot.status === "ready" && snapshot.value !== void 0;
|
|
182
|
+
var [draft, setDraft] = react.useState(null);
|
|
183
|
+
var [busy, setBusy] = react.useState(false);
|
|
184
|
+
var [notice, setNotice] = react.useState(null);
|
|
185
|
+
var [error, setError] = react.useState(null);
|
|
186
|
+
var writePending = react.useRef(false);
|
|
187
|
+
|
|
188
|
+
react.useEffect(function () {
|
|
189
|
+
var alive = true;
|
|
190
|
+
var sync = function () { if (alive) setSnapshot(scope.getSnapshot()); };
|
|
191
|
+
var un = typeof scope.subscribe === "function" ? scope.subscribe(sync) : null;
|
|
192
|
+
// Bound once per plugin, shared across mounts: unmount only unsubscribes,
|
|
193
|
+
// never disposes, so a later remount does not find a dead write queue.
|
|
194
|
+
return function () { alive = false; if (un) un(); };
|
|
195
|
+
}, [scope]);
|
|
196
|
+
|
|
197
|
+
if (snapshot.status === "unavailable") {
|
|
198
|
+
return h("p", { className: "__moli_unavailable" }, t("unavailable"));
|
|
199
|
+
}
|
|
200
|
+
if (!ready) return h("p", { className: "__moli_status" }, t("loading"));
|
|
201
|
+
|
|
202
|
+
var value = snapshot.value;
|
|
203
|
+
var currentDraft = draft === null ? toDraft(value) : draft;
|
|
204
|
+
var valueKey = JSON.stringify(toDraft(value));
|
|
205
|
+
var draftKey = JSON.stringify(currentDraft);
|
|
206
|
+
var dirty = valueKey !== draftKey;
|
|
207
|
+
var saving = busy;
|
|
208
|
+
|
|
209
|
+
function setField(f, v) { setNotice(null); setError(null); setDraft(function (p) { return Object.assign({}, p, { [f.key]: v }); }); }
|
|
210
|
+
|
|
211
|
+
function onSave() {
|
|
212
|
+
if (writePending.current) return;
|
|
213
|
+
writePending.current = true;
|
|
214
|
+
setBusy(true); setNotice(null); setError(null);
|
|
215
|
+
commitSettingsOps(scope, draftOps(currentDraft)).then(function (ok) {
|
|
216
|
+
writePending.current = false;
|
|
217
|
+
setBusy(false);
|
|
218
|
+
setSnapshot(scope.getSnapshot());
|
|
219
|
+
if (!ok) { setError(t("notApplied")); return; }
|
|
220
|
+
setNotice(t("saved"));
|
|
221
|
+
setDraft(null);
|
|
222
|
+
}).catch(function (e) {
|
|
223
|
+
writePending.current = false;
|
|
224
|
+
setBusy(false);
|
|
225
|
+
setSnapshot(scope.getSnapshot());
|
|
226
|
+
setError(String(e && e.message || e));
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Engine lifecycle: the Host reacts to `engineAction` and reports back
|
|
231
|
+
// through `engineState` / `engineVersion`. While the action is pending the
|
|
232
|
+
// form shows "working…"; scope updates (including the reset to `none`)
|
|
233
|
+
// drive the display back to the resulting state.
|
|
234
|
+
var engineState = typeof value.engineState === "string" ? value.engineState : "unknown";
|
|
235
|
+
var engineVersion = typeof value.engineVersion === "string" ? value.engineVersion : "";
|
|
236
|
+
var engineAction = typeof value.engineAction === "string" ? value.engineAction : "none";
|
|
237
|
+
var engineWorking = engineAction !== "none";
|
|
238
|
+
var engineLabel = engineWorking ? t("engineWorking")
|
|
239
|
+
: engineState === "installed" ? t("engineInstalled")
|
|
240
|
+
: engineState === "missing" ? t("engineMissing")
|
|
241
|
+
: t("engineUnknown");
|
|
242
|
+
|
|
243
|
+
function onEngineAction(action) {
|
|
244
|
+
if (engineWorking) return;
|
|
245
|
+
if (action === "uninstall" && !window.confirm(t("engineUninstallHint"))) return;
|
|
246
|
+
setNotice(null); setError(null);
|
|
247
|
+
commitSettingsOps(scope, [{ op: "set", path: ["engineAction"], value: action }]).then(function (ok) {
|
|
248
|
+
setSnapshot(scope.getSnapshot());
|
|
249
|
+
if (!ok) setError(t("notApplied"));
|
|
250
|
+
}).catch(function (e) {
|
|
251
|
+
setSnapshot(scope.getSnapshot());
|
|
252
|
+
setError(String(e && e.message || e));
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return h("div", { className: "__moli_root" },
|
|
257
|
+
// Engine lifecycle group.
|
|
258
|
+
h("div", { className: "__moli_group" },
|
|
259
|
+
h("div", { className: "__moli_label" }, t("engineSection")),
|
|
260
|
+
h("div", { className: "__moli_engineRow" },
|
|
261
|
+
h("span", { className: "__moli_engineState" }, engineLabel),
|
|
262
|
+
engineVersion && engineState === "installed"
|
|
263
|
+
? h("span", { className: "__moli_engineDetail" }, t("engineVersion").replace("{version}", engineVersion))
|
|
264
|
+
: null,
|
|
265
|
+
),
|
|
266
|
+
engineState !== "installed" && !engineWorking
|
|
267
|
+
? h("span", { className: "__moli_engineDetail" }, t("engineInstallHint"))
|
|
268
|
+
: engineState === "installed" && !engineWorking
|
|
269
|
+
? h("span", { className: "__moli_engineDetail" }, t("engineUninstallHint"))
|
|
270
|
+
: null,
|
|
271
|
+
h("div", { className: "__moli_actions" },
|
|
272
|
+
engineState === "installed"
|
|
273
|
+
? h("button", { className: "__moli_btn __moli_btnDanger", disabled: engineWorking, onClick: function () { onEngineAction("uninstall"); } }, t("engineActionUninstall"))
|
|
274
|
+
: h("button", { className: "__moli_btn __moli_btnPrimary", disabled: engineWorking, onClick: function () { onEngineAction("install"); } }, t("engineActionInstall")),
|
|
275
|
+
),
|
|
276
|
+
),
|
|
277
|
+
// Configuration fields group.
|
|
278
|
+
h("div", { className: "__moli_group" },
|
|
279
|
+
FIELDS.map(function (f) {
|
|
280
|
+
if (f.type === "select") {
|
|
281
|
+
return h("label", { key: f.key, className: "__moli_field" },
|
|
282
|
+
h("span", { className: "__moli_label" }, t(f.label)),
|
|
283
|
+
h("select", {
|
|
284
|
+
className: "__moli_input",
|
|
285
|
+
value: currentDraft[f.key],
|
|
286
|
+
onChange: function (e) { setField(f, e.target.value); },
|
|
287
|
+
}, f.options.map(function (o) {
|
|
288
|
+
// Option labels are the raw stored values ("true"/"false"),
|
|
289
|
+
// never translated; only the field label and hint are locale-bound.
|
|
290
|
+
return h("option", { key: o[0], value: o[0] }, o[1]);
|
|
291
|
+
})),
|
|
292
|
+
f.hint ? h("span", { className: "__moli_hint" }, t(f.hint)) : null,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
return h("label", { key: f.key, className: "__moli_field" },
|
|
296
|
+
h("span", { className: "__moli_label" }, t(f.label)),
|
|
297
|
+
h("input", {
|
|
298
|
+
className: "__moli_input",
|
|
299
|
+
type: f.type === "number" ? "number" : "text",
|
|
300
|
+
min: f.min,
|
|
301
|
+
value: currentDraft[f.key],
|
|
302
|
+
placeholder: f.placeholder ? t(f.placeholder) : void 0,
|
|
303
|
+
onChange: function (e) { setField(f, e.target.value); },
|
|
304
|
+
}),
|
|
305
|
+
f.hint ? h("span", { className: "__moli_hint" }, t(f.hint)) : null,
|
|
306
|
+
);
|
|
307
|
+
}),
|
|
308
|
+
h("div", { className: "__moli_actions" },
|
|
309
|
+
h("button", { className: "__moli_btn __moli_btnPrimary", disabled: saving || !dirty, onClick: onSave }, t("save")),
|
|
310
|
+
notice ? h("span", { className: "__moli_status" }, notice) : null,
|
|
311
|
+
error ? h("span", { className: "__moli_error" }, error) : null,
|
|
312
|
+
),
|
|
313
|
+
),
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
var NS = "dsh-moli";
|
|
318
|
+
|
|
319
|
+
// Keep the section re-rendering when the active locale changes.
|
|
320
|
+
function useLocale(locale) {
|
|
321
|
+
var refresh = react.useState(0)[1];
|
|
322
|
+
react.useEffect(function () {
|
|
323
|
+
if (!locale || typeof locale.subscribe !== "function") return;
|
|
324
|
+
return locale.subscribe(function () { refresh(function (revision) { return revision + 1; }); });
|
|
325
|
+
}, [locale]);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function apply(ctx) {
|
|
329
|
+
var t = ctx.locale.bind(NS);
|
|
330
|
+
ctx.effect(function () { return ctx.locale.register(NS, { zh: zh, en: en }); }, "dsh-moli: dictionaries");
|
|
331
|
+
var scope = ctx.configForms.get("dsh-moli");
|
|
332
|
+
ctx.slots.inject("plugins.bundle.config", function () {
|
|
333
|
+
return ctx.slots.register({
|
|
334
|
+
name: "plugins.bundle.config",
|
|
335
|
+
key: "dsh-moli",
|
|
336
|
+
locale: NS
|
|
337
|
+
}, function (props) {
|
|
338
|
+
return h(MoliConfigSection, Object.assign({}, props, { scope: scope, t: t, locale: ctx.locale }));
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
exports.apply = apply;
|
|
344
|
+
exports.inject = ["slots", "locale", "configForms"];
|
|
345
|
+
return module.exports;
|
|
346
|
+
}
|
|
347
|
+
});
|
package/index.js
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
/** Host half of dsh-moli: expose the local `moli` headless browser as callable tools. */
|
|
2
|
-
import { MANUAL_INSTALL, installMoli } from './lib/install.js';
|
|
3
|
-
import { findMoli, moliCandidates, probe, runFetch } from './lib/moli.js';
|
|
2
|
+
import { MANUAL_INSTALL, installMoli, uninstallMoli } from './lib/install.js';
|
|
3
|
+
import { findMoli, moliCandidates, moliVersion, probe, runFetch } from './lib/moli.js';
|
|
4
4
|
|
|
5
5
|
export const name = 'dsh-moli';
|
|
6
6
|
export const inject = ['tools'];
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* dependency stays a symlink target), so a bare specifier
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
9
|
+
* `@deepseek-ai/schemastery` powers the live Config schema and therefore the
|
|
10
|
+
* settings namespace. A profile plugin is loaded from its real path on disk (a
|
|
11
|
+
* `link:` dependency stays a symlink target), so a bare specifier must resolve
|
|
12
|
+
* against the plugin's own `node_modules` chain — which does not exist when the
|
|
13
|
+
* plugin is installed as a `link:`/junction (the README's first install route).
|
|
14
|
+
* The import is therefore optional: without schemastery the settings form does
|
|
15
|
+
* not appear and the plugin degrades to plain patch-layer configuration, while
|
|
16
|
+
* `moli_fetch` / `moli_probe` keep registering — those are the plugin's reason
|
|
17
|
+
* to exist, and losing them would be worse than losing the form.
|
|
16
18
|
*/
|
|
19
|
+
let z;
|
|
20
|
+
try {
|
|
21
|
+
z = (await import('@deepseek-ai/schemastery')).default;
|
|
22
|
+
} catch {
|
|
23
|
+
z = undefined;
|
|
24
|
+
}
|
|
17
25
|
|
|
18
26
|
/** Deployment configuration defaults; override via the plugin's `config` entry. */
|
|
19
27
|
const DEFAULTS = {
|
|
@@ -27,6 +35,35 @@ const DEFAULTS = {
|
|
|
27
35
|
insecureTls: true,
|
|
28
36
|
};
|
|
29
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Live Config schema. Declaring it (and exporting it) makes DSH register the
|
|
40
|
+
* `dsh-moli` settings namespace, which the browser half's `plugins.bundle.config`
|
|
41
|
+
* form edits. `.volatile()` marks the whole object live so the namespace appears
|
|
42
|
+
* in the settings describe mirror and the form's writes are applied via the
|
|
43
|
+
* ConfigEditor / volatile HMR path.
|
|
44
|
+
*
|
|
45
|
+
* `undefined` when `@deepseek-ai/schemastery` cannot be resolved (a `link:`
|
|
46
|
+
* install) — then no settings namespace is registered and the engine lifecycle
|
|
47
|
+
* is skipped, but the tools still register.
|
|
48
|
+
*/
|
|
49
|
+
export const Config = z
|
|
50
|
+
? z.object({
|
|
51
|
+
defaultTimeoutMs: z.number().default(DEFAULTS.defaultTimeoutMs),
|
|
52
|
+
maxChars: z.number().default(DEFAULTS.maxChars),
|
|
53
|
+
insecureTls: z.boolean().default(DEFAULTS.insecureTls),
|
|
54
|
+
proxy: z.string().default(''),
|
|
55
|
+
exePath: z.string().default(''),
|
|
56
|
+
// Engine lifecycle, driven by the settings form. The form writes an action
|
|
57
|
+
// (`install` / `uninstall`) and the Host executes it asynchronously, then
|
|
58
|
+
// reports the outcome back through `engineState` / `engineVersion` and resets
|
|
59
|
+
// `engineAction` to `none`. `engineState` is `installed`, `missing`, or a
|
|
60
|
+
// short error text.
|
|
61
|
+
engineAction: z.union([z.const('none'), z.const('install'), z.const('uninstall')]).default('none'),
|
|
62
|
+
engineState: z.string().default('unknown'),
|
|
63
|
+
engineVersion: z.string().default(''),
|
|
64
|
+
}).volatile()
|
|
65
|
+
: undefined;
|
|
66
|
+
|
|
30
67
|
/**
|
|
31
68
|
* Parameter schema of `moli_fetch`.
|
|
32
69
|
*
|
|
@@ -265,7 +302,10 @@ function renderProbe(_args, value) {
|
|
|
265
302
|
* @param config - deployment configuration for paths, timeout, and output size.
|
|
266
303
|
*/
|
|
267
304
|
export function apply(ctx, config) {
|
|
268
|
-
|
|
305
|
+
// The Config schema is declared volatile, so DSH hands it as a live reference
|
|
306
|
+
// whose `.get()` reads the current values; a deployment that mounts an older
|
|
307
|
+
// loader without volatile wiring passes a plain object instead. Read either.
|
|
308
|
+
const options = typeof config?.get === 'function' ? config.get() : (config ?? {});
|
|
269
309
|
// Mutable: a successful install resolves a path this process can use at once,
|
|
270
310
|
// without waiting for a restart to re-run discovery.
|
|
271
311
|
let cachedExe = findMoli(options.exePath);
|
|
@@ -384,5 +424,167 @@ export function apply(ctx, config) {
|
|
|
384
424
|
},
|
|
385
425
|
});
|
|
386
426
|
|
|
427
|
+
// ── engine lifecycle via the settings form ─────────────────────────────
|
|
428
|
+
// The browser half's `plugins.bundle.config` form drives install/uninstall
|
|
429
|
+
// through the `engineAction` field and reads the outcome from
|
|
430
|
+
// `engineState`/`engineVersion`. State is written back only when it changes,
|
|
431
|
+
// and a finished action always resets `engineAction` to `none`, so the
|
|
432
|
+
// resulting `settings/document-updated` re-entry lands in the plain sync
|
|
433
|
+
// branch instead of looping the action.
|
|
434
|
+
//
|
|
435
|
+
// Everything here depends on the settings namespace, which exists only when
|
|
436
|
+
// schemastery resolved (see the `Config` export): without it there is no form
|
|
437
|
+
// to drive the lifecycle, so the whole block is skipped and the plugin is
|
|
438
|
+
// pure tool registration.
|
|
439
|
+
if (Config) {
|
|
440
|
+
const NS = 'dsh-moli';
|
|
441
|
+
let engineBusy = false;
|
|
442
|
+
const currentConfig = () => (typeof config.get === 'function' ? config.get() : (config ?? {}));
|
|
443
|
+
// Write settings outside the current HMR transaction. `settings.update`
|
|
444
|
+
// routes through dsh-hmr's runExclusive, which rejects when it is called
|
|
445
|
+
// from inside an active transaction — the settings/document-updated event
|
|
446
|
+
// handler runs in one, so a direct update throws
|
|
447
|
+
// "HMR transactions cannot be nested" and the write-back is silently lost
|
|
448
|
+
// (the form then stays on "working…" forever). setImmediate defers the
|
|
449
|
+
// update until the event transaction has exited.
|
|
450
|
+
const writeSettings = (settings, patch) =>
|
|
451
|
+
new Promise((resolve, reject) => {
|
|
452
|
+
setImmediate(() => {
|
|
453
|
+
settings.update(NS, patch).then(resolve, reject);
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
const syncEngineState = async (settings, current) => {
|
|
457
|
+
const exe = findMoli(current.exePath);
|
|
458
|
+
const state = exe === undefined ? 'missing' : 'installed';
|
|
459
|
+
const version = exe === undefined ? '' : (moliVersion(exe) ?? '');
|
|
460
|
+
if (current.engineState === state && current.engineVersion === version) return;
|
|
461
|
+
try {
|
|
462
|
+
await writeSettings(settings, { engineState: state, engineVersion: version });
|
|
463
|
+
} catch (error) {
|
|
464
|
+
console.error(`[dsh-moli] engine-state sync failed: ${String(error?.message ?? error)}`);
|
|
465
|
+
ctx.logger?.warn?.('dsh-moli: engine-state sync failed', String(error?.message ?? error));
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
const runEngineAction = async (settings, current, action) => {
|
|
469
|
+
if (engineBusy) return;
|
|
470
|
+
engineBusy = true;
|
|
471
|
+
try {
|
|
472
|
+
if (action === 'install') {
|
|
473
|
+
const outcome = await installMoli({
|
|
474
|
+
exePath: current.exePath,
|
|
475
|
+
timeoutMs: 180000,
|
|
476
|
+
proxy: current.proxy,
|
|
477
|
+
locate: findMoli,
|
|
478
|
+
candidates: moliCandidates(current.exePath),
|
|
479
|
+
});
|
|
480
|
+
cachedExe = outcome.ok === true ? (outcome.exe ?? cachedExe) : cachedExe;
|
|
481
|
+
await writeSettings(settings, {
|
|
482
|
+
engineAction: 'none',
|
|
483
|
+
engineState: outcome.ok === true ? 'installed' : `install failed: ${String(outcome.error ?? 'unknown error').slice(0, 200)}`,
|
|
484
|
+
engineVersion: outcome.ok === true ? String(outcome.version ?? '') : '',
|
|
485
|
+
});
|
|
486
|
+
} else if (action === 'uninstall') {
|
|
487
|
+
const removed = uninstallMoli(current.exePath);
|
|
488
|
+
cachedExe = undefined;
|
|
489
|
+
await writeSettings(settings, {
|
|
490
|
+
engineAction: 'none',
|
|
491
|
+
engineState: removed === undefined ? 'uninstall failed: nothing to remove' : 'missing',
|
|
492
|
+
engineVersion: '',
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
} catch (error) {
|
|
496
|
+
// console (not ctx.logger) so the failure reaches the service journal
|
|
497
|
+
console.error(`[dsh-moli] engine action failed: ${String(error?.message ?? error)}`);
|
|
498
|
+
ctx.logger?.error?.('dsh-moli: engine action failed', String(error?.message ?? error));
|
|
499
|
+
try {
|
|
500
|
+
await writeSettings(settings, {
|
|
501
|
+
engineAction: 'none',
|
|
502
|
+
engineState: `action failed: ${String(error?.message ?? error).slice(0, 200)}`,
|
|
503
|
+
});
|
|
504
|
+
} catch (error2) {
|
|
505
|
+
console.error(`[dsh-moli] engine action failure report could not be written back: ${String(error2?.message ?? error2)}`);
|
|
506
|
+
ctx.logger?.error?.('dsh-moli: engine action failure report could not be written back', String(error2?.message ?? error2));
|
|
507
|
+
}
|
|
508
|
+
} finally {
|
|
509
|
+
engineBusy = false;
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
// Dispatch an engineAction if one is pending, otherwise sync state. Shared by
|
|
513
|
+
// the settings update handler and the boot-time initial sync, so an action
|
|
514
|
+
// whose `settings/document-updated` event was lost (a crash between the
|
|
515
|
+
// form's write and the handler) is still picked up on the next boot instead
|
|
516
|
+
// of leaving the form stuck on "processing".
|
|
517
|
+
// The event fires inside a dsh-hmr transaction. That transaction propagates
|
|
518
|
+
// through AsyncLocalStorage into anything the handler spawns — including
|
|
519
|
+
// setTimeout/setImmediate callbacks — so a deferred settings.update still
|
|
520
|
+
// nests and is rejected ("HMR transactions cannot be nested"). dsh-hmr
|
|
521
|
+
// itself escapes its own transaction with `executing.exit(...)` (watchConfig
|
|
522
|
+
// does this); running the dispatch through the same exit clears the ALS
|
|
523
|
+
// store so the write-back opens its own outermost transaction. The poller
|
|
524
|
+
// below stays as a defensive fallback (exit missing, or an event lost
|
|
525
|
+
// between the form's write and the handler).
|
|
526
|
+
const dispatchOutsideTransaction = (settings) => {
|
|
527
|
+
const hmr = ctx.get?.('hmr');
|
|
528
|
+
const run = () => {
|
|
529
|
+
const current = currentConfig();
|
|
530
|
+
const action = current.engineAction;
|
|
531
|
+
if (action === 'install' || action === 'uninstall') void runEngineAction(settings, current, action);
|
|
532
|
+
else void syncEngineState(settings, current);
|
|
533
|
+
};
|
|
534
|
+
if (hmr && typeof hmr.executing?.exit === 'function') {
|
|
535
|
+
hmr.executing.exit(run);
|
|
536
|
+
} else {
|
|
537
|
+
run();
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
// Actions are driven by an independent poller as a fallback, and by the
|
|
541
|
+
// transaction-escaped dispatch on the event path. A setInterval callback
|
|
542
|
+
// runs in the timer phase, outside any transaction, so the action and its
|
|
543
|
+
// write-back are safe there too.
|
|
544
|
+
let pollTimer = null;
|
|
545
|
+
let pendingCheck = false;
|
|
546
|
+
const pollCheck = () => {
|
|
547
|
+
const settings = ctx.get('settings');
|
|
548
|
+
if (!settings) return;
|
|
549
|
+
const current = currentConfig();
|
|
550
|
+
const action = current.engineAction;
|
|
551
|
+
if (action === 'install' || action === 'uninstall') {
|
|
552
|
+
void runEngineAction(settings, current, action);
|
|
553
|
+
} else {
|
|
554
|
+
void syncEngineState(settings, current);
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
ctx.on?.('settings/document-updated', (id) => {
|
|
558
|
+
if (id !== NS) return;
|
|
559
|
+
const settings = ctx.get('settings');
|
|
560
|
+
if (!settings) return;
|
|
561
|
+
pendingCheck = true;
|
|
562
|
+
dispatchOutsideTransaction(settings);
|
|
563
|
+
});
|
|
564
|
+
pollTimer = setInterval(() => {
|
|
565
|
+
if (pendingCheck) {
|
|
566
|
+
pendingCheck = false;
|
|
567
|
+
pollCheck();
|
|
568
|
+
}
|
|
569
|
+
}, 500);
|
|
570
|
+
pollTimer.unref?.();
|
|
571
|
+
// Initial state once settings is live. `ctx.get('settings')` may be null in a
|
|
572
|
+
// boot-time effect (the service mounts later), so retry briefly; the poller
|
|
573
|
+
// also picks up any pending engineAction written before the service settled.
|
|
574
|
+
let syncAttempts = 0;
|
|
575
|
+
const tryInitialSync = () => {
|
|
576
|
+
const settings = ctx.get('settings');
|
|
577
|
+
if (!settings) return false;
|
|
578
|
+
pollCheck();
|
|
579
|
+
return true;
|
|
580
|
+
};
|
|
581
|
+
if (!tryInitialSync()) {
|
|
582
|
+
const timer = setInterval(() => {
|
|
583
|
+
if (tryInitialSync() || ++syncAttempts >= 40) clearInterval(timer);
|
|
584
|
+
}, 250);
|
|
585
|
+
timer.unref?.();
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
387
589
|
ctx.logger?.info?.(`dsh-moli: registered moli_fetch and moli_probe (${cachedExe ?? 'moli missing'})`);
|
|
388
590
|
}
|
package/lib/install.js
CHANGED
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
*/
|
|
37
37
|
import { spawn, spawnSync } from 'node:child_process';
|
|
38
38
|
import { createHash } from 'node:crypto';
|
|
39
|
-
import { closeSync, existsSync, mkdtempSync, openSync, readFileSync, rmSync, statSync } from 'node:fs';
|
|
39
|
+
import { closeSync, existsSync, mkdtempSync, openSync, readFileSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
40
40
|
import { createConnection } from 'node:net';
|
|
41
41
|
import { tmpdir } from 'node:os';
|
|
42
|
-
import { join } from 'node:path';
|
|
42
|
+
import { dirname, join } from 'node:path';
|
|
43
43
|
import { findMoli } from './moli.js';
|
|
44
44
|
|
|
45
45
|
/** Official installer for the latest release. */
|
|
@@ -673,7 +673,7 @@ export async function installMoli(options = {}) {
|
|
|
673
673
|
return {
|
|
674
674
|
ok: false,
|
|
675
675
|
error: `installer checksum mismatch: expected ${expected}, got ${download.sha256}`,
|
|
676
|
-
|
|
676
|
+
installerSha256: download.sha256,
|
|
677
677
|
expectedSha256: expected,
|
|
678
678
|
method: download.method,
|
|
679
679
|
note: 'Refusing to execute the installer.',
|
|
@@ -759,3 +759,37 @@ export async function installMoli(options = {}) {
|
|
|
759
759
|
rmSync(workDir, { recursive: true, force: true });
|
|
760
760
|
}
|
|
761
761
|
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Remove an installed Moli engine. Windows deletes the installer-owned
|
|
765
|
+
* `%LOCALAPPDATA%\Moli` tree; POSIX removes the located binary and prunes the
|
|
766
|
+
* install directories it sits in when they end up empty.
|
|
767
|
+
*
|
|
768
|
+
* @param override - explicit path from configuration, when set.
|
|
769
|
+
* @returns the removed path, or undefined when nothing was installed.
|
|
770
|
+
*/
|
|
771
|
+
export function uninstallMoli(override) {
|
|
772
|
+
const exe = findMoli(override);
|
|
773
|
+
if (exe === undefined) return undefined;
|
|
774
|
+
if (process.platform === 'win32') {
|
|
775
|
+
const root = process.env.LOCALAPPDATA ? join(process.env.LOCALAPPDATA, 'Moli') : undefined;
|
|
776
|
+
if (root !== undefined && existsSync(root)) {
|
|
777
|
+
rmSync(root, { recursive: true, force: true });
|
|
778
|
+
return root;
|
|
779
|
+
}
|
|
780
|
+
// No installer-owned tree: nothing to remove. Returning here keeps the
|
|
781
|
+
// uninstall from falling through to the POSIX branch and deleting whatever
|
|
782
|
+
// findMoli located — which could be a user-provided path or a test fixture.
|
|
783
|
+
return undefined;
|
|
784
|
+
}
|
|
785
|
+
rmSync(exe, { force: true });
|
|
786
|
+
const dir = dirname(exe);
|
|
787
|
+
for (const parent of [dir, dirname(dir)]) {
|
|
788
|
+
try {
|
|
789
|
+
if (existsSync(parent) && readdirSync(parent).length === 0) rmSync(parent, { force: true });
|
|
790
|
+
} catch {
|
|
791
|
+
// A non-empty or protected parent is fine; the binary is already gone.
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return exe;
|
|
795
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-moli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DeepSeek Harness tools for the local Moli headless browser: rendered fetches, markdown, semantic trees, screenshots, and PDFs.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,14 +32,19 @@
|
|
|
32
32
|
"registry": "https://registry.npmjs.org",
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@deepseek-ai/schemastery": "^3.18.4"
|
|
37
|
+
},
|
|
35
38
|
"exports": {
|
|
36
39
|
".": "./index.js",
|
|
40
|
+
"./client": "./client.js",
|
|
37
41
|
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
38
42
|
"./package.json": "./package.json"
|
|
39
43
|
},
|
|
40
44
|
"files": [
|
|
41
45
|
"index.js",
|
|
42
46
|
"lib",
|
|
47
|
+
"client.js",
|
|
43
48
|
"cordis.patch.yml",
|
|
44
49
|
"README.md",
|
|
45
50
|
"README.zh.md",
|
|
@@ -48,6 +53,9 @@
|
|
|
48
53
|
"dsh": {
|
|
49
54
|
"bundle": {
|
|
50
55
|
"patch": "./cordis.patch.yml"
|
|
56
|
+
},
|
|
57
|
+
"client": {
|
|
58
|
+
"platform": "web"
|
|
51
59
|
}
|
|
52
60
|
}
|
|
53
61
|
}
|