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.
Files changed (91) hide show
  1. package/README.md +382 -365
  2. package/README.zh-CN.md +19 -2
  3. package/dist/agent/model-turn.js +14 -1
  4. package/dist/agent/run-coordinator.js +7 -1
  5. package/dist/agent/spawn.js +38 -5
  6. package/dist/agent/stages/model-runner.js +1 -1
  7. package/dist/agent/stages/tool-dispatcher.js +1 -0
  8. package/dist/agent/tool-turn.js +2 -0
  9. package/dist/agent/work-discipline.js +3 -1
  10. package/dist/bots/bus.js +117 -0
  11. package/dist/bots/cli.js +102 -0
  12. package/dist/bots/store.js +100 -0
  13. package/dist/config/index.js +53 -2
  14. package/dist/config/presets.js +40 -1
  15. package/dist/config/profiles.js +9 -1
  16. package/dist/context/clearing.js +53 -0
  17. package/dist/context/relevance.js +6 -0
  18. package/dist/context/text-search.js +113 -0
  19. package/dist/headless.js +302 -0
  20. package/dist/index.js +171 -3
  21. package/dist/jobs/approval.js +153 -0
  22. package/dist/jobs/approve-cli.js +30 -0
  23. package/dist/jobs/attach.js +61 -0
  24. package/dist/jobs/checkpoint.js +33 -0
  25. package/dist/jobs/launch.js +107 -0
  26. package/dist/jobs/runner.js +59 -0
  27. package/dist/jobs/store.js +101 -0
  28. package/dist/jobs/worktree.js +63 -0
  29. package/dist/llm/index.js +35 -8
  30. package/dist/llm/providers/anthropic.js +14 -4
  31. package/dist/llm/reasoning.js +141 -0
  32. package/dist/memory/reflect.js +1 -1
  33. package/dist/memory/store.js +31 -28
  34. package/dist/models/budgets.js +11 -0
  35. package/dist/models/catalog.js +130 -0
  36. package/dist/models/map-preset.js +50 -0
  37. package/dist/models/model-panel.js +227 -0
  38. package/dist/models/protocol.js +44 -0
  39. package/dist/models/reasoning-cap.js +88 -0
  40. package/dist/models/search.js +58 -0
  41. package/dist/models/types.js +8 -0
  42. package/dist/notify/index.js +144 -0
  43. package/dist/repl/commands/bots.js +32 -0
  44. package/dist/repl/commands/effort.js +81 -0
  45. package/dist/repl/commands/jobs.js +109 -0
  46. package/dist/repl/commands/model-actions.js +138 -0
  47. package/dist/repl/commands/model-catalog.js +67 -0
  48. package/dist/repl/commands/model-panel-ui.js +107 -0
  49. package/dist/repl/commands/model.js +22 -20
  50. package/dist/repl/commands/registry.js +12 -0
  51. package/dist/repl/commands/schedule.js +37 -0
  52. package/dist/repl/commands/session.js +54 -0
  53. package/dist/repl/commands/stats.js +28 -0
  54. package/dist/repl/commands.js +6 -58
  55. package/dist/repl/runtime.js +5 -1
  56. package/dist/repl/status-bar.js +4 -2
  57. package/dist/rollback/store.js +17 -0
  58. package/dist/runtime/runtime.js +31 -1
  59. package/dist/schedule/cli.js +114 -0
  60. package/dist/schedule/cron.js +79 -0
  61. package/dist/schedule/daemon.js +166 -0
  62. package/dist/schedule/store.js +98 -0
  63. package/dist/session/compact.js +84 -3
  64. package/dist/session/index.js +2 -0
  65. package/dist/session/retention.js +297 -0
  66. package/dist/session/scheduler.js +30 -3
  67. package/dist/session/store.js +108 -6
  68. package/dist/session/usage-stats.js +90 -0
  69. package/dist/skills/activation.js +2 -0
  70. package/dist/skills/builtin-skills.js +153 -0
  71. package/dist/skills/discover.js +3 -1
  72. package/dist/skills/runner.js +4 -1
  73. package/dist/tools/builtins/arena.js +156 -0
  74. package/dist/tools/builtins/index.js +8 -0
  75. package/dist/tools/builtins/message-bus.js +98 -0
  76. package/dist/tools/builtins/read-file.js +19 -1
  77. package/dist/tools/builtins/run-command.js +2 -1
  78. package/dist/tools/builtins/session-search.js +33 -0
  79. package/dist/tools/builtins/task.js +2 -0
  80. package/dist/tools/constants.js +8 -0
  81. package/dist/tools/read-dedup.js +62 -0
  82. package/dist/tools/tool-runtime.js +1 -0
  83. package/dist/ui/batch.js +140 -104
  84. package/dist/ui/composer.js +4 -18
  85. package/dist/ui/fuzzy-picker.js +227 -0
  86. package/dist/ui/hierarchical-picker.js +302 -0
  87. package/dist/ui/intervention.js +17 -1
  88. package/dist/ui/layout-internal/statusbar.js +18 -7
  89. package/dist/ui/prompt-internal/editor.js +10 -27
  90. package/dist/ui/render.js +2 -0
  91. package/package.json +1 -1
