mocode-ai 1.6.4 → 1.6.6
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 +382 -365
- package/README.zh-CN.md +19 -2
- package/dist/agent/model-turn.js +14 -1
- package/dist/agent/run-coordinator.js +7 -1
- package/dist/agent/spawn.js +38 -5
- package/dist/agent/stages/model-runner.js +1 -1
- package/dist/agent/stages/tool-dispatcher.js +1 -0
- package/dist/agent/tool-turn.js +2 -0
- package/dist/agent/work-discipline.js +3 -1
- package/dist/bots/bus.js +117 -0
- package/dist/bots/cli.js +102 -0
- package/dist/bots/store.js +100 -0
- package/dist/config/index.js +53 -2
- package/dist/config/presets.js +40 -1
- package/dist/config/profiles.js +9 -1
- package/dist/context/clearing.js +53 -0
- package/dist/context/relevance.js +6 -0
- package/dist/context/text-search.js +113 -0
- package/dist/headless.js +302 -0
- package/dist/index.js +171 -3
- package/dist/jobs/approval.js +153 -0
- package/dist/jobs/approve-cli.js +30 -0
- package/dist/jobs/attach.js +61 -0
- package/dist/jobs/checkpoint.js +33 -0
- package/dist/jobs/launch.js +107 -0
- package/dist/jobs/runner.js +59 -0
- package/dist/jobs/store.js +101 -0
- package/dist/jobs/worktree.js +63 -0
- package/dist/llm/index.js +35 -8
- package/dist/llm/providers/anthropic.js +14 -4
- package/dist/llm/reasoning.js +141 -0
- package/dist/memory/reflect.js +1 -1
- package/dist/memory/store.js +31 -28
- package/dist/models/budgets.js +11 -0
- package/dist/models/catalog.js +130 -0
- package/dist/models/map-preset.js +50 -0
- package/dist/models/model-panel.js +227 -0
- package/dist/models/protocol.js +44 -0
- package/dist/models/reasoning-cap.js +88 -0
- package/dist/models/search.js +58 -0
- package/dist/models/types.js +8 -0
- package/dist/notify/index.js +144 -0
- package/dist/repl/commands/bots.js +32 -0
- package/dist/repl/commands/effort.js +81 -0
- package/dist/repl/commands/jobs.js +109 -0
- package/dist/repl/commands/model-actions.js +138 -0
- package/dist/repl/commands/model-catalog.js +67 -0
- package/dist/repl/commands/model-panel-ui.js +107 -0
- package/dist/repl/commands/model.js +22 -20
- package/dist/repl/commands/registry.js +12 -0
- package/dist/repl/commands/schedule.js +37 -0
- package/dist/repl/commands/session.js +54 -0
- package/dist/repl/commands/stats.js +28 -0
- package/dist/repl/commands.js +6 -58
- package/dist/repl/runtime.js +5 -1
- package/dist/repl/status-bar.js +4 -2
- package/dist/rollback/store.js +17 -0
- package/dist/runtime/runtime.js +31 -1
- package/dist/schedule/cli.js +114 -0
- package/dist/schedule/cron.js +79 -0
- package/dist/schedule/daemon.js +166 -0
- package/dist/schedule/store.js +98 -0
- package/dist/session/compact.js +84 -3
- package/dist/session/index.js +2 -0
- package/dist/session/retention.js +297 -0
- package/dist/session/scheduler.js +30 -3
- package/dist/session/store.js +108 -6
- package/dist/session/usage-stats.js +90 -0
- package/dist/skills/activation.js +2 -0
- package/dist/skills/builtin-skills.js +153 -0
- package/dist/skills/discover.js +3 -1
- package/dist/skills/runner.js +4 -1
- package/dist/tools/builtins/arena.js +156 -0
- package/dist/tools/builtins/index.js +8 -0
- package/dist/tools/builtins/message-bus.js +98 -0
- package/dist/tools/builtins/read-file.js +19 -1
- package/dist/tools/builtins/run-command.js +2 -1
- package/dist/tools/builtins/session-search.js +33 -0
- package/dist/tools/builtins/task.js +2 -0
- package/dist/tools/constants.js +8 -0
- package/dist/tools/read-dedup.js +62 -0
- package/dist/tools/tool-runtime.js +1 -0
- package/dist/ui/batch.js +140 -104
- package/dist/ui/composer.js +4 -18
- package/dist/ui/fuzzy-picker.js +227 -0
- package/dist/ui/hierarchical-picker.js +302 -0
- package/dist/ui/intervention.js +17 -1
- package/dist/ui/layout-internal/statusbar.js +18 -7
- package/dist/ui/prompt-internal/editor.js +10 -27
- package/dist/ui/render.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** OpenAI 官方端点(官方 provider 记录通常不带 api 字段)。 */
|
|
2
|
+
export const OPENAI_DEFAULT_BASE_URL = 'https://api.openai.com/v1';
|
|
3
|
+
/**
|
|
4
|
+
* Anthropic 官方端点:provider 记录同样不带 api(端点内置 SDK)。
|
|
5
|
+
* 不带 /v1——anthropic.ts 的 endpoint() 会在后面拼 `/v1/messages`。
|
|
6
|
+
*/
|
|
7
|
+
export const ANTHROPIC_DEFAULT_BASE_URL = 'https://api.anthropic.com';
|
|
8
|
+
/**
|
|
9
|
+
* 判定该 provider 能否用 mocode 现有 fetch 直发。
|
|
10
|
+
* - 官方 anthropic 包 → Anthropic Messages 路径
|
|
11
|
+
* - openai-compatible 且带端点 → OpenAI Chat 路径
|
|
12
|
+
* - 官方 openai 包 → OpenAI Chat 路径(端点缺省补官方 /v1)
|
|
13
|
+
* - 其余(google/bedrock/azure/vertex/各 gateway…)→ 二期,UI 置灰
|
|
14
|
+
*/
|
|
15
|
+
export function classifyProvider(provider) {
|
|
16
|
+
switch (provider.npm) {
|
|
17
|
+
case '@ai-sdk/anthropic':
|
|
18
|
+
return 'anthropic-messages';
|
|
19
|
+
case '@ai-sdk/openai-compatible':
|
|
20
|
+
return provider.api && provider.api.trim() ? 'openai-chat' : 'unsupported';
|
|
21
|
+
case '@ai-sdk/openai':
|
|
22
|
+
return 'openai-chat';
|
|
23
|
+
default:
|
|
24
|
+
return 'unsupported';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** provider 实际要用的 baseURL:官方 openai/anthropic 缺 api 时补默认,其余原样返回。 */
|
|
28
|
+
export function resolveBaseURL(provider) {
|
|
29
|
+
if (provider.api && provider.api.trim())
|
|
30
|
+
return provider.api.trim();
|
|
31
|
+
if (provider.npm === '@ai-sdk/openai')
|
|
32
|
+
return OPENAI_DEFAULT_BASE_URL;
|
|
33
|
+
if (provider.npm === '@ai-sdk/anthropic')
|
|
34
|
+
return ANTHROPIC_DEFAULT_BASE_URL;
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
/** 映射到 ModelPreset.provider(presets.ts 的两值)。 */
|
|
38
|
+
export function toPresetProvider(proto) {
|
|
39
|
+
if (proto === 'openai-chat')
|
|
40
|
+
return 'openai';
|
|
41
|
+
if (proto === 'anthropic-messages')
|
|
42
|
+
return 'anthropic';
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { THINK_BUDGET_BY_EFFORT } from './budgets.js';
|
|
2
|
+
/**
|
|
3
|
+
* catalogProviderId → 方言。一家厂商一种方言(alibaba 全系 Qwen)。
|
|
4
|
+
* 未列出的 provider 返回 undefined → 保守不下发。
|
|
5
|
+
*/
|
|
6
|
+
const PROVIDER_DIALECT = {
|
|
7
|
+
openai: 'openai-effort',
|
|
8
|
+
openrouter: 'openai-effort',
|
|
9
|
+
xai: 'openai-effort',
|
|
10
|
+
alibaba: 'qwen',
|
|
11
|
+
zhipuai: 'thinking-toggle',
|
|
12
|
+
volcengine: 'thinking-toggle',
|
|
13
|
+
deepseek: 'thinking-toggle',
|
|
14
|
+
moonshotai: 'thinking-toggle',
|
|
15
|
+
};
|
|
16
|
+
export function dialectForProvider(catalogProviderId) {
|
|
17
|
+
return catalogProviderId ? PROVIDER_DIALECT[catalogProviderId] : undefined;
|
|
18
|
+
}
|
|
19
|
+
/** 在 effort values 中按归一化档位挑一个合法值(按优先级,找不到回退到最低/最高档)。 */
|
|
20
|
+
function pickEffortValue(effort, values, off) {
|
|
21
|
+
const v = values.map((s) => s.toLowerCase());
|
|
22
|
+
const priorities = off
|
|
23
|
+
? { any: ['off', 'disabled', 'minimal', 'low'] }
|
|
24
|
+
: effort === 'low'
|
|
25
|
+
? { any: ['low', 'minimal'] }
|
|
26
|
+
: effort === 'medium'
|
|
27
|
+
? { any: ['medium', 'mid', 'low'] }
|
|
28
|
+
: { any: ['high', 'xhigh', 'max', 'medium'] };
|
|
29
|
+
for (const cand of priorities.any) {
|
|
30
|
+
const hit = v.find((x) => x === cand || x.includes(cand));
|
|
31
|
+
if (hit)
|
|
32
|
+
return values[v.indexOf(hit)];
|
|
33
|
+
}
|
|
34
|
+
// 回退:off/低档取最低,高档取最高。
|
|
35
|
+
return off || effort === 'low' ? values[0] : values[values.length - 1];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 目录能力 → 请求字段;无法安全下发时返回 {}。
|
|
39
|
+
* 依据第一个匹配的 reasoning_option 决定形态。
|
|
40
|
+
*/
|
|
41
|
+
export function paramsFromCatalog(input) {
|
|
42
|
+
const dialect = dialectForProvider(input.catalogProviderId);
|
|
43
|
+
if (!dialect)
|
|
44
|
+
return {};
|
|
45
|
+
const opts = input.reasoningOptions ?? [];
|
|
46
|
+
const toggle = opts.find((o) => o.type === 'toggle');
|
|
47
|
+
const effortOpt = opts.find((o) => o.type === 'effort');
|
|
48
|
+
const budgetOpt = opts.find((o) => o.type === 'budget_tokens');
|
|
49
|
+
const isOff = input.effort === 'off';
|
|
50
|
+
// ── qwen 方言:enable_thinking + thinking_budget(budget 选项优先给预算) ──
|
|
51
|
+
if (dialect === 'qwen') {
|
|
52
|
+
if (isOff)
|
|
53
|
+
return { enable_thinking: false };
|
|
54
|
+
const base = { enable_thinking: true };
|
|
55
|
+
if (budgetOpt) {
|
|
56
|
+
let budget = THINK_BUDGET_BY_EFFORT[input.effort];
|
|
57
|
+
if (typeof budgetOpt.min === 'number')
|
|
58
|
+
budget = Math.max(budgetOpt.min, budget);
|
|
59
|
+
base.thinking_budget = budget;
|
|
60
|
+
}
|
|
61
|
+
return base;
|
|
62
|
+
}
|
|
63
|
+
// ── openai effort 方言 ──
|
|
64
|
+
if (dialect === 'openai-effort') {
|
|
65
|
+
if (effortOpt?.values && effortOpt.values.length > 0) {
|
|
66
|
+
// OpenAI 官方无法真关:off 落到最低档。
|
|
67
|
+
return { reasoning_effort: pickEffortValue(input.effort, effortOpt.values, isOff) };
|
|
68
|
+
}
|
|
69
|
+
if (toggle) {
|
|
70
|
+
return isOff ? { reasoning_effort: 'low' } : { reasoning_effort: input.effort };
|
|
71
|
+
}
|
|
72
|
+
// 无 effort/toggle 元数据但已知是 OpenAI 方言:直接下发归一化档位(off→low)。
|
|
73
|
+
return { reasoning_effort: isOff ? 'low' : input.effort };
|
|
74
|
+
}
|
|
75
|
+
// ── thinking-toggle 方言 ──
|
|
76
|
+
if (dialect === 'thinking-toggle') {
|
|
77
|
+
if (effortOpt?.values && effortOpt.values.length > 0 && !isOff) {
|
|
78
|
+
// 个别厂商用 thinking 但分档(如 deepseek low/high/max):仍只开关,标 enabled。
|
|
79
|
+
return { thinking: { type: 'enabled' } };
|
|
80
|
+
}
|
|
81
|
+
return { thinking: { type: isOff ? 'disabled' : 'enabled' } };
|
|
82
|
+
}
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
/** 目录是否声明该模型支持思考(供 recognizesReasoning)。 */
|
|
86
|
+
export function recognizesFromCatalog(reasoning) {
|
|
87
|
+
return reasoning === true;
|
|
88
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { classifyProvider } from './protocol.js';
|
|
2
|
+
/** 把目录拍平成条目列表。 */
|
|
3
|
+
export function flattenCatalog(providers) {
|
|
4
|
+
const out = [];
|
|
5
|
+
for (const [providerId, provider] of Object.entries(providers)) {
|
|
6
|
+
const supported = classifyProvider(provider) !== 'unsupported';
|
|
7
|
+
const providerName = provider.name || providerId;
|
|
8
|
+
for (const [modelId, model] of Object.entries(provider.models ?? {})) {
|
|
9
|
+
out.push({ providerId, providerName, modelId, model, supported });
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
/** 关键字过滤 + 能力过滤。 */
|
|
15
|
+
export function searchModels(providers, filters = {}) {
|
|
16
|
+
const q = filters.query?.trim().toLowerCase();
|
|
17
|
+
return flattenCatalog(providers)
|
|
18
|
+
.filter((e) => {
|
|
19
|
+
if (!filters.includeUnsupported && !e.supported)
|
|
20
|
+
return false;
|
|
21
|
+
if (filters.reasoning && !e.model.reasoning)
|
|
22
|
+
return false;
|
|
23
|
+
if (filters.toolCall && !e.model.tool_call)
|
|
24
|
+
return false;
|
|
25
|
+
if (filters.attachment && !e.model.attachment)
|
|
26
|
+
return false;
|
|
27
|
+
if (q) {
|
|
28
|
+
const hay = [e.providerName, e.providerId, e.modelId, e.model.name ?? '', e.model.family ?? '']
|
|
29
|
+
.join(' ')
|
|
30
|
+
.toLowerCase();
|
|
31
|
+
// 支持多关键字(空格分隔),全部命中才算。
|
|
32
|
+
if (!q.split(/\s+/).every((tok) => hay.includes(tok)))
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
})
|
|
37
|
+
.sort((a, b) => {
|
|
38
|
+
// 可直发优先;其次厂商名;再模型名(按发布时间倒序更适合挑新模型,但纯函数保持确定性)。
|
|
39
|
+
if (a.supported !== b.supported)
|
|
40
|
+
return a.supported ? -1 : 1;
|
|
41
|
+
const p = a.providerName.localeCompare(b.providerName);
|
|
42
|
+
if (p !== 0)
|
|
43
|
+
return p;
|
|
44
|
+
return (b.model.release_date ?? '').localeCompare(a.model.release_date ?? '');
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** 按 provider 分组(保持组内顺序),供 UI 分区渲染。 */
|
|
48
|
+
export function groupByProvider(entries) {
|
|
49
|
+
const map = new Map();
|
|
50
|
+
for (const e of entries) {
|
|
51
|
+
const arr = map.get(e.providerId);
|
|
52
|
+
if (arr)
|
|
53
|
+
arr.push(e);
|
|
54
|
+
else
|
|
55
|
+
map.set(e.providerId, [e]);
|
|
56
|
+
}
|
|
57
|
+
return map;
|
|
58
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// 任务结束通知:POST 到用户自配的 webhook,按 URL 自动识别格式:
|
|
2
|
+
// - ntfy:https://ntfy.sh/<topic>(POST 正文,Title header)
|
|
3
|
+
// - Bark:https://api.day.app/<key>(POST JSON {title, body})
|
|
4
|
+
// - Telegram:https://api.telegram.org/bot<token>/sendMessage[?chat_id=...]
|
|
5
|
+
// - 其他:generic JSON webhook
|
|
6
|
+
//
|
|
7
|
+
// 刻意 best-effort:通知失败永不影响任务结果、不重试(webhook 是副作用通道,
|
|
8
|
+
// 不该因为通知服务挂了让 job 变 failed)。
|
|
9
|
+
//
|
|
10
|
+
// 回环地址(localhost/127.0.0.1)走 node:http 直连:Node 24 的 fetch 默认使用
|
|
11
|
+
// HTTP_PROXY 环境代理,而 NO_PROXY 对 localhost 的绕过不可靠,带代理的机器上
|
|
12
|
+
// 本地 webhook 会被转发失败。公网地址仍走 fetch(保留代理穿透)。
|
|
13
|
+
function firstLine(prompt) {
|
|
14
|
+
const line = prompt.split('\n')[0] ?? '';
|
|
15
|
+
return line.length > 80 ? `${line.slice(0, 80)}…` : line;
|
|
16
|
+
}
|
|
17
|
+
function buildTitle(ev) {
|
|
18
|
+
if (ev.status === 'succeeded')
|
|
19
|
+
return 'mocode OK';
|
|
20
|
+
if (ev.status === 'failed')
|
|
21
|
+
return 'mocode FAILED';
|
|
22
|
+
return 'mocode APPROVAL NEEDED';
|
|
23
|
+
}
|
|
24
|
+
function buildBody(ev) {
|
|
25
|
+
if (ev.status === 'waiting') {
|
|
26
|
+
return (`Tool: ${ev.tool ?? '?'}\n\n${firstLine(ev.prompt)}\n\nsession: ${ev.sessionId}\nmodel: ${ev.model}` +
|
|
27
|
+
`\n\nApprove: mocode approve ${ev.sessionId}\nDeny: mocode deny ${ev.sessionId}`);
|
|
28
|
+
}
|
|
29
|
+
const secs = (ev.elapsedMs / 1000).toFixed(1);
|
|
30
|
+
return `${firstLine(ev.prompt)}\n\nsession: ${ev.sessionId}\nmodel: ${ev.model}\ntime: ${secs}s`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 按 URL 构造请求(纯函数)。
|
|
34
|
+
* 返回 null 表示 URL 无法识别(调用方静默跳过)。
|
|
35
|
+
*/
|
|
36
|
+
export function buildNotifyRequest(webhookUrl, ev) {
|
|
37
|
+
let url;
|
|
38
|
+
try {
|
|
39
|
+
url = new URL(webhookUrl);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const title = buildTitle(ev);
|
|
45
|
+
const body = buildBody(ev);
|
|
46
|
+
// Telegram:query 中的 chat_id 合并进 JSON body。
|
|
47
|
+
if (url.hostname === 'api.telegram.org' && url.pathname.includes('/sendMessage')) {
|
|
48
|
+
const chatId = url.searchParams.get('chat_id') ?? process.env.MOCODE_NOTIFY_CHAT_ID;
|
|
49
|
+
if (!chatId)
|
|
50
|
+
return null;
|
|
51
|
+
url.search = '';
|
|
52
|
+
return {
|
|
53
|
+
init: {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: { 'Content-Type': 'application/json' },
|
|
56
|
+
body: JSON.stringify({ chat_id: chatId, text: `${title}\n${body}` }),
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// Bark:{title, body}。
|
|
61
|
+
if (url.hostname === 'api.day.app' || url.hostname.endsWith('.day.app')) {
|
|
62
|
+
return {
|
|
63
|
+
init: {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: { 'Content-Type': 'application/json' },
|
|
66
|
+
body: JSON.stringify({ title, body }),
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// ntfy 风格(含自建实例):裸文本正文 + Title/Priority header。
|
|
71
|
+
if (url.hostname === 'ntfy.sh' || url.pathname.split('/').filter(Boolean).length === 1) {
|
|
72
|
+
return {
|
|
73
|
+
init: {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: { Title: title, Priority: ev.status === 'failed' ? 'high' : 'default' },
|
|
76
|
+
body,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// Generic JSON webhook。
|
|
81
|
+
return {
|
|
82
|
+
init: {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: { 'Content-Type': 'application/json' },
|
|
85
|
+
body: JSON.stringify({ ...ev, title, body }),
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** 是否回环地址(localhost/127.0.0.1/[::1])。 */
|
|
90
|
+
function isLoopback(hostname) {
|
|
91
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]' || hostname === '::1';
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 回环地址直连 POST(node:http)。
|
|
95
|
+
*/
|
|
96
|
+
function postLoopback(url, init) {
|
|
97
|
+
return new Promise((resolvePromise) => {
|
|
98
|
+
void (async () => {
|
|
99
|
+
const mod = url.protocol === 'https:' ? await import('node:https') : await import('node:http');
|
|
100
|
+
const headers = {};
|
|
101
|
+
const headersInit = init.headers;
|
|
102
|
+
if (headersInit) {
|
|
103
|
+
for (const [k, v] of Object.entries(headersInit))
|
|
104
|
+
headers[k] = v;
|
|
105
|
+
}
|
|
106
|
+
const body = typeof init.body === 'string' ? init.body : '';
|
|
107
|
+
headers['Content-Length'] = String(Buffer.byteLength(body));
|
|
108
|
+
const req = mod.request(url, { method: init.method, headers, timeout: 10000 }, (res) => {
|
|
109
|
+
res.resume();
|
|
110
|
+
resolvePromise((res.statusCode ?? 500) < 400);
|
|
111
|
+
});
|
|
112
|
+
req.on('error', () => resolvePromise(false));
|
|
113
|
+
req.on('timeout', () => {
|
|
114
|
+
req.destroy();
|
|
115
|
+
resolvePromise(false);
|
|
116
|
+
});
|
|
117
|
+
req.end(body);
|
|
118
|
+
})();
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/** 发送通知。best-effort:成功返回 true,任何失败静默 false。 */
|
|
122
|
+
export async function sendNotification(webhookUrl, ev) {
|
|
123
|
+
if (!webhookUrl)
|
|
124
|
+
return false;
|
|
125
|
+
const spec = buildNotifyRequest(webhookUrl, ev);
|
|
126
|
+
if (!spec)
|
|
127
|
+
return false;
|
|
128
|
+
let url;
|
|
129
|
+
try {
|
|
130
|
+
url = new URL(webhookUrl);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
if (isLoopback(url.hostname))
|
|
137
|
+
return await postLoopback(url, spec.init);
|
|
138
|
+
const res = await fetch(url, { ...spec.init, signal: AbortSignal.timeout(10000) });
|
|
139
|
+
return res.ok;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 具名 Bot 命令:/bots
|
|
3
|
+
* 只读列出 bots;增删改/查看详情走 CLI `mocode bots …`,按名运行用 --bot <name>。
|
|
4
|
+
*/
|
|
5
|
+
import * as layout from '../../ui/layout.js';
|
|
6
|
+
import { ui } from '../../ui/theme.js';
|
|
7
|
+
import { listBots } from '../../bots/store.js';
|
|
8
|
+
import { unhandled, next } from './types.js';
|
|
9
|
+
function oneLine(text) {
|
|
10
|
+
const line = text.split('\n')[0] ?? '';
|
|
11
|
+
return line.length > 50 ? `${line.slice(0, 50)}…` : line;
|
|
12
|
+
}
|
|
13
|
+
export const botsCommands = [
|
|
14
|
+
(ctx) => {
|
|
15
|
+
if (ctx.line !== '/bots')
|
|
16
|
+
return unhandled();
|
|
17
|
+
const all = listBots();
|
|
18
|
+
if (all.length === 0) {
|
|
19
|
+
layout.contentWrite(`${ui.dim}(no bots; mocode bots add --name <n> --prompt "…")${ui.reset}\n`);
|
|
20
|
+
return next();
|
|
21
|
+
}
|
|
22
|
+
for (const b of all) {
|
|
23
|
+
const scopeTag = b.scope === 'global' ? `${ui.dim}g${ui.reset}` : `${ui.cyan}p${ui.reset}`;
|
|
24
|
+
layout.contentWrite(` ${scopeTag} ${ui.bold}${b.name}${ui.reset}` +
|
|
25
|
+
`${b.tools && b.tools.length ? ` ${ui.dim}tools:${b.tools.length}${ui.reset}` : ''}` +
|
|
26
|
+
` ${ui.dim}${b.description}${ui.reset}\n` +
|
|
27
|
+
` ${ui.dim}${oneLine(b.systemPrompt)}${ui.reset}\n`);
|
|
28
|
+
}
|
|
29
|
+
layout.contentWrite(`${ui.dim} run: mocode run --bot ${all[0]?.name ?? '<name>'} "task" · details: mocode bots show <name>${ui.reset}\n`);
|
|
30
|
+
return next();
|
|
31
|
+
},
|
|
32
|
+
];
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 思考强度命令组:/effort [off|low|medium|high|auto](#token-efficiency P3)
|
|
3
|
+
*
|
|
4
|
+
* 无参弹选择菜单(promptIntervention);带参直设;auto = 不下发任何 reasoning 参数。
|
|
5
|
+
* 生效链路:updateModelConfig 改内存/env + 会话 pin 同步 → writeConfigKeys 持久化 →
|
|
6
|
+
* refreshStatusBase 刷底栏。不需要 reconfigureClient(effort 在每次请求体现读)。
|
|
7
|
+
*/
|
|
8
|
+
import * as layout from '../../ui/layout.js';
|
|
9
|
+
import { ui } from '../../ui/theme.js';
|
|
10
|
+
import { config, updateModelConfig } from '../../config/index.js';
|
|
11
|
+
import { writeConfigKeys } from '../../config/file.js';
|
|
12
|
+
import { promptIntervention } from '../../ui/intervention.js';
|
|
13
|
+
import { parseReasoningEffort, recognizesReasoning } from '../../llm/reasoning.js';
|
|
14
|
+
import { unhandled, next } from './types.js';
|
|
15
|
+
const LEVELS = ['off', 'low', 'medium', 'high', 'auto'];
|
|
16
|
+
const LEVEL_LABELS = {
|
|
17
|
+
off: 'off(关闭思考)',
|
|
18
|
+
low: 'low(少思考·快·省)',
|
|
19
|
+
medium: 'medium(均衡)',
|
|
20
|
+
high: 'high(深度思考)',
|
|
21
|
+
auto: 'auto(不下发,后端默认)',
|
|
22
|
+
};
|
|
23
|
+
/** 当前目标模型是否在白名单内(未识别则给提示)。 */
|
|
24
|
+
function currentModelRecognized() {
|
|
25
|
+
return recognizesReasoning({
|
|
26
|
+
provider: config.provider === 'anthropic' ? 'anthropic' : 'openai',
|
|
27
|
+
model: config.model,
|
|
28
|
+
maxTokens: config.maxTokens,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export const effortCommands = [
|
|
32
|
+
async (ctx) => {
|
|
33
|
+
const { line } = ctx;
|
|
34
|
+
if (line !== '/effort' && !line.startsWith('/effort '))
|
|
35
|
+
return unhandled();
|
|
36
|
+
const arg = line.startsWith('/effort ') ? line.slice('/effort '.length).trim() : '';
|
|
37
|
+
let effort;
|
|
38
|
+
if (arg === '') {
|
|
39
|
+
// 无参:弹菜单,预选项为当前档。
|
|
40
|
+
let res;
|
|
41
|
+
try {
|
|
42
|
+
res = await promptIntervention({
|
|
43
|
+
type: 'choice',
|
|
44
|
+
title: '选择思考强度(reasoning effort)',
|
|
45
|
+
detail: '同档名在不同模型上语义不完全等价;未知模型/第三方网关不下发参数。' + `当前:${getActiveEffortLabel()}`,
|
|
46
|
+
options: LEVELS.map((lv) => LEVEL_LABELS[lv]),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return next(); // Ctrl+C
|
|
51
|
+
}
|
|
52
|
+
if (res.action === 'cancelled')
|
|
53
|
+
return next();
|
|
54
|
+
if (!res.value)
|
|
55
|
+
return next();
|
|
56
|
+
const idx = LEVELS.map((lv) => LEVEL_LABELS[lv]).indexOf(res.value);
|
|
57
|
+
if (idx === -1)
|
|
58
|
+
return next();
|
|
59
|
+
effort = LEVELS[idx];
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
effort = parseReasoningEffort(arg);
|
|
63
|
+
if (!effort) {
|
|
64
|
+
layout.contentWrite(`${ui.yellow}未知思考强度: ${arg};可选 off | low | medium | high | auto${ui.reset}\n`);
|
|
65
|
+
return next();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
updateModelConfig({ reasoningEffort: effort });
|
|
69
|
+
writeConfigKeys({ REASONING_EFFORT: effort });
|
|
70
|
+
ctx.refreshStatusBase(ctx.history);
|
|
71
|
+
layout.contentWrite(`${ui.dim}思考强度已设为 ${LEVEL_LABELS[effort]}${ui.reset}\n`);
|
|
72
|
+
if (effort !== 'auto' && !currentModelRecognized()) {
|
|
73
|
+
layout.contentWrite(`${ui.yellow}(当前模型未识别为支持 reasoning 参数,设置不会下发;切换到 o/gpt-5/Qwen3/GLM-4.5+/Claude 等模型后生效)${ui.reset}\n`);
|
|
74
|
+
}
|
|
75
|
+
return next();
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
/** 读当前会话 effort 的展示标签(模块内辅助)。 */
|
|
79
|
+
function getActiveEffortLabel() {
|
|
80
|
+
return LEVEL_LABELS[config.reasoningEffort];
|
|
81
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 后台任务命令组:/jobs · /jobs log [id前缀] · /jobs kill [id前缀]
|
|
3
|
+
*
|
|
4
|
+
* 不依赖 REPL 闭包状态(job 真相在 .mocode/jobs/),所以是这批命令里最独立的一组。
|
|
5
|
+
* id 可只给时间戳前缀(如 /jobs kill 20260926-1730),唯一定位即可。
|
|
6
|
+
*/
|
|
7
|
+
import * as layout from '../../ui/layout.js';
|
|
8
|
+
import { ui } from '../../ui/theme.js';
|
|
9
|
+
import { listJobs, readJobLog } from '../../jobs/store.js';
|
|
10
|
+
import { killBackgroundJob } from '../../jobs/launch.js';
|
|
11
|
+
import { unhandled, next } from './types.js';
|
|
12
|
+
const STATUS_COLOR = {
|
|
13
|
+
paused: ui.cyan,
|
|
14
|
+
running: ui.yellow,
|
|
15
|
+
succeeded: ui.green,
|
|
16
|
+
failed: ui.red,
|
|
17
|
+
killed: ui.dim,
|
|
18
|
+
};
|
|
19
|
+
function findJob(idPrefix) {
|
|
20
|
+
const jobs = listJobs();
|
|
21
|
+
if (!idPrefix)
|
|
22
|
+
return jobs[0];
|
|
23
|
+
const matches = jobs.filter((j) => j.id.startsWith(idPrefix));
|
|
24
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
25
|
+
}
|
|
26
|
+
function oneLinePrompt(prompt) {
|
|
27
|
+
const line = prompt.split('\n')[0] ?? '';
|
|
28
|
+
return line.length > 50 ? `${line.slice(0, 50)}…` : line;
|
|
29
|
+
}
|
|
30
|
+
function renderList() {
|
|
31
|
+
const jobs = listJobs().slice(0, 10);
|
|
32
|
+
if (jobs.length === 0) {
|
|
33
|
+
layout.contentWrite(`${ui.dim}(没有后台任务。用 mocode run --bg "任务" 启动)${ui.reset}\n`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
for (const job of jobs) {
|
|
37
|
+
const color = STATUS_COLOR[job.status];
|
|
38
|
+
layout.contentWrite(` ${color}●${ui.reset} ${ui.bold}${job.id}${ui.reset} ${color}${job.status}${ui.reset}\n` +
|
|
39
|
+
` ${ui.dim}${oneLinePrompt(job.prompt)}${ui.reset}\n`);
|
|
40
|
+
}
|
|
41
|
+
layout.contentWrite(`${ui.dim} /jobs log [id前缀] 看尾日志 · /jobs kill [id前缀] 杀任务${ui.reset}\n`);
|
|
42
|
+
}
|
|
43
|
+
export const jobsCommands = [
|
|
44
|
+
// /jobs approve|deny [id前缀]
|
|
45
|
+
async (ctx) => {
|
|
46
|
+
if (ctx.cmd !== '/jobs')
|
|
47
|
+
return unhandled();
|
|
48
|
+
const parts = ctx.line.split(/\s+/);
|
|
49
|
+
if (parts[1] !== 'approve' && parts[1] !== 'deny')
|
|
50
|
+
return unhandled();
|
|
51
|
+
const job = findJob(parts[2]);
|
|
52
|
+
if (!job) {
|
|
53
|
+
layout.contentWrite(`${ui.yellow}(没找到唯一匹配的任务)${ui.reset}\n`);
|
|
54
|
+
return next();
|
|
55
|
+
}
|
|
56
|
+
if (job.status !== 'paused') {
|
|
57
|
+
layout.contentWrite(`${ui.dim}任务 ${job.id} 状态 ${job.status},无需审批${ui.reset}\n`);
|
|
58
|
+
return next();
|
|
59
|
+
}
|
|
60
|
+
const { runApproveCli } = await import('../../jobs/approve-cli.js');
|
|
61
|
+
runApproveCli([job.id], parts[1] === 'approve' ? 'approved' : 'denied');
|
|
62
|
+
return next();
|
|
63
|
+
},
|
|
64
|
+
// /jobs kill [id前缀]
|
|
65
|
+
(ctx) => {
|
|
66
|
+
if (ctx.cmd !== '/jobs')
|
|
67
|
+
return unhandled();
|
|
68
|
+
const parts = ctx.line.split(/\s+/);
|
|
69
|
+
if (parts[1] !== 'kill')
|
|
70
|
+
return unhandled();
|
|
71
|
+
const job = findJob(parts[2]);
|
|
72
|
+
if (!job) {
|
|
73
|
+
layout.contentWrite(`${ui.yellow}(没找到唯一匹配的任务)${ui.reset}\n`);
|
|
74
|
+
return next();
|
|
75
|
+
}
|
|
76
|
+
if (job.status !== 'running' && job.status !== 'paused') {
|
|
77
|
+
layout.contentWrite(`${ui.dim}任务 ${job.id} 已是 ${job.status},无需 kill${ui.reset}\n`);
|
|
78
|
+
return next();
|
|
79
|
+
}
|
|
80
|
+
const ok = killBackgroundJob(job);
|
|
81
|
+
layout.contentWrite(ok
|
|
82
|
+
? `${ui.yellow}已 kill 任务 ${job.id}${ui.reset}\n`
|
|
83
|
+
: `${ui.red}kill 失败(进程可能已退出;PID=${job.pid ?? '?'})${ui.reset}\n`);
|
|
84
|
+
return next();
|
|
85
|
+
},
|
|
86
|
+
// /jobs log [id前缀]
|
|
87
|
+
(ctx) => {
|
|
88
|
+
if (ctx.cmd !== '/jobs')
|
|
89
|
+
return unhandled();
|
|
90
|
+
const parts = ctx.line.split(/\s+/);
|
|
91
|
+
if (parts[1] !== 'log')
|
|
92
|
+
return unhandled();
|
|
93
|
+
const job = findJob(parts[2]);
|
|
94
|
+
if (!job) {
|
|
95
|
+
layout.contentWrite(`${ui.yellow}(没找到唯一匹配的任务)${ui.reset}\n`);
|
|
96
|
+
return next();
|
|
97
|
+
}
|
|
98
|
+
layout.contentWrite(`${ui.bold}── log ${job.id} ──${ui.reset}\n`);
|
|
99
|
+
layout.contentWrite(`${readJobLog(job.id)}\n`);
|
|
100
|
+
return next();
|
|
101
|
+
},
|
|
102
|
+
// /jobs(列表)
|
|
103
|
+
(ctx) => {
|
|
104
|
+
if (ctx.line !== '/jobs')
|
|
105
|
+
return unhandled();
|
|
106
|
+
renderList();
|
|
107
|
+
return next();
|
|
108
|
+
},
|
|
109
|
+
];
|