mocode-ai 1.6.0 → 1.6.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/README.md +23 -8
- package/README.zh-CN.md +24 -10
- package/dist/agent/run-coordinator.js +4 -0
- package/dist/agent/stages/tool-dispatcher.js +1 -0
- package/dist/agent/tool-turn.js +1 -1
- package/dist/attachments/image.js +119 -2
- package/dist/config/index.js +28 -4
- package/dist/config/profiles.js +42 -10
- package/dist/context/encoders/search.js +6 -2
- package/dist/context/vision-window.js +2 -2
- package/dist/i18n/index.js +30 -14
- package/dist/repl/commands/image.js +7 -2
- package/dist/repl/commands/router.js +13 -1
- package/dist/repl/commands/tool-group.js +1 -1
- package/dist/repl/commands.js +2 -0
- package/dist/runtime/dev-server-manager.js +4 -2
- package/dist/runtime/shell.js +153 -0
- package/dist/skills/builtin-skills.js +1 -1
- package/dist/tools/builtins/dev-server.js +23 -6
- package/dist/tools/builtins/grep.js +97 -26
- package/dist/tools/builtins/index.js +6 -6
- package/dist/tools/builtins/read-file.js +136 -12
- package/dist/tools/builtins/run-command.js +65 -10
- package/dist/tools/builtins/screenshot.js +17 -41
- package/dist/tools/builtins/web-fetch.js +147 -33
- package/dist/tools/builtins/write-file.js +104 -7
- package/dist/tools/constants.js +7 -0
- package/dist/tools/policy.js +25 -6
- package/dist/tools/router.js +20 -4
- package/dist/tools/tool-runtime.js +63 -1
- package/dist/ui/render.js +20 -2
- package/package.json +1 -1
package/dist/i18n/index.js
CHANGED
|
@@ -27,7 +27,7 @@ const zhCN = {
|
|
|
27
27
|
'commands.subagentOff': '禁止 orchestration',
|
|
28
28
|
'commands.subagentStatus': '查看 orchestration gate',
|
|
29
29
|
'commands.fe': '浏览器调试/桌面观察自动路由 gate',
|
|
30
|
-
'commands.feOn': '允许按需路由 browser/
|
|
30
|
+
'commands.feOn': '允许按需路由 browser/screenshot',
|
|
31
31
|
'commands.feOff': '禁止 browser-debug/desktop-observe',
|
|
32
32
|
'commands.feStatus': '查看前端能力 gate',
|
|
33
33
|
'commands.cu': 'Computer Use 自动路由 gate(高危)',
|
|
@@ -46,8 +46,10 @@ const zhCN = {
|
|
|
46
46
|
'commands.modelShow': '显示当前模型配置',
|
|
47
47
|
'commands.modelUse': '按名称应用预设',
|
|
48
48
|
'commands.modelDelete': '删除指定预设',
|
|
49
|
-
'commands.router': '
|
|
50
|
-
'commands.routerStatus': '
|
|
49
|
+
'commands.router': '工具预路由开关与后端(LLM / Jev)',
|
|
50
|
+
'commands.routerStatus': '显示当前预路由开关、后端与配置',
|
|
51
|
+
'commands.routerOn': '开启工具预路由(默认)',
|
|
52
|
+
'commands.routerOff': '关闭工具预路由(仅保留默认工具簇)',
|
|
51
53
|
'commands.routerLlm': '用主 LLM 做预路由(默认)',
|
|
52
54
|
'commands.routerJev': '用 Jev systemone API 做预路由',
|
|
53
55
|
'commands.routerTest': '探测 Jev 端点连通性',
|
|
@@ -232,6 +234,7 @@ const zhCN = {
|
|
|
232
234
|
'toolSummary.emptyFile': '(空文件)',
|
|
233
235
|
'toolSummary.noMatches': '(无匹配)',
|
|
234
236
|
'toolSummary.noOutput': '(无输出)',
|
|
237
|
+
'toolSummary.imageAttached': '图片已作为视觉输入附加',
|
|
235
238
|
'toolError.unknown': '错误:未知工具 "{name}"',
|
|
236
239
|
'toolError.invalidJson': '错误:工具 {name} 的 arguments 不是合法 JSON: {arguments}',
|
|
237
240
|
'toolError.execution': '错误:工具 {name} 执行失败: {message}',
|
|
@@ -291,8 +294,8 @@ const zhCN = {
|
|
|
291
294
|
'fe.status': 'Browser/desktop 自动路由:{state}',
|
|
292
295
|
'fe.stateOn': '允许',
|
|
293
296
|
'fe.stateOff': '禁止',
|
|
294
|
-
'fe.changedOn': '已允许按需路由 browser-debug / desktop-observe
|
|
295
|
-
'fe.changedOff': '已禁止 browser-debug / desktop-observe;后续路由不会暴露 browser / dev_server
|
|
297
|
+
'fe.changedOn': '已允许按需路由 browser-debug / desktop-observe;图片读取(read_file)始终属于公共工具。',
|
|
298
|
+
'fe.changedOff': '已禁止 browser-debug / desktop-observe;后续路由不会暴露 browser / screenshot(dev_server 不受此开关影响,始终可路由)。',
|
|
296
299
|
'fe.usage': '用法:/fe on|off|status',
|
|
297
300
|
'cu.status': 'Computer-control 自动路由:{state}',
|
|
298
301
|
'cu.stateOn': '允许',
|
|
@@ -351,12 +354,17 @@ const zhCN = {
|
|
|
351
354
|
'upgrade.pkgName': '包名:mocode-ai',
|
|
352
355
|
'upgrade.failed': '[upgrade] 失败:{message}',
|
|
353
356
|
'upgrade.usage': '/upgrade 用法:\n /upgrade 升级到最新版本\n /upgrade now 同 /upgrade\n /upgrade check 检查当前版本与最新版本差异\n /upgrade status 显示当前本地版本',
|
|
354
|
-
'router.
|
|
357
|
+
'router.toggleStatus': '工具预路由:{state}',
|
|
358
|
+
'router.stateOn': '开启',
|
|
359
|
+
'router.stateOff': '关闭(仅保留默认工具簇)',
|
|
360
|
+
'router.status': '预路由后端:{mode}',
|
|
355
361
|
'router.modeLlm': 'LLM(与主 Agent 同一后端,默认)',
|
|
356
362
|
'router.modeJev': 'Jev(TypeSafe systemone API)',
|
|
357
363
|
'router.jevConfig': '端点 {baseUrl} · 模型 {model} · key {key} · 阈值 {min}/{minMcp}',
|
|
358
364
|
'router.source': '来源 {env}({path})',
|
|
359
|
-
'router.usage': '/router 用法:\n /router
|
|
365
|
+
'router.usage': '/router 用法:\n /router 显示开关、后端与配置\n /router on 开启预路由(默认)\n /router off 关闭预路由(仅保留默认工具簇)\n /router llm 用主 LLM 做预路由(默认)\n /router jev 用 Jev systemone API 做预路由\n /router base <url> 设置 Jev 端点(本地兼容层可指向 localhost)\n /router key <key> 设置 Jev API key\n /router model <name> 设置 Jev 模型名(默认 jev-latest)\n /router min <0-1> 设置通用出簇阈值(默认 0.65)\n /router min-mcp <0-1> 设置 mcp 专用阈值(默认 0.85)\n /router test 探测 Jev 端点连通性',
|
|
366
|
+
'router.enabledOn': '已开启工具预路由(下一轮生效)',
|
|
367
|
+
'router.enabledOff': '已关闭工具预路由:不再执行逐轮路由调用,仅保留默认工具簇(下一轮生效)',
|
|
360
368
|
'router.changed': '已切换预路由后端 → {mode}(下一轮生效)',
|
|
361
369
|
'router.set': '已设置 {field} = {value}(下一轮生效)',
|
|
362
370
|
'router.invalid': '参数无效:{detail}',
|
|
@@ -395,7 +403,7 @@ const en = {
|
|
|
395
403
|
'commands.subagentOff': 'Block orchestration',
|
|
396
404
|
'commands.subagentStatus': 'Show orchestration gate',
|
|
397
405
|
'commands.fe': 'Browser-debug and desktop-observe routing gate',
|
|
398
|
-
'commands.feOn': 'Allow browser/
|
|
406
|
+
'commands.feOn': 'Allow browser/screenshot routing',
|
|
399
407
|
'commands.feOff': 'Block browser-debug and desktop-observe',
|
|
400
408
|
'commands.feStatus': 'Show frontend capability gate',
|
|
401
409
|
'commands.cu': 'Computer Use automatic-routing gate (high-risk)',
|
|
@@ -414,8 +422,10 @@ const en = {
|
|
|
414
422
|
'commands.modelShow': 'Show current model configuration',
|
|
415
423
|
'commands.modelUse': 'Apply a preset by name',
|
|
416
424
|
'commands.modelDelete': 'Delete a preset',
|
|
417
|
-
'commands.router': 'Tool pre-routing backend (LLM / Jev)',
|
|
418
|
-
'commands.routerStatus': 'Show
|
|
425
|
+
'commands.router': 'Tool pre-routing switch and backend (LLM / Jev)',
|
|
426
|
+
'commands.routerStatus': 'Show pre-routing switch, backend, and config',
|
|
427
|
+
'commands.routerOn': 'Enable tool pre-routing (default)',
|
|
428
|
+
'commands.routerOff': 'Disable tool pre-routing (keep default groups only)',
|
|
419
429
|
'commands.routerLlm': 'Route with the main LLM (default)',
|
|
420
430
|
'commands.routerJev': 'Route with the Jev systemone API',
|
|
421
431
|
'commands.routerTest': 'Probe Jev endpoint connectivity',
|
|
@@ -598,6 +608,7 @@ const en = {
|
|
|
598
608
|
'toolSummary.emptyFile': '(empty file)',
|
|
599
609
|
'toolSummary.noMatches': '(no matches)',
|
|
600
610
|
'toolSummary.noOutput': '(no output)',
|
|
611
|
+
'toolSummary.imageAttached': 'Image attached as visual input',
|
|
601
612
|
'toolError.unknown': 'Error: unknown tool "{name}"',
|
|
602
613
|
'toolError.invalidJson': 'Error: arguments for tool {name} are not valid JSON: {arguments}',
|
|
603
614
|
'toolError.execution': 'Error: tool {name} failed: {message}',
|
|
@@ -657,8 +668,8 @@ const en = {
|
|
|
657
668
|
'fe.status': 'Browser/desktop automatic routing: {state}',
|
|
658
669
|
'fe.stateOn': 'allowed',
|
|
659
670
|
'fe.stateOff': 'blocked',
|
|
660
|
-
'fe.changedOn': 'Automatic routing may now select browser-debug / desktop-observe;
|
|
661
|
-
'fe.changedOff': 'Browser-debug / desktop-observe are blocked; browser /
|
|
671
|
+
'fe.changedOn': 'Automatic routing may now select browser-debug / desktop-observe; image reading (read_file) remains a common tool.',
|
|
672
|
+
'fe.changedOff': 'Browser-debug / desktop-observe are blocked; browser / screenshot will not be exposed (dev_server is unaffected and stays routable).',
|
|
662
673
|
'fe.usage': 'Usage: /fe on|off|status',
|
|
663
674
|
'cu.status': 'Computer-control automatic routing: {state}',
|
|
664
675
|
'cu.stateOn': 'allowed',
|
|
@@ -717,12 +728,17 @@ const en = {
|
|
|
717
728
|
'upgrade.pkgName': 'Package: mocode-ai',
|
|
718
729
|
'upgrade.failed': '[upgrade] failed: {message}',
|
|
719
730
|
'upgrade.usage': 'Usage: /upgrade\n /upgrade Upgrade to latest\n /upgrade now Same as /upgrade\n /upgrade check Check current vs latest\n /upgrade status Show current local version',
|
|
720
|
-
'router.
|
|
731
|
+
'router.toggleStatus': 'Tool pre-routing: {state}',
|
|
732
|
+
'router.stateOn': 'on',
|
|
733
|
+
'router.stateOff': 'off (default groups only)',
|
|
734
|
+
'router.status': 'Pre-routing backend: {mode}',
|
|
721
735
|
'router.modeLlm': 'LLM (same backend as the main agent; default)',
|
|
722
736
|
'router.modeJev': 'Jev (TypeSafe systemone API)',
|
|
723
737
|
'router.jevConfig': 'endpoint {baseUrl} · model {model} · key {key} · thresholds {min}/{minMcp}',
|
|
724
738
|
'router.source': 'Source {env} ({path})',
|
|
725
|
-
'router.usage': 'Usage: /router\n /router Show backend and config\n /router llm Route with the main LLM (default)\n /router jev Route with the Jev systemone API\n /router base <url> Set the Jev endpoint (point at localhost for a local compat server)\n /router key <key> Set the Jev API key\n /router model <name> Set the Jev model name (default jev-latest)\n /router min <0-1> Set the general activation threshold (default 0.65)\n /router min-mcp <0-1> Set the mcp-specific threshold (default 0.85)\n /router test Probe Jev endpoint connectivity',
|
|
739
|
+
'router.usage': 'Usage: /router\n /router Show switch, backend, and config\n /router on Enable pre-routing (default)\n /router off Disable pre-routing (keep default groups only)\n /router llm Route with the main LLM (default)\n /router jev Route with the Jev systemone API\n /router base <url> Set the Jev endpoint (point at localhost for a local compat server)\n /router key <key> Set the Jev API key\n /router model <name> Set the Jev model name (default jev-latest)\n /router min <0-1> Set the general activation threshold (default 0.65)\n /router min-mcp <0-1> Set the mcp-specific threshold (default 0.85)\n /router test Probe Jev endpoint connectivity',
|
|
740
|
+
'router.enabledOn': 'Tool pre-routing enabled (takes effect next turn)',
|
|
741
|
+
'router.enabledOff': 'Tool pre-routing disabled: no per-turn routing call, default tool groups only (takes effect next turn)',
|
|
726
742
|
'router.changed': 'Pre-routing backend switched → {mode} (takes effect next turn)',
|
|
727
743
|
'router.set': 'Set {field} = {value} (takes effect next turn)',
|
|
728
744
|
'router.invalid': 'Invalid argument: {detail}',
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import * as layout from '../../ui/layout.js';
|
|
11
11
|
import { ui } from '../../ui/theme.js';
|
|
12
12
|
import { config } from '../../config/index.js';
|
|
13
|
-
import {
|
|
13
|
+
import { loadImageAttachmentWithDownscale, renderChip, MAX_INLINE_BYTES_DEFAULT } from '../../attachments/image.js';
|
|
14
14
|
import { modelSupportsVision } from '../../llm/capabilities.js';
|
|
15
15
|
import { unhandled, next } from './types.js';
|
|
16
16
|
export const imageCommands = [
|
|
@@ -52,7 +52,7 @@ export const imageCommands = [
|
|
|
52
52
|
return next();
|
|
53
53
|
}
|
|
54
54
|
const maxBytes = config.maxImageBytes ?? MAX_INLINE_BYTES_DEFAULT;
|
|
55
|
-
const r = await
|
|
55
|
+
const r = await loadImageAttachmentWithDownscale(arg, { maxBytes });
|
|
56
56
|
if (!r.ok) {
|
|
57
57
|
layout.contentWrite(`${ui.red}[image] ${r.reason}${ui.reset}\n`);
|
|
58
58
|
return next();
|
|
@@ -60,6 +60,11 @@ export const imageCommands = [
|
|
|
60
60
|
if (!ctx.attachments.list().find((a) => a.id === r.att.id)) {
|
|
61
61
|
ctx.attachments.push(r.att);
|
|
62
62
|
}
|
|
63
|
+
// 用户粘/挂的高 DPI 截图超限时会被降采样:必须说出来,否则用户以为发的是原图,
|
|
64
|
+
// 之后模型「看不清小字」时无从归因。
|
|
65
|
+
if (r.downscaledFrom) {
|
|
66
|
+
layout.contentWrite(` ${ui.yellow}⚠ 原图 ${r.downscaledFrom.width}×${r.downscaledFrom.height} 超过内联上限,已降采样后附加(磁盘原图不变)。${ui.reset}\n`);
|
|
67
|
+
}
|
|
63
68
|
layout.contentWrite(` ${ui.dim}${renderChip(r.att)} — will attach to next message${ui.reset}\n`);
|
|
64
69
|
// 提前警告(不阻断附加):当前模型已知不支持视觉(如 MiniMax M2.x / gpt-3.5 等)时,
|
|
65
70
|
// 附加时就提示,而不是等发送后才在 catch 块里翻译 API 报错——减少一轮无意义请求。
|
|
@@ -15,12 +15,13 @@
|
|
|
15
15
|
import * as layout from '../../ui/layout.js';
|
|
16
16
|
import { ui } from '../../ui/theme.js';
|
|
17
17
|
import { t } from '../../i18n/index.js';
|
|
18
|
-
import { getRouterMode, updateRouterMode, getJevRouterConfig, updateJevRouterConfig, isJevRouterConfigured, } from '../../config/index.js';
|
|
18
|
+
import { getRouterMode, updateRouterMode, getJevRouterConfig, updateJevRouterConfig, isJevRouterConfigured, isToolRoutingEnabled, updateToolRoutingEnabled, } from '../../config/index.js';
|
|
19
19
|
import { writeConfigKeys, CONFIG_PATH } from '../../config/file.js';
|
|
20
20
|
import { probeJev } from '../../tools/jev-client.js';
|
|
21
21
|
import { maskKey } from '../commands.js';
|
|
22
22
|
import { unhandled, next } from './types.js';
|
|
23
23
|
const ROUTER_ENV_KEYS = {
|
|
24
|
+
enabled: 'MOCODE_ROUTER_ENABLED',
|
|
24
25
|
mode: 'MOCODE_ROUTER_MODE',
|
|
25
26
|
baseUrl: 'MOCODE_ROUTER_JEV_BASE_URL',
|
|
26
27
|
apiKey: 'MOCODE_ROUTER_JEV_API_KEY',
|
|
@@ -38,6 +39,9 @@ export const routerCommands = [
|
|
|
38
39
|
const sub = (spaceIndex === -1 ? rest : rest.slice(0, spaceIndex)).toLowerCase();
|
|
39
40
|
const arg = spaceIndex === -1 ? '' : rest.slice(spaceIndex + 1).trim();
|
|
40
41
|
const showStatus = () => {
|
|
42
|
+
const enabled = isToolRoutingEnabled();
|
|
43
|
+
const stateLabel = t(enabled ? 'router.stateOn' : 'router.stateOff');
|
|
44
|
+
layout.contentWrite(`${enabled ? ui.green : ui.yellow}${t('router.toggleStatus', { state: stateLabel })}${ui.reset}\n`);
|
|
41
45
|
const mode = getRouterMode();
|
|
42
46
|
const label = mode === 'jev' ? t('router.modeJev') : t('router.modeLlm');
|
|
43
47
|
layout.contentWrite(`${ui.accent}${t('router.status', { mode: label })}${ui.reset}\n`);
|
|
@@ -56,6 +60,14 @@ export const routerCommands = [
|
|
|
56
60
|
showStatus();
|
|
57
61
|
return next();
|
|
58
62
|
}
|
|
63
|
+
// /router on | /router off —— 预路由总开关。off = 跳过每轮路由调用,只保留默认簇。
|
|
64
|
+
if (sub === 'on' || sub === 'off') {
|
|
65
|
+
const enabled = sub === 'on';
|
|
66
|
+
updateToolRoutingEnabled(enabled);
|
|
67
|
+
writeConfigKeys({ [ROUTER_ENV_KEYS.enabled]: enabled ? 'true' : 'false' });
|
|
68
|
+
layout.contentWrite(`${ui.green}${t(enabled ? 'router.enabledOn' : 'router.enabledOff')}${ui.reset}\n`);
|
|
69
|
+
return next();
|
|
70
|
+
}
|
|
59
71
|
// /router llm | /router jev —— 切换后端
|
|
60
72
|
if (sub === 'llm' || sub === 'jev') {
|
|
61
73
|
const mode = sub === 'jev' ? 'jev' : 'llm';
|
|
@@ -47,7 +47,7 @@ export const toolGroupCommands = [
|
|
|
47
47
|
return next();
|
|
48
48
|
},
|
|
49
49
|
// /fe|/frontend [status|on|off] — browser-debug + desktop-observe 的兼容 gate。
|
|
50
|
-
//
|
|
50
|
+
// 图片读取(read_file 魔数分流)是 common 工具,不受此开关影响。
|
|
51
51
|
(ctx) => {
|
|
52
52
|
const { line } = ctx;
|
|
53
53
|
const isFe = line === '/fe' || line.startsWith('/fe ') || line === '/frontend' || line.startsWith('/frontend ');
|
package/dist/repl/commands.js
CHANGED
|
@@ -94,6 +94,8 @@ export function buildSlashCommands() {
|
|
|
94
94
|
desc: d('commands.router'),
|
|
95
95
|
children: [
|
|
96
96
|
{ name: 'status', value: '/router', desc: d('commands.routerStatus') },
|
|
97
|
+
{ name: 'on', value: '/router on', desc: d('commands.routerOn') },
|
|
98
|
+
{ name: 'off', value: '/router off', desc: d('commands.routerOff') },
|
|
97
99
|
{ name: 'llm', value: '/router llm', desc: d('commands.routerLlm') },
|
|
98
100
|
{ name: 'jev', value: '/router jev', desc: d('commands.routerJev') },
|
|
99
101
|
{ name: 'test', value: '/router test', desc: d('commands.routerTest') },
|
|
@@ -13,6 +13,7 @@ import { mkdir, open, stat } from 'node:fs/promises';
|
|
|
13
13
|
import { dirname, join } from 'node:path';
|
|
14
14
|
import { randomBytes } from 'node:crypto';
|
|
15
15
|
import { filterEnv, getSandboxRoot, isCommandDenied, jailResolve } from '../sandbox/index.js';
|
|
16
|
+
import { defaultShellKind, shellSpawnSpec } from './shell.js';
|
|
16
17
|
export class DevServerManagerError extends Error {
|
|
17
18
|
code;
|
|
18
19
|
constructor(code, message) {
|
|
@@ -194,10 +195,11 @@ export async function startDevServer(opts) {
|
|
|
194
195
|
}
|
|
195
196
|
let child;
|
|
196
197
|
try {
|
|
197
|
-
|
|
198
|
+
const spec = shellSpawnSpec(opts.shell ?? defaultShellKind(), command);
|
|
199
|
+
child = spawn(spec.file, spec.args, {
|
|
198
200
|
cwd,
|
|
199
201
|
env: filterEnv(process.env),
|
|
200
|
-
windowsVerbatimArguments:
|
|
202
|
+
windowsVerbatimArguments: spec.windowsVerbatimArguments,
|
|
201
203
|
windowsHide: true,
|
|
202
204
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
203
205
|
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// shell 选择器:run_command / dev_server 共用的「命令 → spawn 规格」映射。
|
|
2
|
+
//
|
|
3
|
+
// 为什么存在:两处硬编码(win32 → cmd.exe,其余 → bash)让 Windows 上的模型只能用 cmd 语法,
|
|
4
|
+
// 而 LLM 的训练分布压倒性偏 POSIX/pwsh——`&&` 链、`$VAR`、单引号转义在 cmd 里语义不同甚至报错;
|
|
5
|
+
// 非交互 cmd 还有 `timeout /t` 直接失败这类暗坑。给模型一个显式 shell 参数,把选择权交出去。
|
|
6
|
+
//
|
|
7
|
+
// 默认 shell 的选择是**保守**的(刻意):
|
|
8
|
+
// - Windows 默认仍是 cmd.exe,与改动前逐字节一致 —— 不静默翻转所有 Windows 用户的默认解释器,
|
|
9
|
+
// 现有 prompt / skill / codegraph 的 .cmd 调用都假设 cmd,翻默认是产品级破坏性变更,须显式拍板。
|
|
10
|
+
// - 想用 bash/powershell:逐调用传 shell 参数,或设全局 MOCODE_SHELL=bash|powershell|cmd。
|
|
11
|
+
//
|
|
12
|
+
// Windows 的 Git Bash 探测(Git Bash 优先于 PATH 上的裸 bash,排除 WSL):
|
|
13
|
+
// - WSL 的 System32\bash.exe 也在 PATH 上,但它进的是 Linux 发行版:路径 /mnt/f/…、工具链、
|
|
14
|
+
// 甚至被安全策略拦截(wsl.exe 黑名单)都与 Windows 原生预期不符,绝不能当 bash 解析结果。
|
|
15
|
+
// - 只认 Git for Windows 风格的 bash.exe(常见安装路径 + PATH 上非 System32 的命中)。
|
|
16
|
+
// - 仅在**显式请求 shell=bash** 时才探测(懒求值 + 缓存):cmd 默认路径不付这个同步 IO 成本。
|
|
17
|
+
import { existsSync } from 'node:fs';
|
|
18
|
+
import { join, resolve } from 'node:path';
|
|
19
|
+
export const SHELL_KINDS = ['cmd', 'powershell', 'bash'];
|
|
20
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
21
|
+
/** Git for Windows 的常见固定安装位(不依赖 PATH 顺序)。 */
|
|
22
|
+
const GIT_BASH_CANDIDATES = [
|
|
23
|
+
'C:\\Program Files\\Git\\bin\\bash.exe',
|
|
24
|
+
'C:\\Program Files\\Git\\usr\\bin\\bash.exe',
|
|
25
|
+
'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
|
|
26
|
+
`${process.env.LOCALAPPDATA ?? ''}\\Programs\\Git\\bin\\bash.exe`,
|
|
27
|
+
];
|
|
28
|
+
/** PATH 命中里要排除的目录片段:WSL bash 住在 System32,语义完全不同;
|
|
29
|
+
* WindowsApps 目录下是 wsl 存根的应用执行别名,同样不能当 bash 解析。 */
|
|
30
|
+
const WSL_BASH_HINTS = ['system32', 'sysnative', 'windowsapps'];
|
|
31
|
+
function findWindowsBash() {
|
|
32
|
+
for (const candidate of GIT_BASH_CANDIDATES) {
|
|
33
|
+
if (candidate && existsSync(candidate))
|
|
34
|
+
return candidate;
|
|
35
|
+
}
|
|
36
|
+
// PATH 探测:手工拆分而不是 where.exe —— 需要看到完整路径才能排除 System32(WSL)。
|
|
37
|
+
// 先看 PATH 上的 git.exe(Git for Windows 的 <root>\cmd\git.exe):由 git 根反推
|
|
38
|
+
// <root>\bin\bash.exe。自定义安装位(如 D:\Git)不在上面的候选表里,但 git 在 PATH 上
|
|
39
|
+
// 是极普遍形态 —— 从 git 推 bash 比枚举安装目录鲁棒得多。
|
|
40
|
+
const pathVar = process.env.PATH ?? process.env.Path ?? '';
|
|
41
|
+
for (const dir of pathVar.split(';')) {
|
|
42
|
+
if (!dir)
|
|
43
|
+
continue;
|
|
44
|
+
if (WSL_BASH_HINTS.some((hint) => dir.toLowerCase().includes(hint)))
|
|
45
|
+
continue;
|
|
46
|
+
if (!existsSync(join(dir, 'git.exe')))
|
|
47
|
+
continue;
|
|
48
|
+
const gitRoot = resolve(dir, '..');
|
|
49
|
+
for (const candidate of [join(gitRoot, 'bin', 'bash.exe'), join(gitRoot, 'usr', 'bin', 'bash.exe')]) {
|
|
50
|
+
if (existsSync(candidate))
|
|
51
|
+
return candidate;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// 兜底:PATH 上直接出现的 bash.exe(排除 System32/WindowsApps 后,多为与 git 同目录布局)。
|
|
55
|
+
for (const dir of pathVar.split(';')) {
|
|
56
|
+
if (!dir)
|
|
57
|
+
continue;
|
|
58
|
+
if (WSL_BASH_HINTS.some((hint) => dir.toLowerCase().includes(hint)))
|
|
59
|
+
continue;
|
|
60
|
+
const candidate = join(dir, 'bash.exe');
|
|
61
|
+
if (existsSync(candidate))
|
|
62
|
+
return candidate;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
let cachedWindowsBash;
|
|
67
|
+
/** Windows 上的 Git Bash 可执行文件路径;探不到返回 null。结果缓存。 */
|
|
68
|
+
export function windowsGitBash() {
|
|
69
|
+
if (!IS_WINDOWS)
|
|
70
|
+
return null;
|
|
71
|
+
if (cachedWindowsBash === undefined)
|
|
72
|
+
cachedWindowsBash = findWindowsBash();
|
|
73
|
+
return cachedWindowsBash;
|
|
74
|
+
}
|
|
75
|
+
/** 测试/嵌入方显式重置探测缓存(例如单测里想模拟两种环境)。 */
|
|
76
|
+
export function resetShellDetectionCache() {
|
|
77
|
+
cachedWindowsBash = undefined;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 平台默认 shell(**保守**,刻意不翻转既有行为):
|
|
81
|
+
* - Windows:cmd.exe(与改动前逐字节一致)。MOCODE_SHELL 显式指定可整体切换。
|
|
82
|
+
* —— 不自动探到 Git Bash 就当默认:那会给所有 Windows 用户做静默的破坏性解释器变更,
|
|
83
|
+
* 现有 prompt/skill/codegraph 的 .cmd 调用都假设 cmd,翻默认须产品级显式拍板。
|
|
84
|
+
* 想用 bash 的用户:逐调用传 shell=bash(此时才懒探测 Git Bash 路径),或设 MOCODE_SHELL=bash。
|
|
85
|
+
* - 其余平台:bash(与旧硬编码一致);MOCODE_SHELL 只在落到真实 shell(bash/powershell)时生效,
|
|
86
|
+
* cmd 在 Unix 无意义,忽略。
|
|
87
|
+
*/
|
|
88
|
+
export function defaultShellKind() {
|
|
89
|
+
const forced = parseShellKind(process.env.MOCODE_SHELL);
|
|
90
|
+
if (IS_WINDOWS)
|
|
91
|
+
return forced ?? 'cmd';
|
|
92
|
+
return forced === 'bash' || forced === 'powershell' ? forced : 'bash';
|
|
93
|
+
}
|
|
94
|
+
/** 解析模型/用户传入的 shell 值;大小写无关,别名 sh→bash、pwsh/powershell.exe→powershell。非法值返回 null。 */
|
|
95
|
+
export function parseShellKind(raw) {
|
|
96
|
+
if (typeof raw !== 'string')
|
|
97
|
+
return null;
|
|
98
|
+
const value = raw.trim().toLowerCase();
|
|
99
|
+
if (!value)
|
|
100
|
+
return null;
|
|
101
|
+
if (value === 'cmd' || value === 'cmd.exe')
|
|
102
|
+
return 'cmd';
|
|
103
|
+
if (value === 'bash' || value === 'sh')
|
|
104
|
+
return 'bash';
|
|
105
|
+
if (value === 'powershell' || value === 'pwsh' || value === 'powershell.exe' || value === 'pwsh.exe') {
|
|
106
|
+
return 'powershell';
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* 把 shell 种类 + 单行命令映射成 spawn 规格。
|
|
112
|
+
*
|
|
113
|
+
* bash on Windows:优先用探测到的 Git Bash 绝对路径;调用方显式 shell=bash 而探测失败时
|
|
114
|
+
* 仍按裸 'bash' spawn(交给 PATH 解析,包括 WSL——用户显式选择就尊重,失败会由
|
|
115
|
+
* spawn error 显式报出,不静默换 shell)。
|
|
116
|
+
*/
|
|
117
|
+
export function shellSpawnSpec(kind, command) {
|
|
118
|
+
switch (kind) {
|
|
119
|
+
case 'cmd':
|
|
120
|
+
return { file: 'cmd.exe', args: ['/d', '/s', '/c', command], windowsVerbatimArguments: true };
|
|
121
|
+
case 'powershell':
|
|
122
|
+
// -NoProfile:不执行用户 profile,启动快且行为可复现;-NonInteractive:禁交互提示;
|
|
123
|
+
// -Command:单行命令串(与 cmd /c 心智一致)。
|
|
124
|
+
return {
|
|
125
|
+
file: IS_WINDOWS ? 'powershell.exe' : 'pwsh',
|
|
126
|
+
args: ['-NoProfile', '-NonInteractive', '-Command', command],
|
|
127
|
+
windowsVerbatimArguments: false,
|
|
128
|
+
};
|
|
129
|
+
case 'bash': {
|
|
130
|
+
const bashPath = IS_WINDOWS ? (windowsGitBash() ?? 'bash') : 'bash';
|
|
131
|
+
return { file: bashPath, args: ['-c', command], windowsVerbatimArguments: false };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** 平台默认 shell 的可执行文件名(用于系统提示里的 PLATFORM_NOTE 措辞)。 */
|
|
136
|
+
export function defaultShellLabel() {
|
|
137
|
+
const kind = defaultShellKind();
|
|
138
|
+
if (kind === 'bash')
|
|
139
|
+
return IS_WINDOWS ? 'Git Bash (POSIX)' : 'bash';
|
|
140
|
+
if (kind === 'powershell')
|
|
141
|
+
return 'PowerShell';
|
|
142
|
+
return 'cmd.exe';
|
|
143
|
+
}
|
|
144
|
+
/** 供 run_command / dev_server 的 description 复用的 shell 参数说明(单一事实源,防漂移)。
|
|
145
|
+
* 默认值不在文案里烘焙静态答案:真实默认由 defaultShellKind()(运行时读 env)决定,
|
|
146
|
+
* shell.ts 模块求值时 config 链路未必已把 ~/.mocode/config 回填进 env,静态串会与
|
|
147
|
+
* 系统提示(MOCODE_SHELL 分支)自相矛盾。改为 getter,取用时才求值。
|
|
148
|
+
* 平台默认:Windows=cmd、其余=bash;MOCODE_SHELL 可翻转。 */
|
|
149
|
+
export const shellParamDescription = () => 'Shell to run the command in: cmd | powershell | bash (default: ' +
|
|
150
|
+
defaultShellLabel() +
|
|
151
|
+
'). Pick the shell whose syntax the command is written in; do not mix (e.g. no %VAR% in bash, no $VAR in cmd).';
|
|
152
|
+
/** 兼容旧引用:初始化即求值的快照(不追踪 MOCODE_SHELL 翻转;新代码用 shellParamDescription())。 */
|
|
153
|
+
export const SHELL_PARAM_DESCRIPTION = shellParamDescription();
|
|
@@ -20,7 +20,7 @@ run_command({ command: "codegraph node <symbol-or-path>" })
|
|
|
20
20
|
|
|
21
21
|
注意:
|
|
22
22
|
|
|
23
|
-
- Windows 上 \`codegraph\` 是 .cmd
|
|
23
|
+
- Windows 上 \`codegraph\` 是 .cmd:cmd 默认下直接传 \`codegraph ...\` 即可;若默认 shell 是 bash(MOCODE_SHELL=bash),Git Bash 同样能按裸名解析 .cmd(\`bash -c "codegraph --version"\` 实测可用),解析不到就显式 \`shell: "cmd"\`。
|
|
24
24
|
- 单次查询 ≤ 60s 超时,模型按需重试/换 query。
|
|
25
25
|
|
|
26
26
|
## 何时用
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DevServerManagerError, getDevServerStatus, readDevServerLogs, startDevServer, stopDevServer, } from '../../runtime/dev-server-manager.js';
|
|
2
|
+
import { shellParamDescription, parseShellKind } from '../../runtime/shell.js';
|
|
2
3
|
function result(status, code, payload) {
|
|
3
4
|
return { status, code, retryable: false, output: JSON.stringify(payload, null, 2) };
|
|
4
5
|
}
|
|
@@ -20,11 +21,14 @@ function failure(error) {
|
|
|
20
21
|
}
|
|
21
22
|
export const devServerTool = {
|
|
22
23
|
name: 'dev_server',
|
|
23
|
-
description: '
|
|
24
|
-
'
|
|
24
|
+
description: 'Run ANY long-running background process that must outlive this tool call: a dev server, an inference/model ' +
|
|
25
|
+
'service, a watcher, a log tail, a queue consumer. The process keeps running across tool calls, so you can ' +
|
|
26
|
+
'start it, poll it, inspect its output, and stop it later — foreground run_command blocks and is killed at ' +
|
|
27
|
+
'its timeout, so it cannot host such a process (do NOT work around that with `start /b`, `nohup`, `&` or ' +
|
|
28
|
+
'detached spawns: you lose the logs and the handle).\n' +
|
|
25
29
|
'Actions: start (needs command; optional readyUrl or readyPattern to wait until it actually serves), ' +
|
|
26
|
-
'status (all
|
|
27
|
-
'Always stop
|
|
30
|
+
'status (all processes, or one by id), logs (tail output; pass offset for incremental reads), stop (terminate the process tree).\n' +
|
|
31
|
+
'Always stop processes you started once you are done verifying.',
|
|
28
32
|
risk: 'dangerous',
|
|
29
33
|
parameters: {
|
|
30
34
|
type: 'object',
|
|
@@ -34,8 +38,11 @@ export const devServerTool = {
|
|
|
34
38
|
enum: ['start', 'status', 'logs', 'stop'],
|
|
35
39
|
description: 'Operation to perform',
|
|
36
40
|
},
|
|
37
|
-
id: { type: 'string', description: '
|
|
38
|
-
command: {
|
|
41
|
+
id: { type: 'string', description: 'Process id returned by start (required for logs/stop)' },
|
|
42
|
+
command: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
description: 'Command to run for action=start, e.g. "npm run dev" or "python -m laya_play.serve --port 8765"',
|
|
45
|
+
},
|
|
39
46
|
cwd: { type: 'string', description: 'Working directory inside the workspace (default: workspace root)' },
|
|
40
47
|
readyUrl: {
|
|
41
48
|
type: 'string',
|
|
@@ -46,6 +53,7 @@ export const devServerTool = {
|
|
|
46
53
|
description: 'Wait until server output matches this case-insensitive regex, e.g. "ready in|listening on"',
|
|
47
54
|
},
|
|
48
55
|
timeoutMs: { type: 'integer', description: 'Readiness wait budget in ms (default 30000, max 180000)' },
|
|
56
|
+
shell: { type: 'string', enum: ['cmd', 'powershell', 'bash'], description: shellParamDescription() },
|
|
49
57
|
offset: { type: 'integer', description: 'Byte offset for action=logs (default: tail)' },
|
|
50
58
|
limit: { type: 'integer', description: 'Max bytes for action=logs (default 8000, max 64000)' },
|
|
51
59
|
},
|
|
@@ -59,12 +67,21 @@ export const devServerTool = {
|
|
|
59
67
|
const command = optionalString(args, 'command');
|
|
60
68
|
if (!command)
|
|
61
69
|
return invalid('action=start requires a non-empty command.');
|
|
70
|
+
// shell 非法值显式报错,不静默回落:模型以为在 powershell 里起服务却落到 cmd,
|
|
71
|
+
// 语法差异导致的启动失败极难归因。
|
|
72
|
+
let shell = null;
|
|
73
|
+
if (args.shell !== undefined) {
|
|
74
|
+
shell = parseShellKind(args.shell);
|
|
75
|
+
if (!shell)
|
|
76
|
+
return invalid(`Invalid shell "${String(args.shell)}". Use cmd, powershell, or bash.`);
|
|
77
|
+
}
|
|
62
78
|
const started = await startDevServer({
|
|
63
79
|
command,
|
|
64
80
|
cwd: optionalString(args, 'cwd'),
|
|
65
81
|
readyUrl: optionalString(args, 'readyUrl'),
|
|
66
82
|
readyPattern: optionalString(args, 'readyPattern'),
|
|
67
83
|
...(args.timeoutMs === undefined ? {} : { timeoutMs: Number(args.timeoutMs) }),
|
|
84
|
+
...(shell ? { shell } : {}),
|
|
68
85
|
...(ctx?.signal ? { signal: ctx.signal } : {}),
|
|
69
86
|
});
|
|
70
87
|
return result('success', 'OK', started);
|