@@ -0,0 +1,138 @@
1
+ /**
2
+ * 模型命令的共享动作(#模型命令交互):应用预设、添加目录模型、刷新目录。
3
+ *
4
+ * 从 model-catalog.ts 抽出,供「统一模型面板」与旧 /model browse 复用,避免第三处重复。
5
+ * 纯编排:UI 走 promptIntervention,落盘走 presets/file,运行时切 config。
6
+ */
7
+ import * as layout from '../../ui/layout.js';
8
+ import { ui } from '../../ui/theme.js';
9
+ import { bannerLines } from '../../ui/render.js';
10
+ import { config, updateModelConfig } from '../../config/index.js';
11
+ import { writeConfigKeys } from '../../config/file.js';
12
+ import { savePreset, setActivePresetName } from '../../config/presets.js';
13
+ import { reconfigureClient } from '../../llm/index.js';
14
+ import { promptIntervention } from '../../ui/intervention.js';
15
+ import { renderHistory } from '../message-format.js';
16
+ import { refreshCatalog } from '../../models/catalog.js';
17
+ import { buildPreset, defaultPresetName, resolveApiKey } from '../../models/map-preset.js';
18
+ /** 应用预设到 config + 落 config 文件 + 重建 client + 重显。 */
19
+ export function applyPresetConfig(ctx, p) {
20
+ updateModelConfig({
21
+ provider: p.provider,
22
+ model: p.model,
23
+ baseURL: p.baseURL,
24
+ apiKey: p.apiKey,
25
+ contextWindowTokens: p.contextWindow,
26
+ anthropicPromptCache: p.anthropicPromptCache,
27
+ ...(p.reasoningEffort ? { reasoningEffort: p.reasoningEffort } : {}),
28
+ });
29
+ writeConfigKeys({
30
+ LLM_PROVIDER: p.provider,
31
+ LLM_BASE_URL: p.baseURL,
32
+ LLM_API_KEY: p.apiKey,
33
+ LLM_MODEL: p.model,
34
+ CONTEXT_WINDOW_TOKENS: String(p.contextWindow),
35
+ ANTHROPIC_PROMPT_CACHE: p.anthropicPromptCache ? 'true' : 'false',
36
+ });
37
+ reconfigureClient();
38
+ ctx.refreshStatusBase(ctx.history);
39
+ layout.clearContent();
40
+ if (ctx.history.some((m) => m.role === 'user'))
41
+ renderHistory(ctx.history);
42
+ else
43
+ layout.writeBanner(bannerLines(ctx.banner()));
44
+ }
45
+ /**
46
+ * 把一个目录模型添加为预设并激活。
47
+ * @returns 'added' 已切换;'cancelled' 用户中途取消。
48
+ */
49
+ export async function addCatalogModel(ctx, entry, providers) {
50
+ const catalogProvider = providers[entry.providerId];
51
+ if (!catalogProvider)
52
+ return 'cancelled';
53
+ // key:无论环境变量是否找到,都弹一次确认——回车沿用预填值、粘贴即覆盖。
54
+ const envName = catalogProvider.env?.[0] ?? 'API_KEY';
55
+ const detectedKey = resolveApiKey(catalogProvider);
56
+ const kres = await promptIntervention({
57
+ type: 'input',
58
+ title: `${envName}`,
59
+ detail: detectedKey
60
+ ? `已从环境变量 ${envName} 预填 key;回车沿用,或粘贴新 key 覆盖(仅存入本预设与配置文件)。`
61
+ : `未在环境变量 ${envName} 找到 key;粘贴该厂商 key(仅存入本预设与配置文件)。`,
62
+ seed: detectedKey ?? '',
63
+ });
64
+ if (kres.action === 'cancelled')
65
+ return 'cancelled';
66
+ const apiKey = (kres.value ?? '').trim();
67
+ if (!apiKey) {
68
+ layout.contentWrite(`${ui.yellow}未提供 key,已取消。${ui.reset}\n`);
69
+ return 'cancelled';
70
+ }
71
+ const built = buildPreset({ provider: catalogProvider, model: entry.model, apiKey });
72
+ if (!built) {
73
+ layout.contentWrite(`${ui.yellow}该模型当前协议不支持直发。${ui.reset}\n`);
74
+ return 'cancelled';
75
+ }
76
+ const nres = await promptIntervention({
77
+ type: 'input',
78
+ title: '预设名',
79
+ detail: '回车用默认名;可改名。',
80
+ seed: defaultPresetName(entry.providerId, entry.modelId),
81
+ });
82
+ if (nres.action === 'cancelled')
83
+ return 'cancelled';
84
+ built.name = (nres.value ?? '').trim() || built.name;
85
+ const mres = await promptIntervention({
86
+ type: 'input',
87
+ title: '下发模型名',
88
+ detail: `端点 ${built.baseURL}\n一般保持默认;厂商要求专用 ID(如火山 endpoint id)时在此修改。`,
89
+ seed: built.model,
90
+ });
91
+ if (mres.action === 'cancelled')
92
+ return 'cancelled';
93
+ built.model = (mres.value ?? '').trim() || built.model;
94
+ const preset = {
95
+ ...built,
96
+ catalogProvider: entry.providerId,
97
+ catalogModel: entry.modelId,
98
+ capabilities: {
99
+ ...(typeof entry.model.reasoning === 'boolean' ? { reasoning: entry.model.reasoning } : {}),
100
+ ...(entry.model.reasoning_options ? { reasoningOptions: entry.model.reasoning_options } : {}),
101
+ ...(typeof entry.model.tool_call === 'boolean' ? { toolCall: entry.model.tool_call } : {}),
102
+ ...(typeof entry.model.attachment === 'boolean' ? { attachment: entry.model.attachment } : {}),
103
+ },
104
+ ...(entry.model.cost
105
+ ? {
106
+ pricing: {
107
+ ...(typeof entry.model.cost.input === 'number' ? { input: entry.model.cost.input } : {}),
108
+ ...(typeof entry.model.cost.output === 'number' ? { output: entry.model.cost.output } : {}),
109
+ ...(typeof entry.model.cost.cache_read === 'number' ? { cacheRead: entry.model.cost.cache_read } : {}),
110
+ },
111
+ }
112
+ : {}),
113
+ };
114
+ savePreset(preset);
115
+ try {
116
+ setActivePresetName(preset.name);
117
+ }
118
+ catch {
119
+ /* 指针失败不阻断 */
120
+ }
121
+ applyPresetConfig(ctx, preset);
122
+ layout.contentWrite(`${ui.dim}(已添加并切换 → ${preset.model} · ${preset.provider} @ ${preset.baseURL})${ui.reset}\n`);
123
+ layout.contentWrite(`${ui.dim}(已存为预设 “${preset.name}”)${ui.reset}\n`);
124
+ return 'added';
125
+ }
126
+ /** 强制刷新目录并打印统计。 */
127
+ export async function refreshAndReport() {
128
+ const r = await refreshCatalog();
129
+ const n = Object.values(r.providers).reduce((acc, p) => acc + Object.keys(p.models ?? {}).length, 0);
130
+ if (r.source === 'empty') {
131
+ layout.contentWrite(`${ui.yellow}刷新失败且无本地快照。${ui.reset}\n`);
132
+ }
133
+ else {
134
+ const tag = r.source === 'live' ? '已更新' : '离线,使用快照';
135
+ layout.contentWrite(`${ui.dim}模型目录${tag}:${Object.keys(r.providers).length} 厂商 / ${n} 模型(${r.fetchedAt?.slice(0, 10)})${ui.reset}\n`);
136
+ }
137
+ }
138
+ export { config };
@@ -0,0 +1,67 @@
1
+ /**
2
+ * 模型目录命令(#model-catalog,兼容入口):/model browse · /model catalog [refresh]
3
+ *
4
+ * 交互逻辑已抽到 model-actions.ts,统一模型面板(裸 /model)与这里共用同一套动作。
5
+ * 本文件只保留命令路由与目录无参查看。
6
+ */
7
+ import * as layout from '../../ui/layout.js';
8
+ import { ui } from '../../ui/theme.js';
9
+ import { promptIntervention } from '../../ui/intervention.js';
10
+ import { loadCatalog } from '../../models/catalog.js';
11
+ import { searchModels } from '../../models/search.js';
12
+ import { addCatalogModel, refreshAndReport } from './model-actions.js';
13
+ import { unhandled, next } from './types.js';
14
+ /** 一次 browse 列表上限(统一面板可滚动更多;此兼容入口保持收窄)。 */
15
+ const MAX_BROWSE = 30;
16
+ export const modelCatalogCommands = [
17
+ async (ctx) => {
18
+ const { line } = ctx;
19
+ if (line === '/model browse' || line.startsWith('/model browse ')) {
20
+ const cat = await loadCatalog();
21
+ if (cat.source === 'empty' || Object.keys(cat.providers).length === 0) {
22
+ layout.contentWrite(`${ui.yellow}暂无模型目录且离线:联网后 /model catalog refresh,或用 /model 配置。${ui.reset}\n`);
23
+ return next();
24
+ }
25
+ // 兼容 browse:先输入关键字,再从收窄结果里选(添加动作与统一面板一致)。
26
+ const qres = await promptIntervention({
27
+ type: 'input',
28
+ title: '搜索模型',
29
+ detail: '输入厂商/模型关键字(glm、qwen、claude、doubao…),回车列出。',
30
+ seed: '',
31
+ });
32
+ if (qres.action === 'cancelled')
33
+ return next();
34
+ const query = (qres.value ?? '').trim();
35
+ const results = searchModels(cat.providers, { query }).slice(0, MAX_BROWSE);
36
+ if (results.length === 0) {
37
+ layout.contentWrite(`${ui.yellow}没有匹配 “${query}” 的可直发模型。${ui.reset}\n`);
38
+ return next();
39
+ }
40
+ const sel = await promptIntervention({
41
+ type: 'choice',
42
+ title: `选择模型(${results.length})`,
43
+ options: results.map((e) => e.model.name || e.modelId),
44
+ allowCustom: false,
45
+ });
46
+ if (sel.action === 'cancelled')
47
+ return next();
48
+ const picked = results.find((e) => (e.model.name || e.modelId) === sel.value);
49
+ if (picked)
50
+ await addCatalogModel(ctx, picked, cat.providers);
51
+ return next();
52
+ }
53
+ if (line === '/model catalog' || line === '/model catalog refresh') {
54
+ if (line === '/model catalog refresh')
55
+ await refreshAndReport();
56
+ else {
57
+ const cat = await loadCatalog();
58
+ const n = Object.values(cat.providers).reduce((acc, p) => acc + Object.keys(p.models ?? {}).length, 0);
59
+ layout.contentWrite(cat.source === 'empty'
60
+ ? `${ui.yellow}本地无目录:/model catalog refresh 拉取。${ui.reset}\n`
61
+ : `${ui.dim}目录(${cat.source === 'live' ? '在线' : '快照'}):${Object.keys(cat.providers).length} 厂商 / ${n} 模型(${cat.fetchedAt?.slice(0, 10)})${ui.reset}\n`);
62
+ }
63
+ return next();
64
+ }
65
+ return unhandled();
66
+ },
67
+ ];
@@ -0,0 +1,107 @@
1
+ /**
2
+ * 统一模型面板的交互编排(#模型命令交互):openModelPanel。
3
+ *
4
+ * 两级浏览:先列厂商,进入某厂商后再列其下模型/预设;搜索栏为作用域内过滤——
5
+ * 顶层只按厂商名、厂商内只按模型名,不跨层混搜。思考强度统一在 /effort 设置,
6
+ * 不放进模型面板。动作复用 model-actions.ts。
7
+ */
8
+ import * as layout from '../../ui/layout.js';
9
+ import { ui } from '../../ui/theme.js';
10
+ import { promptHierarchicalPicker } from '../../ui/hierarchical-picker.js';
11
+ import { promptIntervention } from '../../ui/intervention.js';
12
+ import { loadCatalog } from '../../models/catalog.js';
13
+ import { flattenCatalog } from '../../models/search.js';
14
+ import { buildModelTree } from '../../models/model-panel.js';
15
+ import { config } from '../../config/index.js';
16
+ import { savePreset } from '../../config/presets.js';
17
+ import { applyPresetConfig, addCatalogModel, refreshAndReport } from './model-actions.js';
18
+ /**
19
+ * 选中已存预设时确认 API key:回车沿用当前 key;粘贴新值则更新并写回预设文件。
20
+ * 修复「预设里存着失效 key、重选同一模型又静默套用 → 401 死循环」——套用任何预设前
21
+ * 都给一次改 key 的机会。取消返回 null。
22
+ */
23
+ async function confirmPresetKey(preset) {
24
+ const res = await promptIntervention({
25
+ type: 'input',
26
+ title: 'API Key',
27
+ detail: `端点:${preset.baseURL}\n` + `回车沿用当前 key;认证失败时粘贴新 key(会一并写回预设 “${preset.name}”)。`,
28
+ seed: preset.apiKey,
29
+ });
30
+ if (res.action === 'cancelled')
31
+ return null;
32
+ const v = (res.value ?? '').trim();
33
+ if (!v || v === preset.apiKey)
34
+ return preset;
35
+ const updated = { ...preset, apiKey: v };
36
+ savePreset(updated);
37
+ return updated;
38
+ }
39
+ /** 域模型树 → 层级选择器节点(叶子 value 为 PanelChoice)。 */
40
+ function toHPNode(node) {
41
+ const base = {
42
+ searchText: node.searchText,
43
+ label: node.label,
44
+ detail: node.detail,
45
+ };
46
+ if (node.children && node.children.length) {
47
+ base.children = node.children.map(toHPNode);
48
+ }
49
+ else if (node.choice) {
50
+ base.value = node.choice;
51
+ }
52
+ return base;
53
+ }
54
+ /**
55
+ * 打开统一模型面板。
56
+ * @returns 'custom' 表示用户选了「手动填写」,调用方回落到手填向导;其余均已处理。
57
+ */
58
+ export async function openModelPanel(ctx) {
59
+ // 目录:新鲜快照不联网;离线/无快照仍能列已存预设与操作行。
60
+ const cat = await loadCatalog();
61
+ const catalog = flattenCatalog(cat.providers);
62
+ const tree = buildModelTree({
63
+ presets: listAllPresets(),
64
+ providers: cat.providers,
65
+ catalog,
66
+ current: {
67
+ provider: config.provider,
68
+ baseURL: config.baseURL,
69
+ model: config.model,
70
+ contextWindowTokens: config.contextWindowTokens,
71
+ },
72
+ });
73
+ const srcTag = cat.source === 'empty' ? '无目录' : cat.source === 'live' ? '在线目录' : '目录快照';
74
+ const picked = await promptHierarchicalPicker({
75
+ title: `选择模型(${srcTag})`,
76
+ roots: tree.map(toHPNode),
77
+ maxVisible: 12,
78
+ });
79
+ if (!picked)
80
+ return 'handled'; // Esc:什么都不做
81
+ switch (picked.kind) {
82
+ case 'preset': {
83
+ const confirmed = await confirmPresetKey(picked.preset);
84
+ if (!confirmed)
85
+ return 'handled';
86
+ applyPresetConfig(ctx, confirmed);
87
+ layout.contentWrite(`${ui.dim}(已切换 → ${confirmed.model})${ui.reset}\n`);
88
+ return 'handled';
89
+ }
90
+ case 'catalog':
91
+ await addCatalogModel(ctx, picked.entry, cat.providers);
92
+ return 'handled';
93
+ case 'action':
94
+ if (picked.action === 'custom')
95
+ return 'custom';
96
+ if (picked.action === 'refresh') {
97
+ await refreshAndReport();
98
+ return 'handled';
99
+ }
100
+ return 'handled';
101
+ }
102
+ }
103
+ // 延迟取预设列表,避免在模块顶部引入多余耦合。
104
+ import { listPresets } from '../../config/presets.js';
105
+ function listAllPresets() {
106
+ return listPresets();
107
+ }
@@ -19,6 +19,7 @@ import { config, updateModelConfig } from '../../config/index.js';
19
19
  import { writeConfigKeys, CONFIG_PATH } from '../../config/file.js';
