@liustack/modlens 3.21.2 → 3.22.1

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.22.1 - 2026-08-20
4
+
5
+ - **dsh: the settings card speaks the language dsh is set to, not the language the page was built in.** dsh 0.1.0-rc.7 freezes `<html lang="zh-CN">` into its built index.html and never rewrites it, so the card, keyed on the page language, stayed Chinese under a dsh switched to English. The card now asks dsh's own locale service first and subscribes to it through `useSyncExternalStore`, so a card sitting open follows a live language switch. The service rides a scoped inject of its own: naming it beside `slots` would keep the card off any host that ships no locale service, since `ctx.inject` waits for every service it names, and where the service is absent the old page-then-browser chain decides, unchanged. The one path that ignored the language either way is closed too: a failed load or save with no server detail used to hard-code English into the footer status region, and now falls back in the card's language, while whatever the server did say still travels untranslated, because that is the diagnosis rather than the card's copy. Verified in a browser against rc.7 with `locale.preference: en`: the card that rendered Chinese before renders English, matching the rest of the settings page.
6
+
7
+ ## 3.22.0 - 2026-08-20
8
+
9
+ - **OpenCode Go's vision models are now borrowable through `reuse.opencode` ([#70](https://github.com/liustack/modlens/issues/70)).** The `opencode models` listing carries no modality data, so the built-in vision table judges the ids, and that table's snapshot predated the qwen 3.7 generation and knew no mimo at all: discovery filtered out `qwen3.7-plus` and `mimo-v2.5`, and the opencode-cli route never registered. The table now recognizes `qwen3.7-plus`, `qwen3.7-flash`, `qwen3.8-max`, `mimo-v2.5` with its `-free` tier, and `mimo-v2-omni`, each verified image-capable against OpenCode's own catalog on models.dev. `mimo-v2.5-pro` and `qwen3.7-max` stay excluded because that catalog declares them text-only, which is why the mimo entries are exact tiers rather than a wildcard. Go-plan users also have a no-release-needed route: the endpoint speaks the OpenAI protocol, so `openai.baseUrl https://opencode.ai/zen/go/v1` plugs it into the openai slot directly. Thanks to @hatanokokosa for the report.
10
+
3
11
  ## 3.21.2 - 2026-08-20
4
12
 
5
13
  - **README: dsh-market now leads the ecosystem partners list.** Both the English and Chinese READMEs list the visual plugin market first, ahead of DeepSeek Harness Desktop. Docs only, no code change.
package/README.md CHANGED
@@ -10,11 +10,10 @@
10
10
 
11
11
  <p align="center">
12
12
  <a href="./README.zh-CN.md">简体中文</a> ·
13
- <a href="docs/troubleshooting.md">Troubleshooting</a> ·
14
13
  <a href="skills/modlens/references/configure.md">Configuration</a> ·
15
- <a href="docs/output-schema.md">Output contract</a> ·
14
+ <a href="docs/troubleshooting.md">Troubleshooting</a> ·
16
15
  <a href="docs/security.md">Security</a> ·
17
- <a href="https://github.com/liustack/modsearch"><b>🔎 ModSearch (web search)</b></a>
16
+ <a href="https://github.com/liustack/modsearch"><b>🔍 ModSearch (the best free web search plugin for DSH)</b></a>
18
17
  </p>
19
18
 
20
19
  <p align="center">
@@ -30,22 +29,22 @@ The flagship DeepSeek and GLM chat models are text-only and cannot read images.
30
29
 
31
30
  ## Talk to us
32
31
 
