From 7daf0e1d05283ff0f858313dbf53704e24d26e56 Mon Sep 17 00:00:00 2001 From: QiaolongLi1201 Date: Sun, 11 Oct 2026 16:25:17 +0800 Subject: [PATCH 1/2] docs(o66/o85): board proxy README + doctor; RDK deps guidance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit O66: document HTTPS_PROXY/HTTP_PROXY/NO_PROXY for boards without outbound network; prefer git clone --depth 1; do not recommend NODE_USE_ENV_PROXY on current main. Doctor reports proxy env and gateway reachability hints, and notes CONNECT alone is not enough (O87 tracks CONNECT-without-answer). O85: document board-side inference deps — ban :8080 and archive…/simple/; only verified X5/X3 1.0.3 whl URLs; X5 OS≥3.5.0 uses board hbm_runtime. Doctor warns on deprecated pip indexes and optionally probes the verified whl downloads. No O87 product fix and no O43 installer changes in this PR. --- README.md | 129 ++++++++++++++ src/cli/doctor-board-proxy.ts | 184 ++++++++++++++++++++ src/cli/doctor-rdk-deps.ts | 279 +++++++++++++++++++++++++++++++ src/cli/doctor.ts | 10 ++ test/cli-doctor-o66-o85.spec.mjs | 262 +++++++++++++++++++++++++++++ 5 files changed, 864 insertions(+) create mode 100644 src/cli/doctor-board-proxy.ts create mode 100644 src/cli/doctor-rdk-deps.ts create mode 100755 test/cli-doctor-o66-o85.spec.mjs diff --git a/README.md b/README.md index 0103e1ea..1c541e35 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,36 @@ npm uninstall -g @rdk-moss/agent 可以删:`~/.moss/cache/npx`、`~/.cache/node-gyp`、npm 缓存(`npm cache clean --force`)、空的 `@rdk-moss` 目录,以及你不再需要的克隆目录。删掉 `~/.config/moss`、`~/.moss` 或项目里的 `.moss/` 会同时去掉 key、设置和任务记录;只有确定不要这些数据时再删。 +## 板子无法访问外网 / 需要代理 + +实验室 / 教室里的 RDK 板子常常**直连不了** GitHub、npm、Node 镜像和模型网关(TCP 超时)。第一步 `git clone` 就会卡住。 + +**必设环境变量(正式推荐,仅此):** + +```bash +export HTTPS_PROXY=http://127.0.0.1:1080 +export HTTP_PROXY=http://127.0.0.1:1080 +export NO_PROXY=localhost,127.0.0.1,::1 +# 当前 Moss main(含 O83 / #81 起)会自动让 Node fetch 走上述代理, +# 不必再设 NODE_USE_ENV_PROXY(那是旧 tip 的临时绕过,当前不需要) +``` + +端口与是否需要 `ssh -R` 隧道按现场配置。 + +**板上首装注意:** + +1. **git clone:** 用 `git clone --depth 1 https://github.com/D-Robotics/moss.git`;完整 clone 在慢网 / 代理下容易 GnuTLS / early EOF。 +2. **npm:** 国内可 `npm config set registry https://registry.npmmirror.com`;镜像**不能代替** `HTTPS_PROXY`——仍需代理才能出网时两者都要。 +3. **Node:** 许多 RDK 镜像已带 Node ≥ 22.16;若需 nvm,安装脚本依赖 `raw.githubusercontent.com`,不出网会超时——优先用系统 Node,或经代理装。 +4. **用户级 prefix:** root 下 `npm i -g` 会写 `/usr`;推荐 `npm config set prefix "$HOME/.npm-global"` 并把 `bin` 加进 PATH。 + +**如何确认代理生效(必须两条都过):** + +1. 代理日志里有目标模型 / 网关主机的 `CONNECT`; +2. 端到端拿到**有效回答**(`moss -p "…"` 退出码 0,不是「网络连接失败」)。 + +**仅有 CONNECT、仍报网络失败 = 未过关。** 排查时可临时 `MOSS_NO_RDK_DOCS=1` 做对照;根因修复见 backlog **O87**(CONNECT 有、回答无),本 README 不替代该项。连接类错误请检查代理 / 防火墙,**不要**按「换更快的模型」处理。 + ## 为什么是 Moss 普通 coding agent 的"完成"是模型说自己完成了。Moss 把这条判定链做成可审计的机器流程: @@ -190,6 +220,40 @@ moss tasks list # 只读查看机器人闭环产物 `src/index.ts` 的导出面是受 semver 保护的合同,由 `test/sdk-contract.spec.mjs` 快照锁定;`examples/` 下三个可运行集成(`npm run examples`)。 +## 板端推理依赖(RDK) + +> **不要**再用 `http://sdk.d-robotics.cc:8080/simple/`(已下线,连接失败)。 +> **不要**再用 `http(s)://archive.d-robotics.cc/simple/`(不是可用的 pip 索引,HTTPS 返回 404)。 + +### 按 OS / 板型选接口 + +| 板型 / 场景 | 应使用 | +| --------------------------------------------------- | -------------------------------------------------------------- | +| RDK X5,RDK OS ≥ 3.5.0(`rdk_x5`) | 板端自带 **`hbm_runtime`**;**禁止** `pip install hbm_runtime` | +| RDK X5 历史 demo(`rdk_x5_legacy` / OS < 3.5.0) | 下方已核实 X5 1.0.3 whl,或板端 `hobot_dnn` | +| RDK X3 | 下方已核实 X3 1.0.3 whl,或板端 `hobot_dnn` | +| RDK S 系列 | 板端 `hbm_runtime`;不要装 X5 的 `hobot_dnn` | + +权威说明:[Model Zoo 推理接口参考](https://developer.d-robotics.cc/model_zoo_doc/infer_api_ref)。 + +### 仅 legacy 的 whl(已核实 HTTP 200) + +文档里**只**出现下面两条直链;Moss **不会**自动装包。 + +```bash +# X5 legacy +wget -nc https://archive.d-robotics.cc/downloads/rdk_model_zoo/rdk_x5/bpu_infer_lib_x5-1.0.3-py3-none-any.whl +pip install bpu_infer_lib_x5-1.0.3-py3-none-any.whl + +# X3 +wget -nc https://archive.d-robotics.cc/downloads/rdk_model_zoo/rdk_x3/bpu_infer_lib_x3-1.0.3-py3-none-any.whl +pip install bpu_infer_lib_x3-1.0.3-py3-none-any.whl +``` + +### 缺少 libhbucp.so + +多为 pip 覆盖了系统 hobot / hbm 包,或系统 < 3.5.0 却跑新 Model Zoo。**禁止** `pip install hbm_runtime`。优先烧录 ≥ 3.5.0 且自带运行时的镜像;若曾用 pip 覆盖,卸载 pip 副本后 `apt` 重装 `hobot-dnn` 并 `ldconfig`。`moss doctor` 会警告旧 pip 源,并可选探测上述 whl 是否可达。 + ## 命令参考 | 子命令 | 用途 | @@ -436,6 +500,37 @@ That removes the command. These stay behind: Safe to delete: `~/.moss/cache/npx`, `~/.cache/node-gyp`, the npm cache (`npm cache clean --force`), that empty `@rdk-moss` directory, and the clone once you no longer need the source. Deleting `~/.config/moss`, `~/.moss`, or a project's `.moss/` also removes keys, settings, and task history. Delete those only when you want that data gone. +### Board has no outbound network / proxy setup + +Lab and classroom RDK boards often **cannot** reach GitHub, npm, Node mirrors, or the model gateway directly (TCP timeouts). The first `git clone` hangs. + +**Required environment variables (official recommendation — these only):** + +```bash +export HTTPS_PROXY=http://127.0.0.1:1080 +export HTTP_PROXY=http://127.0.0.1:1080 +export NO_PROXY=localhost,127.0.0.1,::1 +# Current Moss main (O83 / #81 onward) routes Node fetch through these proxies +# automatically. Do NOT set NODE_USE_ENV_PROXY — that was a temporary workaround +# on older tips and is not needed now. +``` + +Port and whether you need an `ssh -R` tunnel depend on your site. + +**First install on a board:** + +1. **git clone:** use `git clone --depth 1 https://github.com/D-Robotics/moss.git`. A full clone often fails with GnuTLS / early EOF on slow or proxied links. +2. **npm:** in China you may `npm config set registry https://registry.npmmirror.com`. A registry mirror **does not replace** `HTTPS_PROXY` when the board still needs a proxy to leave the LAN. +3. **Node:** many RDK images already ship Node ≥ 22.16. nvm’s install script needs `raw.githubusercontent.com` and will time out offline — prefer the system Node, or install via the proxy. +4. **User-level prefix:** `npm i -g` as root writes under `/usr`. Prefer `npm config set prefix "$HOME/.npm-global"` and put that `bin` on PATH. + +**How to confirm the proxy works (both required):** + +1. The proxy log shows `CONNECT` to the model / gateway host; and +2. End-to-end you get a **successful answer** (`moss -p "…"` exits 0 — not “network connection failed”). + +**CONNECT alone with a network failure is a FAIL.** For triage you can temporarily set `MOSS_NO_RDK_DOCS=1`. Root-cause work for “CONNECT without an answer” is backlog **O87**; this README does not replace that item. Treat connection errors as proxy / firewall issues — **not** “switch to a faster model”. + ### Why Moss A normal coding agent's definition of done is the model saying so. Moss makes that judgement an @@ -516,6 +611,40 @@ moss tasks list # read-only robotics artifacts The `src/index.ts` export surface is a semver-protected contract, snapshotted by `test/sdk-contract.spec.mjs`; three runnable integrations live in `examples/` (`npm run examples`). +### Board-side inference dependencies (RDK) + +> **Do not** use `http://sdk.d-robotics.cc:8080/simple/` (offline; connection refused). +> **Do not** use `http(s)://archive.d-robotics.cc/simple/` (not a working pip index; HTTPS returns 404). + +#### Pick the interface by OS / board + +| Board / scenario | Use | +| ----------------------------------------------------- | -------------------------------------------------------------------- | +| RDK X5, RDK OS ≥ 3.5.0 (`rdk_x5`) | Board-bundled **`hbm_runtime`**; **never** `pip install hbm_runtime` | +| RDK X5 legacy demos (`rdk_x5_legacy` / OS < 3.5.0) | Verified X5 1.0.3 whl below, or board `hobot_dnn` | +| RDK X3 | Verified X3 1.0.3 whl below, or board `hobot_dnn` | +| RDK S series | Board `hbm_runtime`; do not install X5 `hobot_dnn` | + +Official reference: [Model Zoo infer API](https://developer.d-robotics.cc/model_zoo_doc/infer_api_ref). + +#### Legacy whl only (verified HTTP 200) + +Only these two direct URLs appear in Moss docs. Moss **will not** auto-install packages. + +```bash +# X5 legacy +wget -nc https://archive.d-robotics.cc/downloads/rdk_model_zoo/rdk_x5/bpu_infer_lib_x5-1.0.3-py3-none-any.whl +pip install bpu_infer_lib_x5-1.0.3-py3-none-any.whl + +# X3 +wget -nc https://archive.d-robotics.cc/downloads/rdk_model_zoo/rdk_x3/bpu_infer_lib_x3-1.0.3-py3-none-any.whl +pip install bpu_infer_lib_x3-1.0.3-py3-none-any.whl +``` + +#### Missing libhbucp.so + +Usually a pip overlay of system hobot / hbm packages, or OS < 3.5.0 running a new Model Zoo. **Do not** `pip install hbm_runtime`. Prefer an image ≥ 3.5.0 with the board-bundled runtime; if pip overwrote packages, uninstall the pip copies, reinstall `hobot-dnn` via apt, and run `ldconfig`. `moss doctor` warns on deprecated pip indexes and can optionally probe the whl URLs above. + ### Command reference | Subcommand | Purpose | diff --git a/src/cli/doctor-board-proxy.ts b/src/cli/doctor-board-proxy.ts new file mode 100644 index 00000000..1bb640b7 --- /dev/null +++ b/src/cli/doctor-board-proxy.ts @@ -0,0 +1,184 @@ +/** + * O66 — board / proxy doctor lines (docs+doctor only). + * Reports HTTPS_PROXY usage, gateway reachability hints, and that + * NODE_USE_ENV_PROXY is not required on current Moss main. + * Does not implement O87 (CONNECT-without-answer) product fixes. + */ +import { ensureKeepAliveDispatcherInstalled } from '../provider/keep-alive-dispatcher.js'; +import { uiText } from '../utils/ui-language.js'; +import { ok, warn } from './doctor.js'; + +const GATEWAY_PROBE_MS = 2_500; + +export function readProxyEnv(env: NodeJS.ProcessEnv = process.env): { + httpsProxy: string | undefined; + httpProxy: string | undefined; + noProxy: string | undefined; + nodeUseEnvProxy: string | undefined; + hasProxy: boolean; +} { + const httpsProxy = env.HTTPS_PROXY || env.https_proxy; + const httpProxy = env.HTTP_PROXY || env.http_proxy; + const noProxy = env.NO_PROXY || env.no_proxy; + const nodeUseEnvProxy = env.NODE_USE_ENV_PROXY; + return { + httpsProxy, + httpProxy, + noProxy, + nodeUseEnvProxy, + hasProxy: Boolean(httpsProxy || httpProxy), + }; +} + +/** Shorten a proxy URL for display (drop credentials). */ +export function shortProxy(url: string): string { + try { + const u = new URL(url); + if (u.username || u.password) u.username = '***'; + u.password = ''; + return u.toString().replace(/\/$/, ''); + } catch { + return url.replace(/\/\/[^/@]+@/, '//***@'); + } +} + +export function renderProxyEnvDoctorLine(env: NodeJS.ProcessEnv = process.env): string { + const p = readProxyEnv(env); + const label = uiText('proxy', '代理'); + if (!p.hasProxy) { + return ok( + label, + uiText( + 'none — on boards without outbound internet set HTTPS_PROXY (see README “Board has no outbound network”)', + '未设置 — 板子无外网时请设置 HTTPS_PROXY(见 README「板子无法访问外网」)' + ) + ); + } + const parts: string[] = []; + if (p.httpsProxy) parts.push(`HTTPS_PROXY=${shortProxy(p.httpsProxy)}`); + if (p.httpProxy && p.httpProxy !== p.httpsProxy) + parts.push(`HTTP_PROXY=${shortProxy(p.httpProxy)}`); + if (p.noProxy) parts.push(`NO_PROXY set`); + const base = parts.join(', '); + const nodeNote = p.nodeUseEnvProxy + ? uiText( + ' · NODE_USE_ENV_PROXY is set but not required on current Moss main (O83+)', + ' · 已设 NODE_USE_ENV_PROXY,但当前 Moss main(O83 起)一般不需要' + ) + : uiText( + ' · Moss uses these automatically; NODE_USE_ENV_PROXY is not needed', + ' · 当前版本会自动使用这些变量,一般不需要 NODE_USE_ENV_PROXY' + ); + return ok(label, `${base}${nodeNote}`); +} + +export type GatewayProbeResult = + | { kind: 'skipped'; reason: 'no-base-url' } + | { kind: 'ok'; host: string; status: number } + | { kind: 'unreachable'; host: string; error: string }; + +/** + * Lightweight reachability probe: any HTTP response (incl. 401/404) means the + * host answered. Network errors mean unreachable. Does not validate auth. + */ +export async function probeGatewayReachability(input: { + baseUrl?: string; + timeoutMs?: number; + fetchImpl?: typeof fetch; +}): Promise { + const raw = (input.baseUrl ?? '').trim(); + if (!raw) return { kind: 'skipped', reason: 'no-base-url' }; + let host: string; + let target: string; + try { + const u = new URL(raw.includes('://') ? raw : `https://${raw}`); + host = u.host; + // Probe the API root; gateways often 401 without a key — that still means reachable. + target = `${u.origin}${u.pathname.replace(/\/$/, '') || ''}` || u.origin; + } catch { + return { kind: 'unreachable', host: raw, error: 'invalid base URL' }; + } + if (!input.fetchImpl) await ensureKeepAliveDispatcherInstalled(); + const fetchFn = input.fetchImpl ?? fetch; + const timeoutMs = input.timeoutMs ?? GATEWAY_PROBE_MS; + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + try { + const res = await fetchFn(target, { + method: 'GET', + headers: { 'user-agent': 'moss-doctor/1.0' }, + signal: controller.signal, + }); + return { kind: 'ok', host, status: res.status }; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + return { kind: 'unreachable', host, error: msg }; + } finally { + clearTimeout(timer); + } +} + +export function renderGatewayDoctorLines( + result: GatewayProbeResult, + env: NodeJS.ProcessEnv = process.env +): string[] { + const label = uiText('gateway', '网关'); + const p = readProxyEnv(env); + if (result.kind === 'skipped') { + return [ + warn( + label, + uiText( + 'no base URL configured — skip reachability probe', + '未配置 base URL — 跳过可达性探测' + ) + ), + ]; + } + if (result.kind === 'ok') { + const lines = [ + ok( + label, + uiText( + `${result.host} answered HTTP ${result.status} (reachability only — a full chat answer is still required; see O87 if CONNECT succeeds but Moss still fails)`, + `${result.host} 返回 HTTP ${result.status}(仅可达性 — 仍须端到端有效回答;若代理已有 CONNECT 但 Moss 仍失败见 O87)` + ) + ), + ]; + return lines; + } + // unreachable + if (p.hasProxy) { + return [ + warn( + label, + uiText( + `no response from ${result.host} — check HTTPS_PROXY/HTTP_PROXY and firewall. Moss uses these variables automatically; you should not need NODE_USE_ENV_PROXY on current releases. CONNECT alone is not enough: you also need a successful model answer (O87 tracks CONNECT-without-answer).`, + `${result.host} 无响应——请检查 HTTPS_PROXY/HTTP_PROXY 与防火墙。当前版本会自动使用这些变量,一般不需要 NODE_USE_ENV_PROXY。仅有 CONNECT 不够:还须端到端有效回答(CONNECT 有回答无归 O87)。` + ) + ), + ]; + } + return [ + warn( + label, + uiText( + `unreachable (no proxy env). On boards without outbound internet, set HTTPS_PROXY (see README “Board has no outbound network”). Do not treat this as “switch to a faster model”.`, + `不可达(未设代理)。板子无外网时请设置 HTTPS_PROXY(见 README「板子无法访问外网」)。不要按「换更快的模型」处理。` + ) + ), + ]; +} + +export async function renderBoardProxyDoctor( + options: { baseUrl?: string; env?: NodeJS.ProcessEnv; fetchImpl?: typeof fetch } = {} +): Promise { + const env = options.env ?? process.env; + const lines = [renderProxyEnvDoctorLine(env)]; + const probe = await probeGatewayReachability({ + baseUrl: options.baseUrl, + fetchImpl: options.fetchImpl, + }); + lines.push(...renderGatewayDoctorLines(probe, env)); + return lines; +} diff --git a/src/cli/doctor-rdk-deps.ts b/src/cli/doctor-rdk-deps.ts new file mode 100644 index 00000000..9be4e851 --- /dev/null +++ b/src/cli/doctor-rdk-deps.ts @@ -0,0 +1,279 @@ +/** + * O85 — board-side inference dependency doctor (docs+doctor only). + * Warns on deprecated pip indexes (:8080 / archive…/simple/), optionally + * probes verified Model Zoo whl URLs, and never recommends the dead sources. + * Does not auto-install packages or change apt sources. + */ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { ensureKeepAliveDispatcherInstalled } from '../provider/keep-alive-dispatcher.js'; +import { uiText } from '../utils/ui-language.js'; +import { ok, warn } from './doctor.js'; + +/** Verified HTTP 200 whl URLs only (O85 / o85-dependency-sources.md). */ +export const VERIFIED_BPU_INFER_WHLS = [ + { + board: 'x5', + url: 'https://archive.d-robotics.cc/downloads/rdk_model_zoo/rdk_x5/bpu_infer_lib_x5-1.0.3-py3-none-any.whl', + }, + { + board: 'x3', + url: 'https://archive.d-robotics.cc/downloads/rdk_model_zoo/rdk_x3/bpu_infer_lib_x3-1.0.3-py3-none-any.whl', + }, +] as const; + +export const DEPRECATED_PIP_8080 = 'sdk.d-robotics.cc:8080'; +export const DEPRECATED_PIP_SIMPLE = 'archive.d-robotics.cc/simple'; + +const OFFICIAL_REF = 'https://developer.d-robotics.cc/model_zoo_doc/infer_api_ref'; + +const WHL_PROBE_MS = 4_000; + +export type DeprecatedSourceHit = { + kind: '8080' | 'simple'; + source: string; +}; + +/** Match deprecated indexes in free-form text (env, pip.conf, scripts). */ +export function findDeprecatedPipSources(text: string): DeprecatedSourceHit[] { + const hits: DeprecatedSourceHit[] = []; + if (/sdk\.d-robotics\.cc:8080/i.test(text)) { + hits.push({ kind: '8080', source: DEPRECATED_PIP_8080 }); + } + if (/archive\.d-robotics\.cc\/simple/i.test(text)) { + hits.push({ kind: 'simple', source: DEPRECATED_PIP_SIMPLE }); + } + return hits; +} + +function uniqueHits(hits: DeprecatedSourceHit[]): DeprecatedSourceHit[] { + const seen = new Set(); + const out: DeprecatedSourceHit[] = []; + for (const h of hits) { + if (seen.has(h.kind)) continue; + seen.add(h.kind); + out.push(h); + } + return out; +} + +/** Collect candidate text from env + common pip config paths (best-effort). */ +export function collectPipSourceText( + env: NodeJS.ProcessEnv = process.env, + options: { homeDir?: string; cwd?: string; readFile?: (p: string) => string | undefined } = {} +): string { + const chunks: string[] = []; + for (const key of [ + 'PIP_INDEX_URL', + 'PIP_EXTRA_INDEX_URL', + 'UV_INDEX_URL', + 'UV_EXTRA_INDEX_URL', + 'PIP_FIND_LINKS', + ]) { + const v = env[key]; + if (v) chunks.push(`${key}=${v}`); + } + const home = options.homeDir ?? os.homedir(); + const cwd = options.cwd ?? process.cwd(); + const read = + options.readFile ?? + ((p: string) => { + try { + return fs.readFileSync(p, 'utf8'); + } catch { + return undefined; + } + }); + const candidates = [ + path.join(home, '.pip', 'pip.conf'), + path.join(home, '.config', 'pip', 'pip.conf'), + path.join(cwd, 'pip.conf'), + path.join(cwd, 'requirements.txt'), + '/etc/pip.conf', + ]; + // Windows-style path when HOME is a Windows profile (doctor may run cross-OS). + if (env.APPDATA) candidates.push(path.join(env.APPDATA, 'pip', 'pip.ini')); + for (const p of candidates) { + const body = read(p); + if (body) chunks.push(body); + } + return chunks.join('\n'); +} + +export function renderDeprecatedSourceWarnings(hits: DeprecatedSourceHit[]): string[] { + const lines: string[] = []; + for (const hit of uniqueHits(hits)) { + if (hit.kind === '8080') { + lines.push( + warn( + uiText('rdk pip', 'RDK pip 源'), + uiText( + `Deprecated pip index: sdk.d-robotics.cc:8080 is offline. Do not retry it. For RDK X5 on OS ≥ 3.5.0 use board-bundled hbm_runtime (never pip install hbm_runtime). For legacy demos only, wget the verified whl from archive.d-robotics.cc/downloads/… (see README “Board-side inference dependencies”). Official ref: ${OFFICIAL_REF}`, + `已停用的 pip 源:sdk.d-robotics.cc:8080 已下线,请勿重试。RDK X5 且 OS ≥ 3.5.0 请用板端自带 hbm_runtime(禁止 pip install hbm_runtime)。仅历史 demo 才 wget archive.d-robotics.cc/downloads/… 已核实 whl(见 README「板端推理依赖」)。官方说明:${OFFICIAL_REF}` + ) + ) + ); + } else { + lines.push( + warn( + uiText('rdk pip', 'RDK pip 源'), + uiText( + `archive.d-robotics.cc/simple/ is not a working pip index (HTTP 404). Use the direct .whl download URLs in the README, not -i …/simple/.`, + `archive.d-robotics.cc/simple/ 不是可用的 pip 索引(HTTP 404)。请用 README 中的 .whl 直链,不要写 -i …/simple/。` + ) + ) + ); + } + } + return lines; +} + +export type WhlProbeResult = { + board: string; + url: string; + ok: boolean; + status?: number; +}; + +export async function probeVerifiedWhls(input: { + fetchImpl?: typeof fetch; + timeoutMs?: number; + urls?: readonly { board: string; url: string }[]; +}): Promise { + if (!input.fetchImpl) await ensureKeepAliveDispatcherInstalled(); + const fetchFn = input.fetchImpl ?? fetch; + const timeoutMs = input.timeoutMs ?? WHL_PROBE_MS; + const list = input.urls ?? VERIFIED_BPU_INFER_WHLS; + const results: WhlProbeResult[] = []; + for (const item of list) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + try { + const res = await fetchFn(item.url, { + method: 'HEAD', + signal: controller.signal, + headers: { 'user-agent': 'moss-doctor/1.0' }, + }); + // Some CDNs reject HEAD; fall back to ranged GET. + if (res.status === 405 || res.status === 501) { + const getRes = await fetchFn(item.url, { + method: 'GET', + signal: controller.signal, + headers: { 'user-agent': 'moss-doctor/1.0', range: 'bytes=0-0' }, + }); + results.push({ + board: item.board, + url: item.url, + ok: getRes.ok || getRes.status === 206, + status: getRes.status, + }); + } else { + results.push({ board: item.board, url: item.url, ok: res.ok, status: res.status }); + } + } catch { + results.push({ board: item.board, url: item.url, ok: false }); + } finally { + clearTimeout(timer); + } + } + return results; +} + +export function renderWhlProbeLines(results: WhlProbeResult[]): string[] { + const lines: string[] = []; + const anyOk = results.some((r) => r.ok); + const anyFail = results.some((r) => !r.ok); + for (const r of results.filter((x) => x.ok)) { + lines.push( + ok( + uiText('rdk whl', 'RDK whl'), + uiText( + `RDK Model Zoo whl reachable: bpu_infer_lib_${r.board}-1.0.3 (HTTP ${r.status ?? 200}). Legacy install: wget then pip install the local file.`, + `RDK Model Zoo whl 可达:bpu_infer_lib_${r.board}-1.0.3(HTTP ${r.status ?? 200})。legacy 安装:先 wget 再本地 pip install。` + ) + ) + ); + } + if (anyFail && !anyOk) { + lines.push( + warn( + uiText('rdk whl', 'RDK whl'), + uiText( + 'Cannot reach archive.d-robotics.cc downloads (whl probe failed). Check outbound network/proxy; Moss will not auto-install packages.', + '无法访问 archive.d-robotics.cc 下载(whl 探测失败)。请检查出网/代理;Moss 不会自动装包。' + ) + ) + ); + } else if (anyFail) { + for (const r of results.filter((x) => !x.ok)) { + lines.push( + warn( + uiText('rdk whl', 'RDK whl'), + uiText( + `bpu_infer_lib_${r.board}-1.0.3 whl probe failed — check outbound network/proxy; Moss will not auto-install packages.`, + `bpu_infer_lib_${r.board}-1.0.3 whl 探测失败 — 请检查出网/代理;Moss 不会自动装包。` + ) + ) + ); + } + } + return lines; +} + +/** + * Hard rule: doctor output must never recommend :8080 or …/simple/ as install + * commands. This helper is used by tests and keeps the copy in one place. + */ +export function doctorCopyRecommendsDeprecatedSource(text: string): boolean { + // Allow "offline / do not retry / 404" warnings that mention the host. + if ( + /do not retry|已下线|请勿重试|not a working pip index|不是可用的 pip 索引|Deprecated pip index|已停用/i.test( + text + ) + ) { + return false; + } + return ( + /pip install[^\n]*sdk\.d-robotics\.cc:8080/i.test(text) || + /pip install[^\n]*archive\.d-robotics\.cc\/simple/i.test(text) || + /-i\s+https?:\/\/sdk\.d-robotics\.cc:8080/i.test(text) || + /-i\s+https?:\/\/archive\.d-robotics\.cc\/simple/i.test(text) + ); +} + +export async function renderRdkDepsDoctor( + options: { + env?: NodeJS.ProcessEnv; + homeDir?: string; + cwd?: string; + fetchImpl?: typeof fetch; + probeWhls?: boolean; + readFile?: (p: string) => string | undefined; + } = {} +): Promise { + const env = options.env ?? process.env; + const text = collectPipSourceText(env, { + homeDir: options.homeDir, + cwd: options.cwd, + readFile: options.readFile, + }); + const hits = findDeprecatedPipSources(text); + const lines = renderDeprecatedSourceWarnings(hits); + if (options.probeWhls !== false) { + const results = await probeVerifiedWhls({ fetchImpl: options.fetchImpl }); + lines.push(...renderWhlProbeLines(results)); + } else if (hits.length === 0) { + // Quiet tip when no deprecated source and probes skipped (unit tests). + lines.push( + ok( + uiText('rdk deps', 'RDK 依赖'), + uiText( + 'prefer board-bundled hbm_runtime on X5 OS ≥ 3.5.0; legacy demos use verified whl URLs in README (never :8080 or archive…/simple/)', + 'X5 OS ≥ 3.5.0 优先板端自带 hbm_runtime;历史 demo 用 README 已核实 whl 直链(勿用 :8080 或 archive…/simple/)' + ) + ) + ); + } + return lines; +} diff --git a/src/cli/doctor.ts b/src/cli/doctor.ts index ff338ea1..bf2df8b1 100644 --- a/src/cli/doctor.ts +++ b/src/cli/doctor.ts @@ -22,6 +22,8 @@ import { glossConfigValue, localizeConfigSource } from './config-snapshot.js'; import { detectEnvCredentials } from './env-credentials.js'; import { rdkDocsPinNote } from './rdk-docs-pin-check.js'; import type { RdkDocsPinNote } from './rdk-docs-pin-check.js'; +import { renderBoardProxyDoctor } from './doctor-board-proxy.js'; +import { renderRdkDepsDoctor } from './doctor-rdk-deps.js'; function doctorL(en: string, vars?: Record): string { return setupCopy(undefined, en, vars); @@ -640,6 +642,14 @@ export async function renderCliDoctor(options: DoctorOptions): Promise { // install ripgrep. lines.push(renderSearchDoctor(await isRgAvailable())); lines.push(...(await renderSearchBackendDoctor())); + // O66: proxy env + gateway reachability hints (no NODE_USE_ENV_PROXY). + lines.push( + ...(await renderBoardProxyDoctor({ + baseUrl: options.config.baseUrl, + })) + ); + // O85: deprecated RDK pip indexes + optional verified whl probe. + lines.push(...(await renderRdkDepsDoctor())); lines.push(...renderApprovalDoctor(options.config)); lines.push(ok(uiText('detail', '详细程度'), detailModeLabel(options.detailMode))); diff --git a/test/cli-doctor-o66-o85.spec.mjs b/test/cli-doctor-o66-o85.spec.mjs new file mode 100755 index 00000000..a6f16a4a --- /dev/null +++ b/test/cli-doctor-o66-o85.spec.mjs @@ -0,0 +1,262 @@ +#!/usr/bin/env node +/** + * O66 + O85 doctor copy — proxy/gateway hints and deprecated RDK pip sources. + * Product code changes: src/cli/doctor-board-proxy.ts, doctor-rdk-deps.ts. + */ +import assert from 'node:assert/strict'; +import { createServer } from 'node:http'; + +const { + readProxyEnv, + shortProxy, + renderProxyEnvDoctorLine, + renderGatewayDoctorLines, + probeGatewayReachability, + renderBoardProxyDoctor, +} = await import('../dist/cli/doctor-board-proxy.js'); + +const { + findDeprecatedPipSources, + collectPipSourceText, + renderDeprecatedSourceWarnings, + renderWhlProbeLines, + probeVerifiedWhls, + doctorCopyRecommendsDeprecatedSource, + renderRdkDepsDoctor, + VERIFIED_BPU_INFER_WHLS, + DEPRECATED_PIP_8080, + DEPRECATED_PIP_SIMPLE, +} = await import('../dist/cli/doctor-rdk-deps.js'); + +// ─── O66 proxy env ─────────────────────────────────────────────────────────── + +{ + const env = {}; + const p = readProxyEnv(env); + assert.equal(p.hasProxy, false); + const line = renderProxyEnvDoctorLine(env); + assert.match(line, /ok/); + assert.match(line, /HTTPS_PROXY|代理/); + assert.doesNotMatch(line, /set NODE_USE_ENV_PROXY=1/); +} + +{ + const env = { + HTTPS_PROXY: 'http://user:secret@127.0.0.1:1080', + NO_PROXY: 'localhost,127.0.0.1', + }; + const line = renderProxyEnvDoctorLine(env); + assert.match(line, /ok/); + assert.match(line, /1080/); + assert.doesNotMatch(line, /secret/); + assert.match(line, /NODE_USE_ENV_PROXY is not needed|一般不需要 NODE_USE_ENV_PROXY/); +} + +{ + const env = { + HTTPS_PROXY: 'http://127.0.0.1:1080', + NODE_USE_ENV_PROXY: '1', + }; + const line = renderProxyEnvDoctorLine(env); + assert.match(line, /not required|一般不需要/); +} + +assert.equal(shortProxy('http://alice:bob@proxy.example:3128'), 'http://***@proxy.example:3128'); + +// ─── O66 gateway unreachable copy ──────────────────────────────────────────── + +{ + const withProxy = renderGatewayDoctorLines( + { kind: 'unreachable', host: 'ai-api.d-robotics.cc', error: 'timeout' }, + { HTTPS_PROXY: 'http://127.0.0.1:9' } + ).join('\n'); + assert.match(withProxy, /warn/); + assert.match(withProxy, /HTTPS_PROXY/); + assert.match(withProxy, /should not need NODE_USE_ENV_PROXY|一般不需要 NODE_USE_ENV_PROXY/); + assert.match(withProxy, /O87/); + assert.match(withProxy, /check HTTPS_PROXY|请检查 HTTPS_PROXY/); + assert.doesNotMatch(withProxy, /try again shortly or switch to a faster model/i); +} + +{ + const noProxy = renderGatewayDoctorLines( + { kind: 'unreachable', host: 'ai-api.d-robotics.cc', error: 'timeout' }, + {} + ).join('\n'); + assert.match(noProxy, /no proxy env|未设代理/); + assert.match(noProxy, /Board has no outbound network|板子无法访问外网/); + assert.match(noProxy, /Do not treat this as|不要按/); + assert.doesNotMatch(noProxy, /try again shortly or switch to a faster model/i); +} + +{ + const okLine = renderGatewayDoctorLines( + { kind: 'ok', host: 'ai-api.example', status: 401 }, + { HTTPS_PROXY: 'http://127.0.0.1:9' } + ).join('\n'); + assert.match(okLine, /ok/); + assert.match(okLine, /401/); + assert.match(okLine, /O87/); +} + +// Live local server: probe sees HTTP answer. +{ + const server = createServer((req, res) => { + res.writeHead(401, { 'content-type': 'application/json' }); + res.end('{"error":"no key"}'); + }); + await new Promise((r) => server.listen(0, '127.0.0.1', r)); + const { port } = server.address(); + const result = await probeGatewayReachability({ + baseUrl: `http://127.0.0.1:${port}/v1`, + timeoutMs: 2000, + }); + server.close(); + assert.equal(result.kind, 'ok'); + assert.equal(result.status, 401); +} + +{ + const lines = await renderBoardProxyDoctor({ + baseUrl: undefined, + env: {}, + fetchImpl: async () => { + throw new Error('should not fetch'); + }, + }); + assert.ok(lines.some((l) => /proxy|代理/.test(l))); + assert.ok(lines.some((l) => /no base URL|未配置 base URL/.test(l))); +} + +// ─── O85 deprecated source detection ───────────────────────────────────────── + +{ + const hits = findDeprecatedPipSources( + 'pip install bpu_infer_lib_x5 -i http://sdk.d-robotics.cc:8080/simple/' + ); + assert.deepEqual( + hits.map((h) => h.kind), + ['8080'] + ); + assert.equal(hits[0].source, DEPRECATED_PIP_8080); +} + +{ + const hits = findDeprecatedPipSources('PIP_INDEX_URL=https://archive.d-robotics.cc/simple/\n'); + assert.deepEqual( + hits.map((h) => h.kind), + ['simple'] + ); + assert.equal(hits[0].source, DEPRECATED_PIP_SIMPLE); +} + +{ + const text = collectPipSourceText( + { PIP_INDEX_URL: 'http://sdk.d-robotics.cc:8080/simple/' }, + { readFile: () => undefined, homeDir: '/tmp', cwd: '/tmp' } + ); + assert.match(text, /8080/); + const warns = renderDeprecatedSourceWarnings(findDeprecatedPipSources(text)).join('\n'); + assert.match(warns, /Deprecated pip index|已停用的 pip 源/); + assert.match(warns, /hbm_runtime/); + assert.match(warns, /infer_api_ref/); + assert.equal(doctorCopyRecommendsDeprecatedSource(warns), false); +} + +{ + const warns = renderDeprecatedSourceWarnings([ + { kind: 'simple', source: DEPRECATED_PIP_SIMPLE }, + ]).join('\n'); + assert.match(warns, /HTTP 404|不是可用的 pip 索引/); + assert.match(warns, /direct \.whl|whl 直链/); + assert.equal(doctorCopyRecommendsDeprecatedSource(warns), false); +} + +// Hard rule: a recommended pip -i line is rejected by the helper. +assert.equal( + doctorCopyRecommendsDeprecatedSource( + 'pip install bpu_infer_lib_x5 -i http://sdk.d-robotics.cc:8080/simple/' + ), + true +); + +// ─── O85 whl probe copy (fake fetch) ───────────────────────────────────────── + +{ + const okResults = VERIFIED_BPU_INFER_WHLS.map((w) => ({ + board: w.board, + url: w.url, + ok: true, + status: 200, + })); + const lines = renderWhlProbeLines(okResults).join('\n'); + assert.match(lines, /bpu_infer_lib_x5-1.0.3/); + assert.match(lines, /bpu_infer_lib_x3-1.0.3/); + assert.match(lines, /HTTP 200/); + assert.equal(doctorCopyRecommendsDeprecatedSource(lines), false); + assert.doesNotMatch(lines, /:8080\/simple/); + assert.doesNotMatch(lines, /archive\.d-robotics\.cc\/simple\//); +} + +{ + const failLines = renderWhlProbeLines( + VERIFIED_BPU_INFER_WHLS.map((w) => ({ board: w.board, url: w.url, ok: false })) + ).join('\n'); + assert.match(failLines, /whl probe failed|whl 探测失败/); + assert.match(failLines, /will not auto-install|不会自动装包/); +} + +{ + const calls = []; + const results = await probeVerifiedWhls({ + urls: [{ board: 'x5', url: 'https://example.test/x5.whl' }], + fetchImpl: async (url, init) => { + calls.push({ url: String(url), method: init?.method }); + return { ok: true, status: 200 }; + }, + }); + assert.equal(results[0].ok, true); + assert.equal(calls[0].method, 'HEAD'); +} + +{ + const report = ( + await renderRdkDepsDoctor({ + env: { PIP_EXTRA_INDEX_URL: 'http://sdk.d-robotics.cc:8080/simple/' }, + probeWhls: false, + readFile: () => undefined, + homeDir: '/tmp', + cwd: '/tmp', + }) + ).join('\n'); + assert.match(report, /8080/); + assert.match(report, /do not retry|请勿重试/i); + assert.equal(doctorCopyRecommendsDeprecatedSource(report), false); + // Must not emit a copy-pasteable install using the dead index. + assert.doesNotMatch(report, /pip install[^\n]*-i[^\n]*8080/); +} + +{ + const report = ( + await renderRdkDepsDoctor({ + env: {}, + probeWhls: false, + readFile: () => undefined, + homeDir: '/tmp', + cwd: '/tmp', + }) + ).join('\n'); + assert.match(report, /hbm_runtime/); + assert.match(report, /:8080|simple\//); + assert.equal(doctorCopyRecommendsDeprecatedSource(report), false); +} + +// Only verified 1.0.3 URLs are exported for probing / docs. +assert.equal(VERIFIED_BPU_INFER_WHLS.length, 2); +for (const w of VERIFIED_BPU_INFER_WHLS) { + assert.match(w.url, /bpu_infer_lib_(x5|x3)-1\.0\.3-py3-none-any\.whl$/); + assert.doesNotMatch(w.url, /simple\//); + assert.doesNotMatch(w.url, /:8080/); +} + +console.log('[PASS] cli-doctor-o66-o85'); From fe712c47a932bf0501e269d6801dcb57e115fb68 Mon Sep 17 00:00:00 2001 From: QiaolongLi1201 Date: Sun, 11 Oct 2026 16:41:29 +0800 Subject: [PATCH 2/2] fix(ui): zh doctor allowlist + prose for O66/O85 language lint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add only readme/connect/whl/zoo/wget/pip to LITERAL_TOKENS for necessary doctor identifiers. Rewrite zh prose (系统/历史示例/历史方式/ baseUrl) and prefix gateway hosts with https:// so TLD crumbs are not allowlisted. Replace curly quotes in EN doctor copy that tripped the Chinese-residue check. --- src/cli/doctor-board-proxy.ts | 10 +++++----- src/cli/doctor-rdk-deps.ts | 8 ++++---- test/cli-doctor-o66-o85.spec.mjs | 2 +- test/cli-ui-language.spec.mjs | 3 ++- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/cli/doctor-board-proxy.ts b/src/cli/doctor-board-proxy.ts index 1bb640b7..ba5f9be1 100644 --- a/src/cli/doctor-board-proxy.ts +++ b/src/cli/doctor-board-proxy.ts @@ -49,7 +49,7 @@ export function renderProxyEnvDoctorLine(env: NodeJS.ProcessEnv = process.env): return ok( label, uiText( - 'none — on boards without outbound internet set HTTPS_PROXY (see README “Board has no outbound network”)', + 'none — on boards without outbound internet set HTTPS_PROXY (see README "Board has no outbound network")', '未设置 — 板子无外网时请设置 HTTPS_PROXY(见 README「板子无法访问外网」)' ) ); @@ -130,7 +130,7 @@ export function renderGatewayDoctorLines( label, uiText( 'no base URL configured — skip reachability probe', - '未配置 base URL — 跳过可达性探测' + '未配置 baseUrl — 跳过可达性探测' ) ), ]; @@ -141,7 +141,7 @@ export function renderGatewayDoctorLines( label, uiText( `${result.host} answered HTTP ${result.status} (reachability only — a full chat answer is still required; see O87 if CONNECT succeeds but Moss still fails)`, - `${result.host} 返回 HTTP ${result.status}(仅可达性 — 仍须端到端有效回答;若代理已有 CONNECT 但 Moss 仍失败见 O87)` + `https://${result.host} 返回 HTTP ${result.status}(仅可达性 — 仍须端到端有效回答;若代理已有 CONNECT 但 Moss 仍失败见 O87)` ) ), ]; @@ -154,7 +154,7 @@ export function renderGatewayDoctorLines( label, uiText( `no response from ${result.host} — check HTTPS_PROXY/HTTP_PROXY and firewall. Moss uses these variables automatically; you should not need NODE_USE_ENV_PROXY on current releases. CONNECT alone is not enough: you also need a successful model answer (O87 tracks CONNECT-without-answer).`, - `${result.host} 无响应——请检查 HTTPS_PROXY/HTTP_PROXY 与防火墙。当前版本会自动使用这些变量,一般不需要 NODE_USE_ENV_PROXY。仅有 CONNECT 不够:还须端到端有效回答(CONNECT 有回答无归 O87)。` + `https://${result.host} 无响应——请检查 HTTPS_PROXY/HTTP_PROXY 与防火墙。当前版本会自动使用这些变量,一般不需要 NODE_USE_ENV_PROXY。仅有 CONNECT 不够:还须端到端有效回答(CONNECT 有回答无归 O87)。` ) ), ]; @@ -163,7 +163,7 @@ export function renderGatewayDoctorLines( warn( label, uiText( - `unreachable (no proxy env). On boards without outbound internet, set HTTPS_PROXY (see README “Board has no outbound network”). Do not treat this as “switch to a faster model”.`, + `unreachable (no proxy env). On boards without outbound internet, set HTTPS_PROXY (see README "Board has no outbound network"). Do not treat this as "switch to a faster model".`, `不可达(未设代理)。板子无外网时请设置 HTTPS_PROXY(见 README「板子无法访问外网」)。不要按「换更快的模型」处理。` ) ), diff --git a/src/cli/doctor-rdk-deps.ts b/src/cli/doctor-rdk-deps.ts index 9be4e851..01879e3f 100644 --- a/src/cli/doctor-rdk-deps.ts +++ b/src/cli/doctor-rdk-deps.ts @@ -109,8 +109,8 @@ export function renderDeprecatedSourceWarnings(hits: DeprecatedSourceHit[]): str warn( uiText('rdk pip', 'RDK pip 源'), uiText( - `Deprecated pip index: sdk.d-robotics.cc:8080 is offline. Do not retry it. For RDK X5 on OS ≥ 3.5.0 use board-bundled hbm_runtime (never pip install hbm_runtime). For legacy demos only, wget the verified whl from archive.d-robotics.cc/downloads/… (see README “Board-side inference dependencies”). Official ref: ${OFFICIAL_REF}`, - `已停用的 pip 源:sdk.d-robotics.cc:8080 已下线,请勿重试。RDK X5 且 OS ≥ 3.5.0 请用板端自带 hbm_runtime(禁止 pip install hbm_runtime)。仅历史 demo 才 wget archive.d-robotics.cc/downloads/… 已核实 whl(见 README「板端推理依赖」)。官方说明:${OFFICIAL_REF}` + `Deprecated pip index: sdk.d-robotics.cc:8080 is offline. Do not retry it. For RDK X5 on OS ≥ 3.5.0 use board-bundled hbm_runtime (never pip install hbm_runtime). For legacy demos only, wget the verified whl from archive.d-robotics.cc/downloads/… (see README "Board-side inference dependencies"). Official ref: ${OFFICIAL_REF}`, + `已停用的 pip 源:sdk.d-robotics.cc:8080 已下线,请勿重试。RDK X5 且系统 ≥ 3.5.0 请用板端自带 hbm_runtime(禁止 pip install hbm_runtime)。仅历史示例才 wget archive.d-robotics.cc/downloads/… 已核实 whl(见 README「板端推理依赖」)。官方说明:${OFFICIAL_REF}` ) ) ); @@ -190,7 +190,7 @@ export function renderWhlProbeLines(results: WhlProbeResult[]): string[] { uiText('rdk whl', 'RDK whl'), uiText( `RDK Model Zoo whl reachable: bpu_infer_lib_${r.board}-1.0.3 (HTTP ${r.status ?? 200}). Legacy install: wget then pip install the local file.`, - `RDK Model Zoo whl 可达:bpu_infer_lib_${r.board}-1.0.3(HTTP ${r.status ?? 200})。legacy 安装:先 wget 再本地 pip install。` + `RDK Model Zoo whl 可达:bpu_infer_lib_${r.board}-1.0.3(HTTP ${r.status ?? 200})。历史方式安装:先 wget 再本地 pip install。` ) ) ); @@ -270,7 +270,7 @@ export async function renderRdkDepsDoctor( uiText('rdk deps', 'RDK 依赖'), uiText( 'prefer board-bundled hbm_runtime on X5 OS ≥ 3.5.0; legacy demos use verified whl URLs in README (never :8080 or archive…/simple/)', - 'X5 OS ≥ 3.5.0 优先板端自带 hbm_runtime;历史 demo 用 README 已核实 whl 直链(勿用 :8080 或 archive…/simple/)' + 'X5 系统 ≥ 3.5.0 优先板端自带 hbm_runtime;历史示例用 README 已核实 whl 直链(勿用 :8080 或 archive…/simple/)' ) ) ); diff --git a/test/cli-doctor-o66-o85.spec.mjs b/test/cli-doctor-o66-o85.spec.mjs index a6f16a4a..e6f86cde 100755 --- a/test/cli-doctor-o66-o85.spec.mjs +++ b/test/cli-doctor-o66-o85.spec.mjs @@ -125,7 +125,7 @@ assert.equal(shortProxy('http://alice:bob@proxy.example:3128'), 'http://***@prox }, }); assert.ok(lines.some((l) => /proxy|代理/.test(l))); - assert.ok(lines.some((l) => /no base URL|未配置 base URL/.test(l))); + assert.ok(lines.some((l) => /no base URL|未配置 baseUrl/.test(l))); } // ─── O85 deprecated source detection ───────────────────────────────────────── diff --git a/test/cli-ui-language.spec.mjs b/test/cli-ui-language.spec.mjs index 2c1b7b01..893d353c 100644 --- a/test/cli-ui-language.spec.mjs +++ b/test/cli-ui-language.spec.mjs @@ -661,7 +661,8 @@ balanced cautious autonomous manual acceptedits full never prompt moss_lang moss_config_dir moss_config_file moss_no_bundled_default editor wl-paste xclip echo printf rg grep test timeline stdio rdk-docs full-access fleet info processes resources temperature robotics network cameras partial -all-fail devices description view history failures verify false` +all-fail devices description view history failures verify false +readme connect whl zoo wget pip` .split(/\s+/) .filter((word) => word.length > 0) );