20
20
  import { deletePreset, getPreset, isValidPresetName, listPresets, savePreset, setActivePresetName, } from '../../config/presets.js';
21
21
  import { reconfigureClient } from '../../llm/index.js';
22
+ import { recognizesReasoning } from '../../llm/reasoning.js';
22
23
  import { promptIntervention } from '../../ui/intervention.js';
23
24
  import { renderHistory } from '../message-format.js';
24
25
  import { MODEL_PRESETS, maskKey } from '../commands.js';
@@ -57,6 +58,13 @@ export const modelCommands = [
57
58
  return unhandled();
58
59
  const arg = line.startsWith('/model ') ? line.slice('/model '.length).trim() : '';
59
60
  const { history } = ctx;
61
+ // 裸 /model → 统一模型面板(打字过滤+预设+目录+思考强度)。选「自定义」才落到下方手填向导。
62
+ if (arg === '') {
63
+ const { openModelPanel } = await import('./model-panel-ui.js');
64
+ const outcome = await openModelPanel(ctx);
65
+ if (outcome !== 'custom')
66
+ return next();
67
+ }
60
68
  // 共用:apply 一个预设到 config + 持久化 + 重建 client + 重显横幅。无参 /model 选菜单和 /model use 都走这里。
61
69
  const applyPresetAndPersist = (target) => {
62
70
  updateModelConfig({
@@ -66,6 +74,7 @@ export const modelCommands = [
66
74
  apiKey: target.apiKey,
67
75
  contextWindowTokens: target.contextWindow,
68
76
  anthropicPromptCache: target.anthropicPromptCache,
77
+ ...(target.reasoningEffort ? { reasoningEffort: target.reasoningEffort } : {}),
69
78
  });
70
79
  writeConfigKeys({
71
80
  LLM_PROVIDER: target.provider,
@@ -74,6 +83,7 @@ export const modelCommands = [
74
83
  LLM_MODEL: target.model,
75
84
  CONTEXT_WINDOW_TOKENS: String(target.contextWindow),
76
85
  ANTHROPIC_PROMPT_CACHE: target.anthropicPromptCache ? 'true' : 'false',
86
+ ...(target.reasoningEffort ? { REASONING_EFFORT: target.reasoningEffort } : {}),
77
87
  });
78
88
  reconfigureClient();
79
89
  // 记为激活预设:让上下文窗口等配置从此跟随该预设文件(下次启动也用它,不再回退 config 裸键)。
@@ -167,6 +177,13 @@ export const modelCommands = [
167
177
  layout.contentWrite(` ${ui.accent}baseURL ${ui.reset} ${config.baseURL}\n`);
168
178
  layout.contentWrite(` ${ui.accent}apiKey ${ui.reset} ${maskKey(config.apiKey)}\n`);
169
179
  layout.contentWrite(` ${ui.accent}model ${ui.reset} ${config.model}\n`);
180
+ const recognized = recognizesReasoning({
181
+ provider: config.provider === 'anthropic' ? 'anthropic' : 'openai',
182
+ model: config.model,
183
+ maxTokens: config.maxTokens,
184
+ });
185
+ layout.contentWrite(` ${ui.accent}effort ${ui.reset} ${config.reasoningEffort}` +
186
+ (recognized ? '\n' : `${ui.dim}(当前模型未识别,参数不会下发)${ui.reset}\n`));
170
187
  layout.contentWrite(` ${ui.accent}窗口 ${ui.reset} ${config.contextWindowTokens} tokens\n`);
171
188
  if (config.provider === 'anthropic') {
172
189
  layout.contentWrite(` ${ui.accent}缓存 ${ui.reset} Prompt Cache ${config.anthropicPromptCache ? 'on' : 'off'}\n`);
@@ -229,26 +246,11 @@ export const modelCommands = [
229
246
  }
230
247
  // /model 无参 → 直接进入「配置新模型」4 步向导(不弹动作菜单)。
231
248
  // 切换 / 查看 / 删除已配置预设改用显式子命令:/model switch · /model list · /model delete <name>。
232
- // 1) 选 provider 预设(预填 baseURL,后续仍可逐项改)。
233
- let preset;
234
- try {
235
- const res = await promptIntervention({
236
- type: 'choice',
237
- title: '选择后端预设(预填 baseURL,后续可改)',
238
- detail: '选一个会预填 baseURL/model/窗口,之后逐项确认。选「自定义」全部手填。',
239
- options: MODEL_PRESETS.map((p) => p.label),
240
- });
241
- if (res.action === 'cancelled') {
242
- return next();
243
- }
244
- const idx = MODEL_PRESETS.findIndex((p) => p.label === res.value);
245
- if (idx === -1) {
246
- return next();
247
- }
248
- preset = MODEL_PRESETS[idx];
249
- }
250
- catch {
251
- return next(); // Ctrl+C
249
+ // 1) 后端预设:本地/远程都可由一个 OpenAI 兼容端点表达,故只保留「自定义 base_url」
250
+ // 一个入口;既然只有一个预设,不再弹选择菜单,直接进入字段填写。
251
+ const preset = MODEL_PRESETS[0];
252
+ if (!preset) {
253
+ return next();
252
254
  }
253
255
  // 1.5) 一键应用确认:非「自定义」预设(带预填值)给直接应用入口,免连按 4 次回车。
254
256
  // 直接应用 = 用预设 model/baseURL/window + 保留当前 apiKey(等价于下方逐项链连按回车)。
@@ -5,14 +5,20 @@ import { modeCommands } from './mode.js';
5
5
  import { petCommands } from './pet.js';
6
6
  import { imageCommands } from './image.js';
7
7
  import { contextCommands } from './context.js';
8
+ import { statsCommands } from './stats.js';
8
9
  import { sessionCommands } from './session.js';
9
10
  import { memoryCommands } from './memory.js';
10
11
  import { skillCommands } from './skill.js';
11
12
  import { compactCommands } from './compact.js';
12
13
  import { appearanceCommands } from './appearance.js';
13
14
  import { modelCommands } from './model.js';
15
+ import { modelCatalogCommands } from './model-catalog.js';
16
+ import { effortCommands } from './effort.js';
14
17
  import { routerCommands } from './router.js';
15
18
  import { toolGroupCommands } from './tool-group.js';
19
+ import { jobsCommands } from './jobs.js';
20
+ import { scheduleCommands } from './schedule.js';
21
+ import { botsCommands } from './bots.js';
16
22
  /**
17
23
  * 命令注册表。顺序即匹配优先级 —— 更具体的分支放前面。
18
24
  * 组内顺序由各文件自己保证(见文件头注释);组间顺序互不影响(条件互斥)。
@@ -26,14 +32,20 @@ export const commandHandlers = [
26
32
  ...petCommands,
27
33
  ...imageCommands,
28
34
  ...contextCommands,
35
+ ...statsCommands,
29
36
  ...sessionCommands,
30
37
  ...memoryCommands,
31
38
  ...skillCommands,
32
39
  ...compactCommands,
33
40
  ...appearanceCommands,
41
+ ...effortCommands,
42
+ ...modelCatalogCommands,
34
43
  ...modelCommands,
35
44
  ...routerCommands,
36
45
  ...toolGroupCommands,
46
+ ...jobsCommands,
47
+ ...scheduleCommands,
48
+ ...botsCommands,
37
49
  ];
38
50
  /**
39
51
  * 依次询问 handler,返回第一个非 unhandled 的结果。
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 调度计划命令:/schedules
3
+ * 只读列出 schedules + 守护进程状态;增删改/启停走 CLI `mocode schedule …`。
4
+ */
5
+ import * as layout from '../../ui/layout.js';
6
+ import { ui } from '../../ui/theme.js';
7
+ import { listSchedules } from '../../schedule/store.js';
8
+ import { isDaemonRunning, getDaemonState } from '../../schedule/daemon.js';
9
+ import { unhandled, next } from './types.js';
10
+ function oneLine(prompt) {
11
+ const line = prompt.split('\n')[0] ?? '';
12
+ return line.length > 50 ? `${line.slice(0, 50)}…` : line;
13
+ }
14
+ export const scheduleCommands = [
15
+ (ctx) => {
16
+ if (ctx.line !== '/schedules')
17
+ return unhandled();
18
+ const daemonState = getDaemonState();
19
+ const daemon = isDaemonRunning() && daemonState;
20
+ layout.contentWrite(daemon
21
+ ? `${ui.dim}scheduler:${ui.reset} ${ui.green}running${ui.reset} ` +
22
+ `${ui.dim}(pid ${daemonState?.pid}, port ${daemonState?.port})${ui.reset}\n`
23
+ : `${ui.dim}scheduler:${ui.reset} ${ui.yellow}stopped${ui.reset} ` +
24
+ `${ui.dim}(mocode schedule start)${ui.reset}\n`);
25
+ const all = listSchedules();
26
+ if (all.length === 0) {
27
+ layout.contentWrite(`${ui.dim}(no schedules; mocode schedule add …)${ui.reset}\n`);
28
+ return next();
29
+ }
30
+ for (const s of all) {
31
+ const trigger = s.cron ? s.cron : s.webhookToken ? `webhook:${s.webhookToken}` : '?';
32
+ layout.contentWrite(` ${s.enabled ? ui.green + '●' : ui.dim + '○'}${ui.reset} ${ui.bold}${s.name}${ui.reset} ` +
33
+ `${ui.dim}${trigger}${ui.reset}\n ${ui.dim}${oneLine(s.prompt)}${ui.reset}\n`);
34
+ }
35
+ return next();
36
+ },
37
+ ];
@@ -84,6 +84,60 @@ export const sessionCommands = [
84
84
  await ctx.resumeFromPick(pick);
85
85
  return next();
86
86
  },
87
+ // /session gc [--yes]:老会话垃圾回收。30 天未活动→gzip 归档(archive/),90 天→清正文。
88
+ // 默认(及 --dry-run)只列计划不写盘;--yes 才执行——删除不可逆,用显式参数而非交互确认。
89
+ async (ctx) => {
90
+ const m = /^\/sessions?\s+gc\b(.*)$/.exec(ctx.line);
91
+ if (!m)
92
+ return unhandled();
93
+ const args = (m[1] ?? '').trim();
94
+ const execute = args === '--yes';
95
+ if (args && args !== '--yes' && args !== '--dry-run') {
96
+ layout.contentWrite(`${ui.yellow}用法: /session gc [--dry-run|--yes]${ui.reset}\n`);
97
+ return next();
98
+ }
99
+ const fmtKb = (n) => `${(n / 1024).toFixed(0)}KB`;
100
+ if (execute) {
101
+ const result = await ctx.runtime.session.gc({ dryRun: false });
102
+ layout.contentWrite(`${ui.accent}已归档 ${result.archived}、清除 ${result.purged},回收 ${fmtKb(result.bytesReclaimed)}${ui.reset}\n`);
103
+ return next();
104
+ }
105
+ const plan = ctx.runtime.session.gc({ dryRun: true });
106
+ if (plan.items.length === 0) {
107
+ layout.contentWrite(`${ui.dim}(没有需要归档或清除的会话)${ui.reset}\n`);
108
+ return next();
109
+ }
110
+ for (const it of plan.items) {
111
+ const label = it.action === 'archive' ? `${ui.cyan}归档` : `${ui.yellow}清除`;
112
+ layout.contentWrite(` ${label}${ui.reset} ${it.id} ${it.ageDays.toFixed(0)}天 ${fmtKb(it.sizeBytes)} ${it.firstUser}\n`);
113
+ }
114
+ layout.contentWrite(` ${ui.dim}共 ${plan.items.length} 项,可回收 ${fmtKb(plan.bytesReclaimable)}${ui.reset}\n`);
115
+ layout.contentWrite(`${ui.dim}(dry-run;加 --yes 执行,归档/清除不可逆)${ui.reset}\n`);
116
+ return next();
117
+ },
118
+ // /ssearch <关键词>:跨会话搜索归档(episodic),命中显示 id/日期/摘要。
119
+ // 未 purge 的会话可直接 /resume <id> 取回;已 purge 仅可检索摘要。
120
+ (ctx) => {
121
+ if (!ctx.line.startsWith('/ssearch'))
122
+ return unhandled();
123
+ const query = ctx.line.replace(/^\/ssearch\s*/, '').trim();
124
+ if (!query) {
125
+ layout.contentWrite(`${ui.yellow}用法: /ssearch <关键词>${ui.reset}\n`);
126
+ return next();
127
+ }
128
+ const hits = ctx.runtime.session.searchArchive(query, 10);
129
+ if (hits.length === 0) {
130
+ layout.contentWrite(`${ui.dim}(无匹配归档会话)${ui.reset}\n`);
131
+ return next();
132
+ }
133
+ for (const h of hits) {
134
+ const tag = h.purgedAt ? ` ${ui.yellow}[仅摘要]${ui.reset}` : ` ${ui.dim}[可 resume]${ui.reset}`;
135
+ layout.contentWrite(` ${ui.cyan}${h.id}${ui.reset} ${h.createdAt}${tag}\n`);
136
+ const body = (h.summary || h.firstUser).split('\n').slice(0, 4).join('\n');
137
+ layout.contentWrite(` ${ui.dim}${body}${ui.reset}\n\n`);
138
+ }
139
+ return next();
140
+ },
87
141
  // /rollback:打开轮次菜单(↑/↓ 选,Enter 回滚到该轮并预填其输入,再 Enter 重新跑)。
88
142
  // 忽略任何数字参数(原「输数字选回滚」已删,统一走菜单)。无快照的旧轮次(/resume 重建)文件改动不可撤销。
89
143
  async (ctx) => {
@@ -0,0 +1,28 @@
1
+ /**
2
+ * 用量命令组:/stats(#token-efficiency P1)
3
+ *
4
+ * dialog-only:读取当前会话 usage.jsonl,输出 cache 命中率 / 分层用量 / compact 次数。
5
+ * 后端从未上报 cached 时命中率显示 n/a,不报错。
6
+ */
7
+ import * as layout from '../../ui/layout.js';
8
+ import { getActiveSessionStore } from '../../session/store.js';
9
+ import { formatUsageSummary, summarizeUsage } from '../../session/usage-stats.js';
10
+ import { unhandled, next } from './types.js';
11
+ export const statsCommands = [
12
+ (ctx) => {
13
+ if (ctx.line !== '/stats')
14
+ return unhandled();
15
+ const sessionId = ctx.state.currentSessionId;
16
+ if (!sessionId) {
17
+ layout.contentWrite(' No active session yet.\n');
18
+ return next();
19
+ }
20
+ const raw = getActiveSessionStore().readUsage(sessionId);
21
+ if (raw.length === 0) {
22
+ layout.contentWrite(' No usage recorded yet (run a task first).\n');
23
+ return next();
24
+ }
25
+ layout.contentWrite(` ${formatUsageSummary(summarizeUsage(raw))}\n`);
26
+ return next();
27
+ },
28
+ ];
@@ -24,9 +24,15 @@ export function buildSlashCommands() {
24
24
  { name: '/skills', desc: d('commands.skills') },
25
25
  { name: '/skill', desc: '执行某个 skill(/skill <name> [args-json])' },
26
26
  { name: '/compact', desc: d('commands.compact') },
27
+ { name: '/stats', desc: '查看本会话 token 用量与缓存命中率' },
28
+ { name: '/effort', desc: '设置思考强度(/effort [low|medium|high|auto])' },
27
29
  { name: '/resume', desc: d('commands.sessionResume') },
28
30
  { name: '/sessions', desc: d('commands.sessionBrowse') },
29
31
  { name: '/rollback', desc: d('commands.sessionRollback') },
32
+ { name: '/ssearch <关键词>', value: '/ssearch ', submit: false, desc: '跨会话搜索归档会话' },
33
+ { name: '/jobs', desc: '后台任务列表(/jobs log/kill <id前缀>)' },
34
+ { name: '/schedules', desc: '查看定时计划与调度守护状态' },
35
+ { name: '/bots', desc: '查看具名 Bot 列表' },
30
36
  {
31
37
  name: '/memory',
32
38
  desc: d('commands.memory'),
@@ -268,64 +274,6 @@ export function themeDescription(name) {
268
274
  }
269
275
  /** /model 预设后端:同时声明原生协议;旧服务继续走 OpenAI-compatible。 */
270
276
  export const MODEL_PRESETS = [
271
- {
272
- label: 'Anthropic Claude',
273
- provider: 'anthropic',
274
- baseURL: 'https://api.anthropic.com',
275
- model: 'claude-sonnet-4-5',
276
- window: 200000,
277
- anthropicPromptCache: true,
278
- },
279
- {
280
- label: 'GLM(智谱)',
281
- provider: 'openai',
282
- baseURL: 'https://open.bigmodel.cn/api/v3',
283
- model: 'glm-4.6',
284
- window: DEFAULT_CONTEXT_WINDOW_TOKENS,
285
- anthropicPromptCache: false,
286
- },
287
- {
288
- label: 'DeepSeek',
289
- provider: 'openai',
290
- baseURL: 'https://api.deepseek.com',
291
- model: 'deepseek-chat',
292
- window: DEFAULT_CONTEXT_WINDOW_TOKENS,
293
- anthropicPromptCache: false,
294
- },
295
- {
296
- label: 'Qwen(阿里)',
297
- provider: 'openai',
298
- baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
299
- model: 'qwen-plus',
300
- window: DEFAULT_CONTEXT_WINDOW_TOKENS,
301
- anthropicPromptCache: false,
302
- },
303
- // MiniMax OpenAI 兼容端点(https://platform.minimax.io/docs/api-reference/text-openai-api)。
304
- // MiniMax-M3 为唯一支持图片/视频输入的模型;M2 系列纯文本(见 llm/capabilities.ts KNOWN_TEXT_ONLY_PREFIXES)。
305
- {
306
- label: 'MiniMax',
307
- provider: 'openai',
308
- baseURL: 'https://api.minimax.io/v1',
309
- model: 'MiniMax-M3',
310
- window: DEFAULT_CONTEXT_WINDOW_TOKENS,
311
- anthropicPromptCache: false,
312
- },
313
- {
314
- label: '本地 Ollama',
315
- provider: 'openai',
316
- baseURL: 'http://localhost:11434/v1',
317
- model: 'qwen2.5:7b',
318
- window: DEFAULT_CONTEXT_WINDOW_TOKENS,
319
- anthropicPromptCache: false,
320
- },
321
- {
322
- label: '本地 vLLM',
323
- provider: 'openai',
324
- baseURL: 'http://localhost:8000/v1',
325
- model: 'default',
326
- window: DEFAULT_CONTEXT_WINDOW_TOKENS,
327
- anthropicPromptCache: false,
328
- },
329
277
  {
330
278
  label: '自定义 base_url',
331
279
  provider: 'openai',