Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@ Node binary. README
(en + zh) leads with the npm one-liner; source clone remains the optional developer
path. No curl native-binary installer in this phase.

### O60 — custom OpenAI-compatible gateway docs

README (zh + en) documents `provider openai-compatible` with `model` / `baseUrl` /
`apiKeyEnv`. A trailing `/v1` on `baseUrl` is optional; Moss appends
`/v1/chat/completions` and never produces `/v1/v1`. `moss config --help` and the
invalid-`baseUrl` hint say `/v1` is optional. `test/api-v1-url.spec.mjs` locks
`buildApiV1Url` for bare hosts, trailing slash, `/v1`, and the d-robotics preset.

### O59 — doctor binary path and PATH duplicates

`moss doctor` prints `version` (with build stamp) and `binary` (realpath of the
running entry + package root), and warns when several distinct `moss` executables
are on PATH (PATH order, which one wins, whether it is the running one; PATHEXT on
Windows, symlinks deduped). Scanner lives in `src/cli/moss-binary-path.ts`.

### Install follow-ups

`npm ci` runs `scripts/check-node-version.cjs` as `preinstall`. The script is
Expand Down
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,21 @@ npm 11 可能打印 `npm warn install-scripts`。`npm ci` 可能会点名 `ssh2`
看一下这个目录里有什么
```

### 自定义 OpenAI 兼容网关

内置预设之外的网关用 `openai-compatible`。四条命令分别写入服务商、模型、地址和密钥变量名(`apiKeyEnv` 的值是变量名,不是密钥本身):

```bash
moss config set provider openai-compatible
moss config set model your-model-name
moss config set baseUrl https://gw.example
moss config set apiKeyEnv MY_GATEWAY_API_KEY
export MY_GATEWAY_API_KEY=your-key
moss doctor
```

`baseUrl` 末尾的 `/v1` 可写可不写。`https://gw.example` 和 `https://gw.example/v1` 都可以。Moss 自己拼接 `/v1/chat/completions`,不会变成 `/v1/v1`。不必写 `/chat/completions`,写了也会被去掉。`moss doctor` 核对服务商、模型、地址,以及密钥变量是否已设置。

进到交互界面后:

- `moss "整理这个项目的 README"` 直接派活;`@` 引用文件,`!` 执行 shell
Expand Down Expand Up @@ -490,6 +505,21 @@ With no usable config, `moss` sets itself up in that screen (press a number to p
look around this folder and tell me what it is
```

### Custom OpenAI-compatible gateway

For a gateway that is not a built-in preset, use `openai-compatible`. These four commands set the provider, model, address, and the key variable name (`apiKeyEnv` stores the name, not the secret):

```bash
moss config set provider openai-compatible
moss config set model your-model-name
moss config set baseUrl https://gw.example
moss config set apiKeyEnv MY_GATEWAY_API_KEY
export MY_GATEWAY_API_KEY=your-key
moss doctor
```

A trailing `/v1` on `baseUrl` is optional. Both `https://gw.example` and `https://gw.example/v1` work. Moss appends `/v1/chat/completions` itself and never produces `/v1/v1`. Do not include `/chat/completions`; it is not needed, and Moss strips it if it is present. `moss doctor` checks the provider, model, address, and whether the key variable is set.

Inside Moss: give it a job (`@` to reference files, `!` for shell), `Shift+Tab` to cycle modes
(`plan` = read-only planning), `Ctrl+V` to attach a clipboard image / Finder file / local path
(macOS; Linux: wl-paste/xclip; Windows: PowerShell), `/help` for keys and commands.
Expand Down
8 changes: 6 additions & 2 deletions src/cli/config-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ export function renderConfigHelp(zh = false): string {
' moss config set profile autonomous',
' moss config set provider openai-compatible',
' moss config set model <your-model>',
' moss config set baseUrl https://your-gateway.example # API root, not /v1 or /chat/completions',
' moss config set baseUrl https://your-gateway.example # /v1 is optional (https://host/v1 also works); /chat/completions is not needed',
' moss setup # stores the API key (hidden prompt, safer than command line)',
' moss config set --project safetyMode workspace-write',
` # ${WORKSPACE_WRITE_LIMIT_EN}`,
Expand Down Expand Up @@ -646,6 +646,7 @@ function renderConfigHelpZh(): string {
` # ${WORKSPACE_WRITE_LIMIT_ZH}`,
' `moss config set provider openai-compatible`',
' `moss config set model <模型>`',
' `moss config set baseUrl https://your-gateway.example` # /v1 可写可不写(https://主机/v1 也可以);不必带 /chat/completions',
' `moss config set rdkDocs false`',
' `moss config set rdkDocs.package ../rdk-docs-mcp`',
' `moss config set language auto|en|zh` # 只写用户配置(不能 --project,项目 .env 也不能设置)',
Expand Down Expand Up @@ -1061,7 +1062,10 @@ function applyConfigSetPair(
ok: false,
messages: [
`Invalid baseUrl: ${value.trim()}`,
'baseUrl must be a full http(s) URL, e.g. https://your-gateway.example (API root, no /v1)',
uiText(
'baseUrl must be a full http(s) URL, e.g. https://your-gateway.example or https://your-gateway.example/v1 (/v1 is optional)',
'baseUrl 必须是完整的 http(s) URL,例如 https://your-gateway.example 或 https://your-gateway.example/v1(/v1 可写可不写)'
),
],
};
}
Expand Down
79 changes: 78 additions & 1 deletion src/cli/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import {
import { isZhLocale, setupCopy } from './cli-locale.js';
import { glossConfigValue, localizeConfigSource } from './config-snapshot.js';
import { detectEnvCredentials } from './env-credentials.js';
import { collectMossRuntimeFacts, displayMossPath } from './moss-binary-path.js';
import { formatVersionLine, getPackageJsonPath, readBuildStamp } from './package-info.js';
import type { BuildStamp } from './package-info.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';
Expand Down Expand Up @@ -142,6 +145,80 @@ export function cliDoctorHasFailure(report: string): boolean {
return report.split('\n').some((line) => line.startsWith(' fail '));
}

/**
* Version label for doctor. Same stamp as `moss --version` when a commit was
* recorded. Chinese avoids the English word `dirty` and keeps the commit in
* the same token as the version digits.
*/
export function formatDoctorVersion(version: string, stamp: BuildStamp | null): string {
if (!version || version === 'unknown') return uiText('unknown', '未知');
const formatted = formatVersionLine(version, stamp);
const marked = `moss v${version}`;
const en =
formatted.startsWith(marked) && formatted.length > marked.length
? `${version}${formatted.slice(marked.length)}`
: version;
const commit = stamp?.commit;
if (!commit) return uiText(en, version);
const dirtyNote = stamp?.dirty ? ',有未提交改动' : '';
const dateNote = stamp?.date ? `,${stamp.date}` : '';
return uiText(en, `${version}(${commit}${dirtyNote}${dateNote})`);
}

/** Version (with build stamp), running entry, package root, and a PATH collision warning. */
export function renderMossRuntimeDoctorLines(input: {
version: string;
stamp: BuildStamp | null;
entryPath: string;
packageRoot: string;
warning: string | null;
}): string[] {
const entry = displayMossPath(input.entryPath);
const root = displayMossPath(input.packageRoot);
const lines = [
ok(uiText('version', '版本'), formatDoctorVersion(input.version, input.stamp)),
ok(
uiText('binary', '程序'),
uiText(`${entry} (package ${root})`, `${entry}(安装根目录 ${root})`)
),
];
if (input.warning) lines.push(warn(uiText('binaries', '可执行文件'), input.warning));
return lines;
}

/**
* Facts for the running install. Stat and realpath only. On any failure this
* still returns a version line, and never a `fail` line.
*/
function mossRuntimeDoctorLines(version: string): string[] {
try {
const facts = collectMossRuntimeFacts({
pathEnv: process.env.PATH,
pathExt: process.env.PATHEXT,
platform: process.platform,
fs: {
statSync(filePath: string) {
return fs.statSync(filePath);
},
realpathSync(filePath: string) {
return fs.realpathSync(filePath);
},
},
argv1: process.argv[1],
packageJsonPath: getPackageJsonPath(),
});
return renderMossRuntimeDoctorLines({
version,
stamp: readBuildStamp(),
entryPath: facts.entryPath,
packageRoot: facts.packageRoot,
warning: facts.scan.warning,
});
} catch {
return [ok(uiText('version', '版本'), version || uiText('unknown', '未知'))];
}
}

/** Informational. A newer npm release is a warning, never a `fail` line. */
export function renderRdkDocsPinDoctorLine(note: RdkDocsPinNote): string {
if (note.kind === 'latest-newer') {
Expand Down Expand Up @@ -474,7 +551,7 @@ export function renderAuthDoctorLine(
export async function renderCliDoctor(options: DoctorOptions): Promise<string> {
const lines = [uiText('[doctor] Moss', '[诊断] Moss')];
lines.push(renderNodeDoctorLine());
lines.push(ok(uiText('version', '版本'), options.currentVersion));
lines.push(...mossRuntimeDoctorLines(options.currentVersion));
const pinNote = await rdkDocsPinNote(process.env);
if (pinNote) lines.push(renderRdkDocsPinDoctorLine(pinNote));
lines.push(renderAuthDoctorLine(options.config));
Expand Down
Loading
Loading