@liustack/modlens 3.25.2 → 3.25.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/CHANGELOG.md +4 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/dist/main.js +30 -14
- package/docs/harness-setup.md +2 -2
- package/docs/harness-setup.zh-CN.md +2 -2
- package/docs/troubleshooting.md +2 -2
- package/docs/troubleshooting.zh-CN.md +2 -2
- package/package.json +1 -1
- package/skills/modlens/SKILL.md +4 -4
- package/skills/modlens/references/runtime.md +1 -1
- package/skills/modlens/scripts/run.ps1 +1 -1
- package/skills/modlens/scripts/run.sh +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.25.3 - 2026-08-30
|
|
4
|
+
|
|
5
|
+
- **API provider requests survive Node 26.0's built-in fetch (empty headers, still-gzipped body) ([#91](https://github.com/liustack/modlens/issues/91)).** Importing npm undici — which modlens resolves from node_modules since #23 — claims the process-wide `undici.globalDispatcher.2` slot at load time. Node 26.0.0's built-in fetch is undici 8.0.2 and reads that same slot, so on the no-proxy path (which used the host fetch) it drove npm undici 8.10.0's Agent. The two disagree about HTTP/2 response headers: 8.0.2's fetch walks `rawHeaders` as a flat array while 8.10.0's h2 client hands over an object, so the walk runs zero times and every header is dropped — `content-encoding` included. The gzip body was never decompressed and `response.json()` threw `Unexpected token '\x1f'` on the raw bytes. Gemini's endpoint negotiates HTTP/2, which is why `gemini-api` surfaced it; HTTP/1.1 headers are already an array and are immune, as are Node 24 (built-in undici 7.x on the `.1` slot, with h2 forced off by the compat wrapper) and Node 26.8+ (built-in 8.10.0 agrees with the npm copy). `apiFetch` now never touches the host fetch: with or without a proxy it uses npm undici's own fetch with a same-sourced dispatcher, verified by an integration test over the real undici stack and against a local HTTP/2 gzip server on Node 26.0.0. A body that fails mid-read after the response arrived keeps its status and headers readable and rejects `text()`/`json()` with the original error, so 401 classification and API key rotation are unchanged, and connect failures still raise the connectivity hint. Thanks to @Sol1l0quy for a report that had already isolated the failing layer with a fetch probe and byte-for-byte replays.
|
|
6
|
+
|
|
3
7
|
## 3.25.2 - 2026-08-28
|
|
4
8
|
|
|
5
9
|
- **GLM-5.3-Flash is recognized as native vision.** Z.ai released `glm-5.3-flash` on 2026-08-26 as the GLM-5 line's first natively multimodal model, and its name carries neither `v` nor `vision`. The reuse vision table matches the complete slug and delimited suffixes (`:free`, `-air`), so the bare slug, OpenRouter's `z-ai/glm-5.3-flash`, HuggingFace's `zai-org/GLM-5.3-Flash`, and `glm-5.3-flash:free` are judged image-capable, while a run-on name like `glm-5.3-flashlight` is not. The dsh wrapper's name gate uses the same boundary, so a catalog that copies the id without modalities does not mint a `(modlens vision)` twin and strip native sight. GLM-5.3 itself stays text-only. The guard example in `configure.md` drops the broad `glm-5.*` allow in favour of the known text spellings on both bare and namespaced forms (`glm-5.2*`, `*/glm-5.2*`, `glm-5.3`, `*/glm-5.3`). Vision denials are also namespaced: `glm-*v*` and `*/glm-*v*` carve `z-ai/glm-5.2v` and `z-ai/glm-5.2-vision` out of `*/glm-5.2*`, and flash denials use delimited patterns rather than a trailing `*`. Docs also note that GLM-5.3 and GLM-5.3-Flash cannot disable thinking.
|
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ Issues are welcome any time: [open one](https://github.com/liustack/modlens/issu
|
|
|
33
33
|
|
|
34
34
|
## Highlights
|
|
35
35
|
|
|
36
|
-
**🥇 The most capable vision plugin for DeepSeek Harness (dsh):** install it instantly with one command: `npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.25.
|
|
36
|
+
**🥇 The most capable vision plugin for DeepSeek Harness (dsh):** install it instantly with one command: `npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.25.3`. See the [setup guide](docs/harness-setup.md) for installation and update details. If the command line is not your thing but you still want to try DSH, check out <a href="https://github.com/liustack/aimanager"><b>AIManager</b></a>, the lightest desktop wrapper for DeepSeek Harness. It gets you started with zero code or configuration and installs every dependency for you with one click.
|
|
37
37
|
|
|
38
38
|
Pasting an image works two ways. **① Just paste.** On a text-only model the pasted image lands as a private temp file and its path enters the composer (the same interaction OpenCode and Pi ship), then the `modlens_read_image` tool takes it from there. **② Pick a `(modlens vision)` entry** in the model selector (it remembers your choice, so once is enough), then paste: the thumbnail stays visible in your message, closer to the Codex app feel, and the image is converted to structured evidence at request time, answered by the same underlying route. The plugin auto-discovers every provider route carrying eligible text-only DeepSeek, GLM, or MiMo Pro models and adds a wrapped entry per route. A stock install gets **`DeepSeek-V4-Flash (modlens vision)`** and **`DeepSeek-V4-Pro (modlens vision)`**, while extra routes like opencode-go or zai get their own. Native vision models in those families, including GLM-5.3-Flash, are excluded automatically. Which paste route applies is the host's per-model call: only a model its metadata positively confirms text-only is taken over, anything unconfirmed is left alone, so vision models keep their native paste ([details](docs/harness-setup.md)).
|
|
39
39
|
|
package/README.zh-CN.md
CHANGED
|
@@ -33,7 +33,7 @@ DeepSeek 的主力对话模型和 GLM-5.3 本体仍是纯文本,无法读图
|
|
|
33
33
|
|
|
34
34
|
## 亮点
|
|
35
35
|
|
|
36
|
-
**🥇 全网最强的 DeepSeek Harness(dsh)外挂视觉识别插件:**一条命令即刻安装 `npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.25.
|
|
36
|
+
**🥇 全网最强的 DeepSeek Harness(dsh)外挂视觉识别插件:**一条命令即刻安装 `npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.25.3`。更多安装与更新细节参考 [配置手册](docs/harness-setup.zh-CN.md) 。如果用不惯命令行,也想想玩玩 DSH,推荐食用全网最轻量级的 DeepSeek Harness 桌面版封装 <a href="https://github.com/liustack/aimanager"><b> AIManager</b></a>,零代码零配置起手,一键帮你安装所有依赖环境。
|
|
37
37
|
|
|
38
38
|
DeepSeek Harness 粘贴识图有两种玩法。
|
|
39
39
|
|
|
@@ -71,7 +71,7 @@ agy # 浏览器完成
|
|
|
71
71
|
**DeepSeek Harness(dsh)用户不走 skill 流程**,本包就是原生 dsh 插件:
|
|
72
72
|
|
|
73
73
|
```sh
|
|
74
|
-
npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.25.
|
|
74
|
+
npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.25.3
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
装完即有 `modlens_read_image` 工具,选「(modlens vision)」模型变体即可直接粘贴识图。引擎配置同样在 `~/.modlens`,详见[宿主接入](docs/harness-setup.zh-CN.md)。
|
package/dist/main.js
CHANGED
|
@@ -4,7 +4,7 @@ import * as fs from "fs";
|
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import * as os from "os";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
|
-
import { fetch
|
|
7
|
+
import { fetch, Agent, ProxyAgent, EnvHttpProxyAgent } from "undici";
|
|
8
8
|
import * as dns from "dns/promises";
|
|
9
9
|
import { isIP } from "net";
|
|
10
10
|
import { execFileSync, spawn } from "child_process";
|
|
@@ -346,7 +346,7 @@ async function fetchRemoteImageBase64(url, timeoutMs) {
|
|
|
346
346
|
const pinned = await assertSafeRemoteTarget(current);
|
|
347
347
|
const dispatcher = pinnedDispatcher(pinned);
|
|
348
348
|
dispatchers.push(dispatcher);
|
|
349
|
-
const response = await fetch
|
|
349
|
+
const response = await fetch(current, {
|
|
350
350
|
method: "GET",
|
|
351
351
|
redirect: "manual",
|
|
352
352
|
signal,
|
|
@@ -475,13 +475,13 @@ function connectFailureHint(error, url) {
|
|
|
475
475
|
return `Could not connect to ${host} (${cause.code}). The request never reached the network. If this machine reaches the internet through a proxy, set HTTPS_PROXY/HTTP_PROXY, or run: modlens config set proxy <url>`;
|
|
476
476
|
}
|
|
477
477
|
async function apiFetch(url, init, proxy, env = process.env) {
|
|
478
|
-
const dispatcher = apiProxyDispatcher(proxy, env);
|
|
478
|
+
const dispatcher = apiProxyDispatcher(proxy, env) ?? new Agent();
|
|
479
479
|
try {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
480
|
+
const response = await fetch(url, {
|
|
481
|
+
...init,
|
|
482
|
+
dispatcher
|
|
483
|
+
});
|
|
484
|
+
try {
|
|
485
485
|
const buffered = Buffer.from(await response.arrayBuffer());
|
|
486
486
|
await dispatcher.close();
|
|
487
487
|
return new Response(buffered, {
|
|
@@ -489,17 +489,33 @@ async function apiFetch(url, init, proxy, env = process.env) {
|
|
|
489
489
|
statusText: response.statusText,
|
|
490
490
|
headers: response.headers
|
|
491
491
|
});
|
|
492
|
-
}
|
|
493
|
-
return await fetch(url, init);
|
|
494
|
-
} catch (error) {
|
|
495
|
-
if (dispatcher) {
|
|
492
|
+
} catch (bodyError) {
|
|
496
493
|
await dispatcher.close().catch(() => {
|
|
497
494
|
});
|
|
495
|
+
return bodyFailedResponse(response, bodyError);
|
|
498
496
|
}
|
|
497
|
+
} catch (error) {
|
|
498
|
+
await dispatcher.close().catch(() => {
|
|
499
|
+
});
|
|
499
500
|
const hint = connectFailureHint(error, url);
|
|
500
501
|
throw hint ? new Error(hint, { cause: error }) : error;
|
|
501
502
|
}
|
|
502
503
|
}
|
|
504
|
+
function bodyFailedResponse(response, error) {
|
|
505
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
506
|
+
return new Response(
|
|
507
|
+
new ReadableStream({
|
|
508
|
+
start(controller) {
|
|
509
|
+
controller.error(cause);
|
|
510
|
+
}
|
|
511
|
+
}),
|
|
512
|
+
{
|
|
513
|
+
status: response.status,
|
|
514
|
+
statusText: response.statusText,
|
|
515
|
+
headers: response.headers
|
|
516
|
+
}
|
|
517
|
+
);
|
|
518
|
+
}
|
|
503
519
|
const JSON_TEMPLATE_INSTRUCTION = `Respond with ONE JSON object only, no markdown fences, no commentary. Fill this exact structure with your findings from the image (do not repeat this template literally, replace every value):
|
|
504
520
|
{"summary":"one paragraph describing the image","ocr":{"full_text":"all visible text","lines":[{"text":"one line","language":"en"}]},"layout":{"regions":[{"type":"a short kind, e.g. title, heading, paragraph, list, table, chart, form, code, image, icon, link, nav, button, search, or any other short label that fits better","reading_order":1,"text":"region text"}]},"semantics":{"scene":"what kind of scene","intent":"what the image is for","entities":[{"name":"entity","type":"kind","evidence":"where seen"}],"relations":[{"subject":"a","predicate":"relates to","object":"b"}]},"visual":{"dominant_colors":["color"],"style":"visual style","notes":["notable visual detail"]},"uncertainty":["anything unreadable or ambiguous"]}`;
|
|
505
521
|
function buildVisionPrompt(options) {
|
|
@@ -5541,7 +5557,7 @@ function parsePositiveInt(raw, flag) {
|
|
|
5541
5557
|
}
|
|
5542
5558
|
return Number.parseInt(raw, 10);
|
|
5543
5559
|
}
|
|
5544
|
-
program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.25.
|
|
5560
|
+
program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.25.3");
|
|
5545
5561
|
program.command("analyze", { isDefault: true }).description("Analyze an image into structured JSON evidence (default command)").requiredOption("-i, --input <path|url>", "Input image path or https URL").option("-o, --output <path>", "Write result JSON to a file").option("-m, --model <name>", "Provider model name").option("-p, --provider <name>", `Vision provider (${listProviders().join(", ")})`).option("--prompt <text>", "Extra focus for this image").option("--timeout <ms>", "Provider timeout in milliseconds", "180000").option("--provider-bin <path>", "Provider binary path (default: agy)").option("--workdir <path>", "Working directory for the provider").option(
|
|
5546
5562
|
"--extra-body <json>",
|
|
5547
5563
|
`JSON merged into the API request body, e.g. '{"thinking":{"type":"disabled"}}'`
|
|
@@ -5652,7 +5668,7 @@ program.command("doctor").description(
|
|
|
5652
5668
|
configPath: CONFIG_PATH,
|
|
5653
5669
|
// Lets doctor name an installed skill copy that is older than
|
|
5654
5670
|
// the CLI reporting on it (issue #33).
|
|
5655
|
-
version: "3.25.
|
|
5671
|
+
version: "3.25.3"
|
|
5656
5672
|
});
|
|
5657
5673
|
const output = options.json ? JSON.stringify(report, null, 2) : renderDoctorReport(report);
|
|
5658
5674
|
process.stdout.write(`${output}
|
package/docs/harness-setup.md
CHANGED
|
@@ -55,7 +55,7 @@ OpenCode with DeepSeek: `opencode auth login`, pick DeepSeek and paste the key (
|
|
|
55
55
|
dsh is different from the other harnesses: modlens plugs in as a native tool, not a prompt-triggered skill. The package itself is a dsh bundle, so one command installs it into a profile:
|
|
56
56
|
|
|
57
57
|
```sh
|
|
58
|
-
npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.25.
|
|
58
|
+
npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.25.3
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
This registers a `modlens_read_image` tool whose schema reaches the model on every request (no trigger heuristics), runs the modlens CLI shipped inside the same package, and returns the structured evidence as the tool's canonical JSON output. Engines, reuse grants, and guard rules stay in `~/.modlens/config.json`, shared with every other harness. dsh is in developer preview and its plugin surface may change; the plugin keeps its touch small (raw tool registration, the llm adapter surface for the vision variants, the attachment reader, and one agent pre-step hook) and degrades loudly if any of them moves.
|
|
@@ -87,7 +87,7 @@ modlens ships often, and both install shapes freeze at whatever version they
|
|
|
87
87
|
got. On dsh, re-run the install with the version named:
|
|
88
88
|
|
|
89
89
|
```sh
|
|
90
|
-
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.25.
|
|
90
|
+
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.25.3
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
`npm view @liustack/modlens version` prints the current one, and this page is
|
|
@@ -55,7 +55,7 @@ OpenCode 接 DeepSeek:执行 `opencode auth login`,选择 DeepSeek 并粘贴
|
|
|
55
55
|
dsh 与其他 harness 不同:modlens 以原生工具的形式接入,而不是靠提示词触发的 skill。本包自身就是一个 dsh bundle,一条命令即可装进某个 profile:
|
|
56
56
|
|
|
57
57
|
```sh
|
|
58
|
-
npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.25.
|
|
58
|
+
npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.25.3
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
这会注册一个 `modlens_read_image` 工具,它的 schema 随每次请求抵达模型(不靠触发启发式),运行同一个包里自带的 modlens CLI,并把结构化证据作为工具的标准 JSON 输出返回。引擎、复用授权和 guard 规则仍在 `~/.modlens/config.json` 里,与其他所有 harness 共享。dsh 还在开发者预览阶段,插件接口可能变化。这个插件刻意保持很小的接触面(原生工具注册、视觉变体所用的 llm 适配层、附件读取器,以及一个 agent 执行前钩子),其中任何一处变动,它都会大声报错而不是无声退化。
|
|
@@ -73,7 +73,7 @@ dsh 的网页用户面前没有终端,所以引擎设置有一张卡片,在*
|
|
|
73
73
|
modlens 发布很频繁,而两种安装形态都会冻结在装进来的那个版本上。dsh 上重跑一遍安装即可,版本号要点名:
|
|
74
74
|
|
|
75
75
|
```sh
|
|
76
|
-
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.25.
|
|
76
|
+
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.25.3
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
`npm view @liustack/modlens version` 可以查到当前版本号,本页的版本号则由发布流程自动写入。
|
package/docs/troubleshooting.md
CHANGED
|
@@ -163,7 +163,7 @@ simply lands on an older one. Name the exact version instead, which pnpm treats
|
|
|
163
163
|
as a deliberate request rather than a resolution:
|
|
164
164
|
|
|
165
165
|
```sh
|
|
166
|
-
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.25.
|
|
166
|
+
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.25.3
|
|
167
167
|
```
|
|
168
168
|
|
|
169
169
|
`npm view @liustack/modlens version` prints the current one. pnpm 11 installs a named
|
|
@@ -178,7 +178,7 @@ file:
|
|
|
178
178
|
|
|
179
179
|
```yaml
|
|
180
180
|
minimumReleaseAgeExclude:
|
|
181
|
-
- '@liustack/modlens@3.25.
|
|
181
|
+
- '@liustack/modlens@3.25.3'
|
|
182
182
|
```
|
|
183
183
|
|
|
184
184
|
Or lift the gate for a single command, which lifts it for everything that
|
|
@@ -144,7 +144,7 @@ dsh profile 装到的是旧版 modlens。`dsh.bundle` 声明从 3.9.0 起才存
|
|
|
144
144
|
`@latest` 绕不开这一层,本页早先的说法是错的。冷静期先把候选版本过滤掉,dist-tag 才在剩下的里面解析,于是它直接落到了更旧的那个上。改成写死精确版本号,pnpm 会把它当作一次明确的指定,而不是一次解析:
|
|
145
145
|
|
|
146
146
|
```sh
|
|
147
|
-
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.25.
|
|
147
|
+
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.25.3
|
|
148
148
|
```
|
|
149
149
|
|
|
150
150
|
`npm view @liustack/modlens version` 可以查到当前版本号。pnpm 11 会装上被点名的版本,11.1.3 起还会把它作为一条已批准的例外写进该 profile 的 `pnpm-workspace.yaml`,其余所有包和 modlens 以后的版本仍然留在窗口后面。
|
|
@@ -153,7 +153,7 @@ npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.25.2
|
|
|
153
153
|
|
|
154
154
|
```yaml
|
|
155
155
|
minimumReleaseAgeExclude:
|
|
156
|
-
- '@liustack/modlens@3.25.
|
|
156
|
+
- '@liustack/modlens@3.25.3'
|
|
157
157
|
```
|
|
158
158
|
|
|
159
159
|
或者只为这一条命令解除冷静期,注意它解除的是这条命令解析到的所有包,不只 modlens:
|
package/package.json
CHANGED
package/skills/modlens/SKILL.md
CHANGED
|
@@ -20,11 +20,11 @@ powershell -ExecutionPolicy Bypass -File <skill-dir>\scripts\run.ps1 <args>
|
|
|
20
20
|
|
|
21
21
|
It resolves a working runtime (PATH `modlens`, then `npx`, then `bunx`) and forwards your arguments unchanged. Exit 78 means no runtime: relay the `nextSteps` from its stderr JSON instead of retrying.
|
|
22
22
|
|
|
23
|
-
If your harness forbids running scripts, reason through the same order by hand and run the first line that works (the pinned version is 3.25.
|
|
23
|
+
If your harness forbids running scripts, reason through the same order by hand and run the first line that works (the pinned version is 3.25.3):
|
|
24
24
|
|
|
25
|
-
1. A `modlens` on `PATH` whose major version is 3 and is at least 3.25.
|
|
26
|
-
2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.25.
|
|
27
|
-
3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.25.
|
|
25
|
+
1. A `modlens` on `PATH` whose major version is 3 and is at least 3.25.3: `modlens <args>`.
|
|
26
|
+
2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.25.3 modlens <args>`.
|
|
27
|
+
3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.25.3 <args>`.
|
|
28
28
|
4. Otherwise tell the user no JavaScript runtime was found and that installing Node 22.19+ (https://nodejs.org) or Bun (https://bun.sh) is the next step. Do not claim modlens itself failed.
|
|
29
29
|
|
|
30
30
|
`references/runtime.md` documents the pin and the diagnostic fields.
|
|
@@ -24,7 +24,7 @@ $ErrorActionPreference = 'Stop'
|
|
|
24
24
|
# package.json version, and the release script rewrites it on every bump.
|
|
25
25
|
$Package = '@liustack/modlens'
|
|
26
26
|
$Bin = 'modlens'
|
|
27
|
-
$Pinned = '3.25.
|
|
27
|
+
$Pinned = '3.25.3'
|
|
28
28
|
# -------------------------------------------------------------------------------
|
|
29
29
|
|
|
30
30
|
$NativeNote = 'no native artifact is published for this tool yet; phase A ships npm launch paths only'
|
|
@@ -22,7 +22,7 @@ set -eu
|
|
|
22
22
|
# package.json version, and the release script rewrites it on every bump.
|
|
23
23
|
PKG="@liustack/modlens"
|
|
24
24
|
BIN="modlens"
|
|
25
|
-
PINNED="3.25.
|
|
25
|
+
PINNED="3.25.3"
|
|
26
26
|
# -------------------------------------------------------------------------------
|
|
27
27
|
|
|
28
28
|
NATIVE_NOTE="no native artifact is published for this tool yet; phase A ships npm launch paths only"
|