33
- Issues are welcome any time: [open one](https://github.com/liustack/modlens/issues/new/choose). And come find me on X: **[@liustack](https://x.com/liustack)**. What you built with it, which harness you are on, what should come next. New releases land there first, and a proper community space is on the way.
32
+ Issues are welcome any time: [open one](https://github.com/liustack/modlens/issues/new/choose). Follow the liustack WeChat official account, and come find me on X: **[@liustack](https://x.com/liustack)**. What you built with it, which harness you are on, and what should come next are all shared on WeChat and X. A proper community space is on the way.
34
33
 
35
34
  ## Highlights
36
35
 
37
- **🥇 The most capable vision plugin for DeepSeek Harness (dsh):** one command, `npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.21.2`, and the text-only DeepSeek model behind dsh reads images through a native `modlens_read_image` tool. Updating is the same command again. The version is named rather than `@latest` on purpose: pnpm 11 holds back releases published in the last 24 hours and resolves the tag against what survives, so `@latest` would install whatever shipped a day ago ([details](docs/harness-setup.md#keeping-it-up-to-date)).
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.22.1`. 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.
38
37
 
39
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 — and 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 text-only DeepSeek or GLM models and adds a wrapped entry per route (a stock install gets **`DeepSeek-V4-Flash (modlens vision)`** and **`DeepSeek-V4-Pro (modlens vision)`**; extra routes like opencode-go or zai get their own); the two families' own vision models 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)).
40
39
 
41
- **Paste an image and it reads it.** No saving to a file and passing a path first.
40
+ **Paste images directly in every harness.** No saving to a file and passing a path first.
42
41
 
43
42
  - **The lightest touch on the market.** No hooks, no wrappers, no local proxy daemon, not a single line changed in any harness config: on the skill harnesses it is exactly one skill folder, on dsh exactly one plugin. Uninstalling is deleting a folder, and your agents are back to stock.
44
- - **Zero-config start.** Reuses what Claude Code, Codex, OpenCode, or Pi already have set up: the multimodal models on your machine go straight to work. Nothing at all? Antigravity CLI is a free no-key channel, and a free Gemini key brings a read down to 5-10 seconds.
43
+ - **Zero-config start.** Reuses existing setup in Claude Code, Codex, OpenCode, and Pi, plus other multimodal models already on your machine. Nothing installed locally? Antigravity CLI is a free no-key channel, and a free Gemini key brings a read down to 5-10 seconds. API keys from every major OpenAI-compatible provider work too.
45
44
  - **Evidence, not imagination.** Full transcription, reading-order layout regions, entity and relation lists. The model quotes specifics.
46
45
  - **Install once, use everywhere.** Verified on real machines in Claude Code, Codex, Pi, and OpenCode.
47
46
 
48
- ## Installation
47
+ ## Install in other harnesses
49
48
 
50
49
  **Step 1, hand it to your AI.** Send it this line:
51
50
 
package/README.zh-CN.md CHANGED
@@ -10,11 +10,10 @@
10
10
 
11
11
  <p align="center">
12
12
  <a href="./README.md">English</a> ·
13
- <a href="docs/troubleshooting.zh-CN.md">故障排查</a> ·
14
13
  <a href="skills/modlens/references/configure.zh-CN.md">配置</a> ·
15
- <a href="docs/output-schema.zh-CN.md">输出契约</a> ·
14
+ <a href="docs/troubleshooting.zh-CN.md">故障排查</a> ·
16
15
  <a href="docs/security.zh-CN.md">安全</a> ·
17
- <a href="https://github.com/liustack/modsearch"><b>🔎 ModSearch(联网搜索)</b></a>
16
+ <a href="https://github.com/liustack/modsearch"><b>🔍 ModSearch(DSH 最强免费联网搜索插件)</b></a>
18
17
  </p>
19
18
 
20
19
  <p align="center">
@@ -30,11 +29,11 @@ DeepSeek 和 GLM 的主力对话模型是纯文本的,无法进行图片识别
30
29
 
31
30
  ## 交流
32
31
 
33
- 欢迎随时提[issue](https://github.com/liustack/modlens/issues/new/choose)。也欢迎来 X 上聊:**[@liustack](https://x.com/liustack)**,你用它做了什么、在哪个 harness 上跑、接下来该做什么,新版本也是那边先发。社群正在筹备中。
32
+ 有问题随时提[issue](https://github.com/liustack/modlens/issues/new/choose)。推荐你关注公众号 liustack,也欢迎你来 X 上聊:**[@liustack](https://x.com/liustack)**。你用它做了什么、在哪个 harness 上跑、接下来该做什么,都会在公众号和 X 同步更新。社群正在筹备中。
34
33
 
35
34
  ## 亮点
36
35
 
37
- **🥇 全网最强的 DeepSeek Harness(dsh)外挂视觉识别插件:**一条命令 `npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.21.2`,dsh 背后的纯文本 DeepSeek 模型即可通过原生 `modlens_read_image` 工具读图。更新就是再跑一遍同一条命令。这里点名版本号而不用 `@latest` 是有意的:pnpm 11 会扣住最近 24 小时内发布的版本,dist-tag 只在剩下的里面解析,用 `@latest` 装到的会是一天前发布的那个([细节](docs/harness-setup.zh-CN.md#保持更新))。
36
+ **🥇 全网最强的 DeepSeek Harness(dsh)外挂视觉识别插件:**一条命令即刻安装 `npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.22.1`。更多安装与更新细节参考 [配置手册](docs/harness-setup.zh-CN.md) 。如果用不惯命令行,也想想玩玩 DSH,推荐食用全网最轻量级的 DeepSeek Harness 桌面版封装 <a href="https://github.com/liustack/aimanager"><b> AIManager</b></a>,零代码零配置起手,一键帮你安装所有依赖环境。
38
37
 
39
38
  DeepSeek Harness 粘贴识图有两种玩法。
40
39
 
@@ -42,14 +41,14 @@ DeepSeek Harness 粘贴识图有两种玩法。
42
41
 
43
42
  **② 切到带 `(modlens vision)` 后缀的模型变体**(选择器有记忆,选一次就行)再粘贴:缩略图直接可见、所见即所得,体验更接近 Codex App。变体由插件自动发现生成:每条承载纯文本 DeepSeek 或 GLM 模型的 provider 路由各得一组包装条目(默认安装下就是 **`DeepSeek-V4-Flash (modlens vision)`** 和 **`DeepSeek-V4-Pro (modlens vision)`**,装了 opencode-go、zai 等额外路由的机器会各自多出一组),两家自己的视觉型号自动排除。走哪条通路由 host 依据真实模型元数据逐个裁决:只有被元数据确认纯文本的模型才会被接管,确认不了的一律不动,视觉模型因此保留原生贴图([细节](docs/harness-setup.zh-CN.md))。
44
43
 
45
- **直接粘贴图片识别** 无需先保存成文件再提供路径。
44
+ **所有 Harness 直接粘贴图片识别** 无需先保存成文件再提供路径。
46
45
 
47
46
  - **全网最轻量。** 不用 hook,不套壳,不跑本地代理进程,不改任何 harness 配置的一行字:在 skill 类 harness 里它就是一个 skill 文件夹,在 dsh 里就是一个插件。卸载等于删个文件夹,你的 agent 立刻回到原样。
48
- - **零配置起手。** 复用 Claude Code、Codex、OpenCode、Pi 已配置,直接复用你本机的其他多模态模型。什么都没有?Antigravity CLI 是免 key 的免费通道,配一个免费 Gemini key 可将识别耗时降至 5 到 10 秒。
49
- - **基于证据而非想象。** 全文转录、按阅读顺序划分的版面区块、实体与关系列表,模型引用的是具体内容。
47
+ - **零配置起手。** 复用 Claude Code、Codex、OpenCode、Pi 已有配置,直接复用你本机的其他多模态模型。如果你本机什么都没安装?Antigravity CLI 是免 key 的免费通道,配一个免费 Gemini key 可将识别耗时降至 5 到 10 秒。也支持所有主流的 OpenAI 兼容格式 API key。
48
+ - **基于证据,而非想象。** 全文转录、按阅读顺序划分的版面区块、实体与关系列表,模型引用的是具体内容。
50
49
  - **一次安装,多端可用。** Claude Code、Codex、Pi、OpenCode 均经真机验证。
51
50
 
52
- ## 安装
51
+ ## 在其他 Harness 中安装
53
52
 
54
53
  **第一步,交给你的 AI。** 把这句话发给它:
55
54
 
@@ -69,7 +68,7 @@ agy # 浏览器完成
69
68
  **DeepSeek Harness(dsh)用户不走 skill 流程**,本包就是原生 dsh 插件:
70
69
 
71
70
  ```sh
72
- npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.21.2
71
+ npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.22.1
73
72
  ```
74
73
 
75
74
  装完即有 `modlens_read_image` 工具,选「(modlens vision)」模型变体即可直接粘贴识图。引擎配置同样在 `~/.modlens`,详见[宿主接入](docs/harness-setup.zh-CN.md)。
package/dist/main.js CHANGED
@@ -2733,6 +2733,9 @@ const VISION_MODEL_PATTERNS = [
2733
2733
  "qwen*-vl*",
2734
2734
  "qwen3.5-plus*",
2735
2735
  "qwen3.6-plus*",
2736
+ "qwen3.7-plus*",
2737
+ "qwen3.7-flash*",
2738
+ "qwen3.8-max*",
2736
2739
  "kimi-k2.5*",
2737
2740
  "kimi-k2.6*",
2738
2741
  "kimi-k2.7*",
@@ -2740,6 +2743,11 @@ const VISION_MODEL_PATTERNS = [
2740
2743
  "moonshot-v1-*vision*",
2741
2744
  "minimax-vl*",
2742
2745
  "minimax-m3*",
2746
+ // mimo-v2.5's pro tier is text-only, so the free/base tiers are named
2747
+ // exactly instead of a mimo-v2.5* wildcard.
2748
+ "*mimo-v2.5",
2749
+ "*mimo-v2.5-free",
2750
+ "mimo-v2-omni*",
2743
2751
  "deepseek-vl*",
2744
2752
  "deepseek-ocr*",
2745
2753
  "janus*",
@@ -4995,7 +5003,7 @@ function parsePositiveInt(raw, flag) {
4995
5003
  }
4996
5004
  return Number.parseInt(raw, 10);
4997
5005
  }
4998
- program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.21.2");
5006
+ program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.22.1");
4999
5007
  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(
5000
5008
  "--extra-body <json>",
5001
5009
  `JSON merged into the API request body, e.g. '{"thinking":{"type":"disabled"}}'`
@@ -5105,7 +5113,7 @@ program.command("doctor").description(
5105
5113
  configPath: CONFIG_PATH,
5106
5114
  // Lets doctor name an installed skill copy that is older than
5107
5115
  // the CLI reporting on it (issue #33).
5108
- version: "3.21.2"
5116
+ version: "3.22.1"
5109
5117
  });
5110
5118
  const output = options.json ? JSON.stringify(report, null, 2) : renderDoctorReport(report);
5111
5119
  process.stdout.write(`${output}
@@ -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.21.2
58
+ npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.22.1
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.
@@ -75,13 +75,19 @@ The card never receives a stored key, only whether one is set, and leaving the
75
75
  key field empty keeps the stored one. `settingsCard: false` in the plugin row
76
76
  removes it, route included.
77
77
 
78
+ The card's own text follows dsh's interface language setting, switching
79
+ between Chinese and English as you switch dsh. On a dsh too old to expose that
80
+ setting to plugins, it follows the browser language instead. What the server
81
+ says back on a failure (an unknown engine, a path that would not open) stays as
82
+ it is: that is the diagnosis, not copy.
83
+
78
84
  ### Keeping it up to date
79
85
 
80
86
  modlens ships often, and both install shapes freeze at whatever version they
81
87
  got. On dsh, re-run the install with the version named:
82
88
 
83
89
  ```sh
84
- npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.21.2
90
+ npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.22.1
85
91
  ```
86
92
 
87
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.21.2
58
+ npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.22.1
59
59
  ```
60
60
 
61
61
  这会注册一个 `modlens_read_image` 工具,它的 schema 随每次请求抵达模型(不靠触发启发式),运行同一个包里自带的 modlens CLI,并把结构化证据作为工具的标准 JSON 输出返回。引擎、复用授权和 guard 规则仍在 `~/.modlens/config.json` 里,与其他所有 harness 共享。dsh 还在开发者预览阶段,插件接口可能变化。这个插件刻意保持很小的接触面(原生工具注册、视觉变体所用的 llm 适配层、附件读取器,以及一个 agent 执行前钩子),其中任何一处变动,它都会大声报错而不是无声退化。
@@ -66,12 +66,14 @@ dsh 的网页用户面前没有终端,所以引擎设置有一张卡片,在*
66
66
 
67
67
  这些值仍然住在 `~/.modlens/config.json`,与其他所有 harness 共享:卡片通过一条回环路由读写那个文件,所以在这里改一笔,和 `modlens config set` 改的是同一笔。卡片从不拿到已保存的密钥,只知道有没有;密钥框留空就不动已存的那个。在插件配置行里设 `settingsCard: false` 可以连同路由一起去掉它。
68
68
 
69
+ 卡片自己的文案跟随 dsh 的界面语言设置,你在 dsh 里切换语言,卡片跟着切。dsh 版本太旧、拿不到这个设置时,退回跟随浏览器语言。失败时服务端回的那句话保持原样(例如未知引擎、打不开的路径),那是诊断信息,不是界面文案。
70
+
69
71
  ### 保持更新
70
72
 
71
73
  modlens 发布很频繁,而两种安装形态都会冻结在装进来的那个版本上。dsh 上重跑一遍安装即可,版本号要点名:
72
74
 
73
75
  ```sh
74
- npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.21.2
76
+ npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.22.1
75
77
  ```
76
78
 
77
79
  `npm view @liustack/modlens version` 可以查到当前版本号,本页的版本号则由发布流程自动写入。
@@ -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.21.2
166
+ npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.22.1
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.21.2'
181
+ - '@liustack/modlens@3.22.1'
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.21.2
147
+ npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.22.1
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.21.2
153
153
 
154
154
  ```yaml
155
155
  minimumReleaseAgeExclude:
156
- - '@liustack/modlens@3.21.2'
156
+ - '@liustack/modlens@3.22.1'
157
157
  ```
158
158
 
159
159
  或者只为这一条命令解除冷静期,注意它解除的是这条命令解析到的所有包,不只 modlens:
package/dsh/client.js CHANGED
@@ -205,9 +205,9 @@ window.__ModuleLoader__.load({
205
205
  cliNote: 'This engine signs in through its own CLI: no key, no endpoint.',
206
206
  autoTitle: 'Auto mode',
207
207
  autoHint: 'Reuse the vision engines already on this machine.',
208
- found: 'found',
209
208
  notLoggedIn: 'found, not signed in',
210
- notFound: 'not on this machine',
209
+ loadFailed: 'load failed',
210
+ saveFailed: 'save failed',
211
211
  envSourced:
212
212
  'These come from environment variables. Saving copies them into the config file, which then becomes this engine’s only source.',
213
213
  },
@@ -232,18 +232,37 @@ window.__ModuleLoader__.load({
232
232
  cliNote: '该引擎通过自己的 CLI 登录,无需密钥和接口地址。',
233
233
  autoTitle: 'auto 模式',
234
234
  autoHint: '自动复用本机已有视觉引擎。',
235
- found: '已找到',
236
235
  notLoggedIn: '已找到,未登录',
237
- notFound: '本机没有',
236
+ loadFailed: '加载失败',
237
+ saveFailed: '保存失败',
238
238
  envSourced: '这些值来自环境变量。保存会把它们写进配置文件,此后该引擎只认配置文件。',
239
239
  },
240
240
  }
241
241
 
242
- function labels() {
243
- var lang = (document.documentElement.lang || navigator.language || 'en').toLowerCase()
242
+ // The copy this card speaks. `active` is dsh's own interface language,
243
+ // when its locale service was there to say. Asked first because the page
244
+ // language is not an answer on dsh 0.1.0-rc.7: the built index.html
245
+ // freezes `<html lang="zh-CN">` and never rewrites it, so a user set to
246
+ // English still read a Chinese card. Absent the service (older
247
+ // hosts, profiles that ship no locale) the page then the browser decide,
248
+ // unchanged.
249
+ function labels(active) {
250
+ var lang = (active || document.documentElement.lang || navigator.language || 'en').toLowerCase()
244
251
  return lang.indexOf('zh') === 0 ? TEXT.zh : TEXT.en
245
252
  }
246
253
 
254
+ // What the footer says when a request fails. Whatever the server said
255
+ // travels untranslated ('unknown engine: x', a path error): that is the
256
+ // diagnosis, and mapping it to error codes would buy a translation table
257
+ // with a much larger contact surface than this card is worth. Only the
258
+ // silent case, where there is no detail to show, gets a localized line.
259
+ function noteFrom(error, fallback) {
260
+ // An Error whose message is empty is the no-detail case, not a thing to
261
+ // stringify: String(error) on it reads 'Error'.
262
+ var detail = error && typeof error.message === 'string' ? error.message : error ? String(error) : ''
263
+ return detail || fallback
264
+ }
265
+
247
266
  // The next draft when the engine changes or a summary arrives. The three
248
267
  // engine fields belong to the newly selected engine; the reuse grants are
249
268
  // the user's pending answers and survive an engine switch, since granting
@@ -352,10 +371,25 @@ window.__ModuleLoader__.load({
352
371
  }
353
372
  }
354
373
 
355
- function ConfigCard(react, ui) {
374
+ // `localeRef` is a { current } handle on dsh's locale service, not the
375
+ // service itself: it is optional and may land after the card is built,
376
+ // so it is read at render time rather than captured here. Absent, both
377
+ // helpers below hand back nothing and labels() takes its old path.
378
+ function ConfigCard(react, ui, localeRef) {
356
379
  var h = react.createElement
357
380
  var Input = ui.Input
358
381
 
382
+ // Built once per card so useSyncExternalStore is not handed a new
383
+ // subscribe on every render, which would resubscribe every render.
384
+ var subscribeLocale = (onChange) => {
385
+ var locale = localeRef?.current
386
+ return locale ? locale.subscribe(onChange) : () => {}
387
+ }
388
+ var readLocale = () => {
389
+ var locale = localeRef?.current
390
+ return locale ? locale.getSnapshot().active : ''
391
+ }
392
+
359
393
  // The chrome is the native plugin card's, value for value (border,
360
394
  // layer backgrounds, 12px radius, header row with a rotating chevron,
361
395
  // footer with discard ghost + save primary), so this card reads as a
@@ -385,7 +419,18 @@ window.__ModuleLoader__.load({
385
419
  )
386
420
 
387
421
  return function ModlensCard() {
388
- var t = labels()
422
+ // Subscribed, not sampled: the language is a live setting, and a card
423
+ // sitting open while the user switches has to follow. getSnapshot and
424
+ // subscribe are the pair dsh documents as useSyncExternalStore-safe.
425
+ // That hook is React 18 and up; where it is missing the language is
426
+ // read once per render instead, which still follows a switch as soon
427
+ // as anything re-renders the card. The branch is on a closure
428
+ // constant, so hook order never varies within one card.
429
+ var t = labels(
430
+ typeof react.useSyncExternalStore === 'function'
431
+ ? react.useSyncExternalStore(subscribeLocale, readLocale)
432
+ : readLocale(),
433
+ )
389
434
  var openState = react.useState(false)
390
435
  var summaryState = react.useState(null)
391
436
  var draftState = react.useState(null)
@@ -403,7 +448,7 @@ window.__ModuleLoader__.load({
403
448
  fetch('/modlens/config?discover=1')
404
449
  .then((r) =>
405
450
  r.json().then((body) => {
406
- if (!r.ok) throw new Error(body.error || 'load failed')
451
+ if (!r.ok) throw new Error(body.error || '')
407
452
  return body
408
453
  }),
409
454
  )
@@ -413,7 +458,7 @@ window.__ModuleLoader__.load({
413
458
  noteState[1]('')
414
459
  })
415
460
  .catch((error) => {
416
- noteState[1](String(error.message ? error.message : error))
461
+ noteState[1](noteFrom(error, t.loadFailed))
417
462
  })
418
463
  }, [])
419
464
 
@@ -732,7 +777,7 @@ window.__ModuleLoader__.load({
732
777
  })
733
778
  .then((r) =>
734
779
  r.json().then((payload) => {
735
- if (!r.ok) throw new Error(payload.error || 'save failed')
780
+ if (!r.ok) throw new Error(payload.error || '')
736
781
  return payload
737
782
  }),
738
783
  )
@@ -745,7 +790,7 @@ window.__ModuleLoader__.load({
745
790
  noteState[1](t.saved)
746
791
  })
747
792
  .catch((error) => {
748
- noteState[1](String(error.message ? error.message : error))
793
+ noteState[1](noteFrom(error, t.saveFailed))
749
794
  })
750
795
  },
751
796
  style: {
@@ -829,11 +874,31 @@ window.__ModuleLoader__.load({
829
874
  }
830
875
 
831
876
  function registerCard(ctx) {
832
- // Reaching for an undeclared service throws in cordis, so the optional
833
- // dependency rides a scoped ctx.inject: the closure runs where slots
834
- // exists and never runs where it does not, exactly as the host half
835
- // takes webServer.
877
+ // Reaching for an undeclared service throws in cordis, so each optional
878
+ // dependency rides a scoped ctx.inject of its own: the closure runs
879
+ // where the service exists and never runs where it does not, exactly as
880
+ // the host half takes webServer.
836
881
  if (typeof ctx.inject !== 'function') return
882
+
883
+ // dsh's language service gets an inject of its own, and fills a handle
884
+ // the card reads later. Listing it beside slots would be worse than
885
+ // useless: ctx.inject waits for every service named, so on a host that
886
+ // never provides locale the card would never register at all. Here a
887
+ // missing service just leaves the handle empty, and the card falls back
888
+ // to the page language.
889
+ var localeRef = { current: null }
890
+ ctx.inject(['locale'], (scope) => {
891
+ localeRef.current = scope.locale
892
+ if (typeof scope.effect === 'function') {
893
+ scope.effect(
894
+ () => () => {
895
+ localeRef.current = null
896
+ },
897
+ 'modlens: locale handle',
898
+ )
899
+ }
900
+ })
901
+
837
902
  ctx.inject(['slots'], (scope) => {
838
903
  // The card and its route live and die together: with the host route
839
904
  // off (settingsCard: false, or no web profile) a card would only
@@ -844,7 +909,7 @@ window.__ModuleLoader__.load({
844
909
  .then((response) => {
845
910
  if (response.status === 404) return
846
911
  try {
847
- mountCard(scope)
912
+ mountCard(scope, localeRef)
848
913
  } catch (error) {
849
914
  console.error(`[modlens] settings card skipped: ${error}`)
850
915
  }
@@ -853,7 +918,7 @@ window.__ModuleLoader__.load({
853
918
  })
854
919
  }
855
920
 
856
- function mountCard(ctx) {
921
+ function mountCard(ctx, localeRef) {
857
922
  var react
858
923
  try {
859
924
  react = require('react')
@@ -862,7 +927,7 @@ window.__ModuleLoader__.load({
862
927
  return
863
928
  }
864
929
  var ui = require('@deepseek-ai/dsh-client-ui-primitives')
865
- var Card = ConfigCard(react, ui)
930
+ var Card = ConfigCard(react, ui, localeRef)
866
931
  ctx.slots.inject('settings.plugin.item', function* () {
867
932
  yield ctx.slots.register({ name: 'settings.plugin.item', id: 'modlens', key: 'modlens', order: 30 }, Card)
868
933
  })
@@ -892,7 +957,8 @@ window.__ModuleLoader__.load({
892
957
  secretFieldProps: secretFieldProps,
893
958
  ConfigCard: ConfigCard,
894
959
  }
895
- // `slots` is optional, so it is not required here: registerCard checks.
960
+ // `slots` and `locale` are both optional, so neither is required here:
961
+ // registerCard takes each on its own scoped inject.
896
962
  exports.inject = []
897
963
  return module.exports
898
964
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liustack/modlens",
3
- "version": "3.21.2",
3
+ "version": "3.22.1",
4
4
  "description": "Plug-in vision for text-only LLMs, powered by the free Antigravity CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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.21.2):
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.22.1):
24
24
 
25
- 1. A `modlens` on `PATH` whose major version is 3 and is at least 3.21.2: `modlens <args>`.
26
- 2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.21.2 modlens <args>`.
27
- 3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.21.2 <args>`.
25
+ 1. A `modlens` on `PATH` whose major version is 3 and is at least 3.22.1: `modlens <args>`.
26
+ 2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.22.1 modlens <args>`.
27
+ 3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.22.1 <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.
@@ -8,7 +8,7 @@ shell syntax.
8
8
 
9
9
  ## Pinned version
10
10
 
11
- - Pinned CLI version: 3.21.2
11
+ - Pinned CLI version: 3.22.1
12
12
  - npm package: `@liustack/modlens`
13
13
  - CLI binary name: `modlens`
14
14
 
@@ -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.21.2'
27
+ $Pinned = '3.22.1'
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.21.2"
25
+ PINNED="3.22.1"
26
26
  # -------------------------------------------------------------------------------
27
27
 
28
28
  NATIVE_NOTE="no native artifact is published for this tool yet; phase A ships npm launch paths only"