@jack200714/mafw 4.10.1 → 4.10.2

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.
@@ -97,7 +97,6 @@ const push_gateway_1 = require("./mobile/push-gateway");
97
97
  const device_store_1 = require("./mobile/device-store");
98
98
  const pairing_1 = require("./mobile/pairing");
99
99
  const tray_1 = require("./tray");
100
- const serve_sidecar_1 = require("./runtime/serve-sidecar");
101
100
  const self_update_1 = require("./self-update");
102
101
  const step_inject_1 = require("./recall/step-inject");
103
102
  const inject_format_1 = require("./recall/inject-format");
@@ -110,24 +109,15 @@ const validate_1 = require("./runtime/validate");
110
109
  const loader_1 = require("./runtime/loader");
111
110
  const pi_runtime_1 = require("./runtime/plugins/pi-runtime");
112
111
  const permission_1 = require("./routes/permission");
113
- const event_publish_1 = require("./routes/event-publish");
114
- const runtime_switch_1 = require("./routes/runtime-switch");
115
- const plugins_1 = require("./routes/plugins");
116
112
  const package_host_1 = require("./plugins/package-host");
117
113
  const package_context_1 = require("./plugins/package-context");
118
- const hub_1 = require("./plugins/hub");
119
- const restart_agent_1 = require("./routes/restart-agent");
120
- const session_mutations_1 = require("./routes/session-mutations");
121
114
  const serve_supervisor_1 = require("./runtime/serve-supervisor");
122
115
  const serve_for_runtime_1 = require("./runtime/serve-for-runtime");
123
- const media_switch_1 = require("./routes/media-switch");
124
- const usage_plugins_1 = require("./routes/usage-plugins");
125
116
  const model_stats_1 = require("./usage/model-stats");
126
- const model_config_1 = require("./routes/model-config");
127
- const session_branch_1 = require("./routes/session-branch");
128
- const session_summarize_1 = require("./routes/session-summarize");
129
- const goal_sessions_1 = require("./routes/goal-sessions");
130
- const embedding_config_1 = require("./routes/embedding-config");
117
+ const registry_1 = require("./routes/registry");
118
+ const route_catalog_1 = require("./routes/route-catalog");
119
+ const wave1_handlers_1 = require("./routes/wave1-handlers");
120
+ const wave2_handlers_1 = require("./routes/wave2-handlers");
131
121
  const manager_rotate_1 = require("./routes/manager-rotate");
132
122
  function readBody(req) {
133
123
  return new Promise((resolve, reject) => {
@@ -228,6 +218,12 @@ class MafwScheduler {
228
218
  mcpEndpoint;
229
219
  mcpStreamableEndpoint;
230
220
  opencodeClient = null;
221
+ /**
222
+ * P4 shadow registry + P5 Wave 1 dispatch:catalog 全量登记(130 条,贡献 spec),
223
+ * Wave 1 已 attach 的 22 条在请求链顶端优先 dispatch(见 startApiServer),
224
+ * 未 attach 的 shadow 条目自然落回 legacy 内联链。
225
+ */
226
+ routeRegistry = new registry_1.RouteRegistry().register(...(0, route_catalog_1.buildRouteCatalog)());
231
227
  runtimeCaps = (0, contract_1.minimalCapabilities)();
232
228
  runtimeName = 'opencode';
233
229
  runtimeLoader;
@@ -272,51 +268,34 @@ class MafwScheduler {
272
268
  this.registryPath = config_1.config.paths.registryFile;
273
269
  this.chatSessions = new chat_sessions_1.ChatSessionManager();
274
270
  this.serveSupervisor = (0, serve_supervisor_1.createServeSupervisor)({
275
- port: config_1.config.server.servePort,
276
- external: !!process.env.MAFW_SERVER_SERVE_URL,
277
- killPort: (port) => (0, serve_sidecar_1.killServePort)(port),
278
- // Spawn 原语来自活跃 runtime 的契约(agentProcess.spawnServe)——
279
- // gateway 核心不硬编码任何具体 agent 的 server 拉起细节。
271
+ // 全部经 runtime 契约且 late-bound(runtime 可热切换):
272
+ // managed = runtime 是否持有启停原语;health = 契约 healthCheck;
273
+ // baseUrl/killServe = runtime 自报地址与清场原语;spawn 不传 host/port
274
+ managed: () => !!this.opencodeClient?.agentProcess?.spawnServe,
275
+ health: async () => {
276
+ try {
277
+ return (await this.opencodeClient?.healthCheck?.()) ?? false;
278
+ }
279
+ catch {
280
+ return false;
281
+ }
282
+ },
283
+ baseUrl: () => this.opencodeClient?.getBaseUrl?.() ?? this.serveUrl,
284
+ killServe: () => this.opencodeClient?.agentProcess?.killServe?.(),
280
285
  spawn: async (opts) => {
281
286
  const spawnServe = this.opencodeClient?.agentProcess?.spawnServe;
282
287
  if (!spawnServe) {
283
288
  throw new Error('active runtime does not own a server process (agentProcess.spawnServe missing)');
284
289
  }
285
- const sidecar = await spawnServe({
286
- host: opts.host,
287
- port: opts.port,
288
- timeoutMs: opts.timeoutMs,
289
- });
290
+ const sidecar = await spawnServe({ timeoutMs: opts.timeoutMs });
290
291
  return { url: sidecar.url, close: () => sidecar.close() };
291
292
  },
292
- probe: async (url) => {
293
- try {
294
- const res = await fetch(`${url}/global/health`, { signal: AbortSignal.timeout(3000) });
295
- return res.ok;
296
- }
297
- catch {
298
- return false;
299
- }
300
- },
301
293
  });
302
294
  }
303
295
  get serveRunning() {
304
296
  return !!this.serveInstance;
305
297
  }
306
298
  /** Check actual serve health via TCP connection, not just object existence. */
307
- async checkServeActualHealth() {
308
- try {
309
- const url = `${this.serveUrl}/global/health`;
310
- const res = await fetch(url, {
311
- signal: AbortSignal.timeout(2000),
312
- headers: { 'User-Agent': 'MAFW-Gateway-HealthCheck' }
313
- });
314
- return res.ok;
315
- }
316
- catch {
317
- return false;
318
- }
319
- }
320
299
  async start() {
321
300
  logger_1.log.info('MAFW Scheduler v5.0 starting...');
322
301
  // Single-instance guard: if a healthy gateway already owns apiPort, this
@@ -433,7 +412,8 @@ class MafwScheduler {
433
412
  }
434
413
  else {
435
414
  logger_1.log.info('OpenCode Serve not reachable, checking for stale process...');
436
- (0, serve_sidecar_1.killServePort)(config_1.config.server.servePort);
415
+ // 清场原语归 runtime(serve 端口是实现细节;无原语的 runtime 无从清场)
416
+ this.opencodeClient?.agentProcess?.killServe?.();
437
417
  try {
438
418
  await this.startServe();
439
419
  serveReady = !!this.serveInstance;
@@ -1279,6 +1259,120 @@ class MafwScheduler {
1279
1259
  const result = await rt.indexer.backfill(ids);
1280
1260
  return { ...result, vectors: rt.vectors.size(), indexEntries: entries.length };
1281
1261
  }
1262
+ /** P5 Wave 2:/api/runtime 路由 deps(自内联块上移,行为逐字节等价)。 */
1263
+ runtimeDeps() {
1264
+ return {
1265
+ loader: this.runtimeLoader,
1266
+ persist: (o) => config_1.config.persistOverrides(o),
1267
+ getCurrent: () => this.opencodeClient,
1268
+ runtimeName: () => this.runtimeName,
1269
+ runtimeCaps: () => this.runtimeCaps,
1270
+ envOverride: () => !!process.env.MAFW_RUNTIME_PLUGIN,
1271
+ createRuntime: async () => {
1272
+ const sdkConfig = {
1273
+ baseUrl: this.serveUrl,
1274
+ directory: this.projectDir,
1275
+ headers: {},
1276
+ };
1277
+ const opencodePassword = process.env.MAFW_OPENCODE_PASSWORD;
1278
+ if (opencodePassword) {
1279
+ sdkConfig.headers = { Authorization: 'Basic ' + Buffer.from(`opencode:${opencodePassword}`).toString('base64') };
1280
+ }
1281
+ return this.createRuntime(sdkConfig);
1282
+ },
1283
+ onSwitched: async (rt, prev) => {
1284
+ this.opencodeClient = rt;
1285
+ this.runtimeCaps = rt.capabilities;
1286
+ this.runtimeName = rt.name;
1287
+ // Switching onto a runtime that owns serve (builtin opencode) must
1288
+ // ensure the sidecar exists — the gateway may have started under an
1289
+ // external runtime (pi) that never spawned one. Must run AFTER the
1290
+ // assignment above: the supervisor's spawn closure reads
1291
+ // this.opencodeClient to find agentProcess.spawnServe.
1292
+ if (rt.agentProcess?.spawnServe) {
1293
+ await (0, serve_for_runtime_1.ensureServeForBuiltinRuntime)(this.serveSupervisor, { startWatchdog: () => this.startServeWatchdog() }, logger_1.log);
1294
+ }
1295
+ this.sdkSession.setClient(this.opencodeClient);
1296
+ if (this.trajectoryCollector)
1297
+ this.trajectoryCollector.setOpencodeClient(this.opencodeClient);
1298
+ if (this.automationEngine)
1299
+ this.automationEngine.setRuntimeClient(rt);
1300
+ await this.resubscribeEvents(`runtime switched to '${rt.name}'`);
1301
+ // Desktop hint: a runtime switch swaps the session storage backend
1302
+ // (opencode SQLite vs pi), so cached session lists are stale.
1303
+ this.broadcast({ type: 'runtime_switched', runtime: rt.name, previous: prev?.name ?? null });
1304
+ if (prev && prev.dispose) {
1305
+ try {
1306
+ await prev.dispose();
1307
+ }
1308
+ catch (err) {
1309
+ logger_1.log.warn(`[Runtime] dispose old runtime failed: ${err.message}`);
1310
+ }
1311
+ }
1312
+ },
1313
+ };
1314
+ }
1315
+ /** P5 Wave 2:/api/runtime/restart-agent deps。 */
1316
+ restartAgentDeps() {
1317
+ return {
1318
+ capabilities: () => this.runtimeCaps,
1319
+ isRecovering: () => this.serveRecovering,
1320
+ isSwitching: () => this.switchingRuntime,
1321
+ begin: () => { this.serveRecovering = true; },
1322
+ end: () => { this.serveRecovering = false; },
1323
+ restartAgent: () => this.restartAgentOrchestrated(),
1324
+ };
1325
+ }
1326
+ /** P5 Wave 2:/api/plugins* deps(Plugin Hub 四类型统一面)。 */
1327
+ pluginHubDeps() {
1328
+ return {
1329
+ hub: {
1330
+ dirs: {
1331
+ runtime: config_1.config.resolvePath('runtime-plugins'),
1332
+ media: config_1.config.resolvePath('media-plugins'),
1333
+ usage: config_1.config.resolvePath('usage-plugins'),
1334
+ ui: process.env.MAFW_UI_PLUGINS_DIR || path.join(os.homedir(), '.mafw', 'ui-plugins'),
1335
+ },
1336
+ builtinEntries: () => {
1337
+ const entries = [];
1338
+ const rt = (name) => ({ type: 'runtime', name, file: '(builtin)', status: 'enabled', size: 0, mtime: '' });
1339
+ for (const name of this.runtimeLoader?.getBuiltinNames?.() ?? [])
1340
+ entries.push(rt(name));
1341
+ for (const name of this.mediaPluginLoader?.getBuiltinEngineNames?.() ?? []) {
1342
+ entries.push({ type: 'media', name, file: '(builtin)', status: 'enabled', size: 0, mtime: '' });
1343
+ }
1344
+ const usageState = this.pluginLoader?.getState?.() ?? [];
1345
+ for (const s of usageState) {
1346
+ if (s.builtin && s.status === 'ok' && s.name) {
1347
+ entries.push({ type: 'usage', name: s.name, file: s.file, status: 'enabled', size: 0, mtime: '', pluginType: s.pluginType });
1348
+ }
1349
+ }
1350
+ return entries;
1351
+ },
1352
+ getErrors: (type) => {
1353
+ const stateOf = (loader) => (loader && typeof loader.getState === 'function' ? loader.getState() : []);
1354
+ const source = type === 'runtime' ? this.runtimeLoader : type === 'media' ? this.mediaPluginLoader : type === 'usage' ? this.pluginLoader : null;
1355
+ const out = {};
1356
+ for (const p of stateOf(source)) {
1357
+ if (p && p.error)
1358
+ out[p.name || p.file] = p.error;
1359
+ }
1360
+ return out;
1361
+ },
1362
+ configDisabledUsage: () => new Set(Array.isArray(config_1.config.usage?.disabledPlugins) ? config_1.config.usage.disabledPlugins : []),
1363
+ reload: async (type) => {
1364
+ if (type === 'runtime')
1365
+ await this.runtimeLoader?.scan();
1366
+ else if (type === 'media')
1367
+ await this.mediaPluginLoader?.reload();
1368
+ else if (type === 'usage')
1369
+ await this.pluginLoader?.reload();
1370
+ // ui: desktop main fs.watch picks it up automatically
1371
+ },
1372
+ getPackages: () => this.pluginHost?.getState() ?? [],
1373
+ },
1374
+ };
1375
+ }
1282
1376
  embeddingConfigDeps() {
1283
1377
  return {
1284
1378
  currentConfig: () => {
@@ -2085,12 +2179,10 @@ class MafwScheduler {
2085
2179
  return;
2086
2180
  }
2087
2181
  logger_1.log.info('Starting OpenCode Serve sidecar...');
2088
- const port = config_1.config.server.servePort;
2089
- const host = config_1.config.server.serveHost;
2090
2182
  try {
2183
+ // host/port 归 runtime 自定(serve 端口是实现细节);sidecar 实际 URL
2184
+ // 由 runtime 吸收(getBaseUrl 跟随),gateway 只记 bookkeeping。
2091
2185
  const sidecar = await spawnServe({
2092
- host,
2093
- port,
2094
2186
  onOutput: (chunk) => logger_1.log.debug(`[Serve] ${chunk.trimEnd()}`),
2095
2187
  onExit: (code) => this.handleServeExit(code),
2096
2188
  });
@@ -2352,18 +2444,14 @@ class MafwScheduler {
2352
2444
  return projectID ? await this.sdkSession.listByProject(projectID) : await this.sdkSession.list();
2353
2445
  }
2354
2446
  async isServeHealthy() {
2355
- const url = `${this.serveUrl}/global/health`;
2356
- return new Promise((resolve) => {
2357
- const req = http.get(url, { timeout: 5000 }, (res) => {
2358
- resolve(res.statusCode === 200);
2359
- res.resume();
2360
- });
2361
- req.on('error', () => resolve(false));
2362
- req.on('timeout', () => {
2363
- req.destroy();
2364
- resolve(false);
2365
- });
2366
- });
2447
+ // 健康唯一真相源 = runtime 契约 healthCheck()(serve 型探测 serve,
2448
+ // 进程内 runtime 探测自身引擎);gateway 不自带 HTTP 探测。
2449
+ try {
2450
+ return (await this.opencodeClient?.healthCheck?.()) ?? false;
2451
+ }
2452
+ catch {
2453
+ return false;
2454
+ }
2367
2455
  }
2368
2456
  async waitForServeReady() {
2369
2457
  const maxRetries = config_1.config.timeouts.serveReadyMaxRetries;
@@ -2380,9 +2468,45 @@ class MafwScheduler {
2380
2468
  // 鈹€鈹€ 2. HTTP API 鈹€鈹€
2381
2469
  async startApiServer() {
2382
2470
  return new Promise((resolve) => {
2471
+ // P5 Wave 1: catalog shadow 登记 + 模块 handler 绑定(一次性,重复 attach 会抛错)。
2472
+ // adapter 显式桥接私有成员(结构化类型不认 private)。
2473
+ (0, wave1_handlers_1.attachWave1Handlers)(this.routeRegistry, {
2474
+ getGatewayDb: () => this.getGatewayDb(),
2475
+ opencodeClient: this.opencodeClient,
2476
+ automationEngine: this.automationEngine,
2477
+ ledger: this.ledger,
2478
+ rotateDeps: () => this.rotateDeps(),
2479
+ embeddingConfigDeps: () => this.embeddingConfigDeps(),
2480
+ modelConfigDeps: () => this.modelConfigDeps(),
2481
+ usagePluginsDeps: () => this.usagePluginsDeps(),
2482
+ pluginLoader: this.pluginLoader,
2483
+ mediaPluginLoader: this.mediaPluginLoader,
2484
+ broadcast: (e) => this.broadcast(e),
2485
+ runtimeCaps: this.runtimeCaps,
2486
+ });
2487
+ (0, wave2_handlers_1.attachWave2Handlers)(this.routeRegistry, {
2488
+ runtimeDeps: () => this.runtimeDeps(),
2489
+ restartAgentDeps: () => this.restartAgentDeps(),
2490
+ pluginHubDeps: () => this.pluginHubDeps(),
2491
+ runtimeSwitchBlocked: () => this.serveRecovering || this.switchingRuntime,
2492
+ beginRuntimeSwitch: () => { this.switchingRuntime = true; },
2493
+ endRuntimeSwitch: () => { this.switchingRuntime = false; },
2494
+ });
2383
2495
  const server = http.createServer(async (req, res) => {
2384
2496
  try {
2385
2497
  res.setHeader('Content-Type', 'application/json');
2498
+ // P5 Wave 1: registry dispatch —— attach 过 handler 的路由在 legacy 链之前
2499
+ // 统一接管(shadow 条目无 handler 自然落回内联链)。挂在鉴权之后、一切
2500
+ // /api matcher 之前:goals.sessions 须先于 dashboard 兜底、embedding-config
2501
+ // 须先于 /api/memory/* 委托(AGENTS.md §6.5 顺序教训由 dispatch 位置一次性满足)。
2502
+ {
2503
+ const matched = this.routeRegistry.match(req.method, req.url || '');
2504
+ if (matched?.def.handler) {
2505
+ const handled = await matched.def.handler(req, res, matched.params);
2506
+ if (handled !== false)
2507
+ return;
2508
+ }
2509
+ }
2386
2510
  // CORS headers for SSE
2387
2511
  res.setHeader("Access-Control-Allow-Origin", config_1.config.server.cors.origin);
2388
2512
  res.setHeader("Access-Control-Allow-Methods", config_1.config.server.cors.methods);
@@ -3350,16 +3474,8 @@ class MafwScheduler {
3350
3474
  return;
3351
3475
  }
3352
3476
  // GET/POST /api/memory/embedding-config — memory embedding engine
3353
- // settings (hot-swap: rebuild runtime + background backfill). MUST
3354
- // stay above the /api/memory/* dashboard delegation below.
3355
- if (req.url?.match(/^\/api\/memory\/embedding-config(?:\?|$)/) && req.method === 'GET') {
3356
- await (0, embedding_config_1.handleEmbeddingConfigGet)(req, res, this.embeddingConfigDeps());
3357
- return;
3358
- }
3359
- if (req.url?.match(/^\/api\/memory\/embedding-config(?:\?|$)/) && req.method === 'POST') {
3360
- await (0, embedding_config_1.handleEmbeddingConfigUpdate)(req, res, this.embeddingConfigDeps());
3361
- return;
3362
- }
3477
+ // settings(P5 Wave 1 起由 registry dispatch 接管,见 routes/wave1-handlers.ts;
3478
+ // 历史顺序约束"须高于 /api/memory/* dashboard 委托"由 dispatch 位置满足)
3363
3479
  // GET /api/memory/stats — consolidation health + vector coverage (P2)
3364
3480
  if (req.url?.match(/^\/api\/memory\/stats(?:\?|$)/) && req.method === 'GET') {
3365
3481
  try {
@@ -3606,16 +3722,8 @@ class MafwScheduler {
3606
3722
  res.end(JSON.stringify({ status: 'accepted' }));
3607
3723
  return;
3608
3724
  }
3609
- // GET /api/goals/:id/sessions — goal session 映射(编排可视化下钻)。
3610
- // 必须挂在 Dashboard API 的 /api/goals* 兜底之前(AGENTS.md §6.5 路由顺序教训)。
3611
- if (req.url?.match(/^\/api\/goals\/[^/]+\/sessions(?:\?|$)/) && req.method === 'GET') {
3612
- const handled = await (0, goal_sessions_1.handleGoalSessions)(req, res, req.url, {
3613
- listGoalSessions: (goalId) => this.getGatewayDb().listGoalSessions(goalId),
3614
- getSession: (sessionID) => this.opencodeClient?.session.get({ sessionID }).catch(() => null),
3615
- });
3616
- if (handled)
3617
- return;
3618
- }
3725
+ // GET /api/goals/:id/sessions — P5 Wave 1 起由 registry dispatch 接管
3726
+ // (历史约束"须挂 Dashboard /api/goals* 兜底之前"由 dispatch 位置满足)。
3619
3727
  // Dashboard API
3620
3728
  if (req.url?.startsWith("/api/goals") || req.url?.startsWith("/api/stats") || req.url?.startsWith("/api/memory")) {
3621
3729
  res.setHeader("Content-Type", "application/json");
@@ -3729,8 +3837,8 @@ class MafwScheduler {
3729
3837
  });
3730
3838
  return;
3731
3839
  }
3732
- // 控制指令
3733
- if (req.url === '/control' && req.method === 'POST') {
3840
+ // 控制指令(/control 为 MCP handler 兼容路径;/api/goals/control 为 SDK goals.control 契约路径,两者等价)
3841
+ if ((req.url === '/control' || req.url === '/api/goals/control') && req.method === 'POST') {
3734
3842
  let body = '';
3735
3843
  req.on('data', chunk => body += chunk);
3736
3844
  req.on('end', async () => {
@@ -3833,12 +3941,7 @@ class MafwScheduler {
3833
3941
  }
3834
3942
  return;
3835
3943
  }
3836
- // POST /api/manager/session/rotate — start a new manager topic
3837
- // (thin wiring → routes/manager-rotate.ts).
3838
- if (req.method === 'POST' && req.url?.match(/^\/api\/manager\/session\/rotate(?:\?|$)/)) {
3839
- await (0, manager_rotate_1.handleManagerRotate)(req, res, this.rotateDeps());
3840
- return;
3841
- }
3944
+ // POST /api/manager/session/rotate — P5 Wave 1 起由 registry dispatch 接管
3842
3945
  // GET /api/manager/session — return manager session info (per-project,
3843
3946
  // read from the gateway DB). ?projectDir= filters a single project.
3844
3947
  if (req.url && req.url.startsWith('/api/manager/session') && req.method === 'GET') {
@@ -4189,173 +4292,12 @@ class MafwScheduler {
4189
4292
  return;
4190
4293
  }
4191
4294
  // 鈹€鈹€ Provider & Agents (composer model pill / @agent mention) 鈹€鈹€
4192
- // ── /api/runtime routes (thin wiring routes/runtime-switch.ts) ──
4193
- if (req.url?.match(/^\/api\/runtime(?:\/|$)/)) {
4194
- const runtimeDeps = {
4195
- loader: this.runtimeLoader,
4196
- persist: (o) => config_1.config.persistOverrides(o),
4197
- getCurrent: () => this.opencodeClient,
4198
- runtimeName: () => this.runtimeName,
4199
- runtimeCaps: () => this.runtimeCaps,
4200
- envOverride: () => !!process.env.MAFW_RUNTIME_PLUGIN,
4201
- createRuntime: async () => {
4202
- const sdkConfig = {
4203
- baseUrl: this.serveUrl,
4204
- directory: this.projectDir,
4205
- headers: {},
4206
- };
4207
- const opencodePassword = process.env.MAFW_OPENCODE_PASSWORD;
4208
- if (opencodePassword) {
4209
- sdkConfig.headers = { Authorization: 'Basic ' + Buffer.from(`opencode:${opencodePassword}`).toString('base64') };
4210
- }
4211
- return this.createRuntime(sdkConfig);
4212
- },
4213
- onSwitched: async (rt, prev) => {
4214
- this.opencodeClient = rt;
4215
- this.runtimeCaps = rt.capabilities;
4216
- this.runtimeName = rt.name;
4217
- // Switching onto a runtime that owns serve (builtin opencode) must
4218
- // ensure the sidecar exists — the gateway may have started under an
4219
- // external runtime (pi) that never spawned one. Must run AFTER the
4220
- // assignment above: the supervisor's spawn closure reads
4221
- // this.opencodeClient to find agentProcess.spawnServe.
4222
- if (rt.agentProcess?.spawnServe) {
4223
- await (0, serve_for_runtime_1.ensureServeForBuiltinRuntime)(this.serveSupervisor, { startWatchdog: () => this.startServeWatchdog() }, logger_1.log);
4224
- }
4225
- this.sdkSession.setClient(this.opencodeClient);
4226
- if (this.trajectoryCollector)
4227
- this.trajectoryCollector.setOpencodeClient(this.opencodeClient);
4228
- if (this.automationEngine)
4229
- this.automationEngine.setRuntimeClient(rt);
4230
- await this.resubscribeEvents(`runtime switched to '${rt.name}'`);
4231
- // Desktop hint: a runtime switch swaps the session storage backend
4232
- // (opencode SQLite vs pi), so cached session lists are stale.
4233
- this.broadcast({ type: 'runtime_switched', runtime: rt.name, previous: prev?.name ?? null });
4234
- if (prev && prev.dispose) {
4235
- try {
4236
- await prev.dispose();
4237
- }
4238
- catch (err) {
4239
- logger_1.log.warn(`[Runtime] dispose old runtime failed: ${err.message}`);
4240
- }
4241
- }
4242
- },
4243
- };
4244
- if (req.method === 'GET' && req.url?.match(/^\/api\/runtime(?:\?|$)/)) {
4245
- await (0, runtime_switch_1.handleRuntimeGet)(req, res, runtimeDeps);
4246
- return;
4247
- }
4248
- if (req.method === 'POST' && req.url?.match(/^\/api\/runtime\/switch(?:\?|$)/)) {
4249
- if (this.serveRecovering || this.switchingRuntime) {
4250
- res.writeHead(409, { 'Content-Type': 'application/json' });
4251
- res.end(JSON.stringify({ error: 'Cannot switch runtime during agent restart' }));
4252
- return;
4253
- }
4254
- this.switchingRuntime = true;
4255
- try {
4256
- await (0, runtime_switch_1.handleRuntimeSwitch)(req, res, runtimeDeps);
4257
- }
4258
- finally {
4259
- this.switchingRuntime = false;
4260
- }
4261
- return;
4262
- }
4263
- if (req.method === 'POST' && req.url?.match(/^\/api\/runtime\/reload(?:\?|$)/)) {
4264
- await (0, runtime_switch_1.handleRuntimeReload)(req, res, runtimeDeps);
4265
- return;
4266
- }
4267
- if (req.method === 'POST' && req.url?.match(/^\/api\/runtime\/restart-agent(?:\?|$)/)) {
4268
- await (0, restart_agent_1.handleRestartAgent)(req, res, {
4269
- capabilities: () => this.runtimeCaps,
4270
- isRecovering: () => this.serveRecovering,
4271
- isSwitching: () => this.switchingRuntime,
4272
- begin: () => { this.serveRecovering = true; },
4273
- end: () => { this.serveRecovering = false; },
4274
- restartAgent: () => this.restartAgentOrchestrated(),
4275
- });
4276
- return;
4277
- }
4278
- return;
4279
- }
4280
- // ── Plugin Hub (all four plugin types) ──
4281
- const pluginHubDeps = {
4282
- hub: {
4283
- dirs: {
4284
- runtime: config_1.config.resolvePath('runtime-plugins'),
4285
- media: config_1.config.resolvePath('media-plugins'),
4286
- usage: config_1.config.resolvePath('usage-plugins'),
4287
- ui: process.env.MAFW_UI_PLUGINS_DIR || path.join(os.homedir(), '.mafw', 'ui-plugins'),
4288
- },
4289
- builtinEntries: () => {
4290
- const entries = [];
4291
- const rt = (name) => ({ type: 'runtime', name, file: '(builtin)', status: 'enabled', size: 0, mtime: '' });
4292
- // opencode 经 registerBuiltin 注册,getBuiltinNames 已含——不再手工 push(防重复)
4293
- for (const name of this.runtimeLoader?.getBuiltinNames?.() ?? [])
4294
- entries.push(rt(name));
4295
- for (const name of this.mediaPluginLoader?.getBuiltinEngineNames?.() ?? []) {
4296
- entries.push({ type: 'media', name, file: '(builtin)', status: 'enabled', size: 0, mtime: '' });
4297
- }
4298
- const usageState = this.pluginLoader?.getState?.() ?? [];
4299
- for (const s of usageState) {
4300
- if (s.builtin && s.status === 'ok' && s.name) {
4301
- entries.push({ type: 'usage', name: s.name, file: s.file, status: 'enabled', size: 0, mtime: '', pluginType: s.pluginType });
4302
- }
4303
- }
4304
- return entries;
4305
- },
4306
- getErrors: (type) => {
4307
- const stateOf = (loader) => (loader && typeof loader.getState === 'function' ? loader.getState() : []);
4308
- const source = type === 'runtime' ? this.runtimeLoader : type === 'media' ? this.mediaPluginLoader : type === 'usage' ? this.pluginLoader : null;
4309
- const out = {};
4310
- for (const p of stateOf(source)) {
4311
- if (p && p.error)
4312
- out[p.name || p.file] = p.error;
4313
- }
4314
- return out;
4315
- },
4316
- configDisabledUsage: () => new Set(Array.isArray(config_1.config.usage?.disabledPlugins) ? config_1.config.usage.disabledPlugins : []),
4317
- reload: async (type) => {
4318
- if (type === 'runtime')
4319
- await this.runtimeLoader?.scan();
4320
- else if (type === 'media')
4321
- await this.mediaPluginLoader?.reload();
4322
- else if (type === 'usage')
4323
- await this.pluginLoader?.reload();
4324
- // ui: desktop main fs.watch picks it up automatically
4325
- },
4326
- getPackages: () => this.pluginHost?.getState() ?? [],
4327
- },
4328
- };
4329
- try {
4330
- const cleaned = (0, hub_1.cleanupExamples)(pluginHubDeps.hub);
4331
- if (cleaned.removed.length)
4332
- logger_1.log.info(`[PluginsHub] removed stale examples: ${cleaned.removed.length}`);
4333
- if (cleaned.failed.length)
4334
- logger_1.log.warn(`[PluginsHub] cleanupExamples failed: ${cleaned.failed.join(', ')}`);
4335
- }
4336
- catch (err) {
4337
- logger_1.log.warn(`[PluginsHub] cleanupExamples error: ${err.message}`);
4338
- }
4339
- if (req.method === 'GET' && req.url?.match(/^\/api\/plugins(?:\?|$)/)) {
4340
- await (0, plugins_1.handlePluginsList)(req, res, pluginHubDeps);
4341
- return;
4342
- }
4343
- if (req.method === 'POST' && req.url?.match(/^\/api\/plugins\/install(?:\?|$)/)) {
4344
- await (0, plugins_1.handlePluginsInstall)(req, res, pluginHubDeps);
4345
- return;
4346
- }
4347
- if (req.method === 'POST' && req.url?.match(/^\/api\/plugins\/enable(?:\?|$)/)) {
4348
- await (0, plugins_1.handlePluginsEnable)(req, res, pluginHubDeps);
4349
- return;
4350
- }
4351
- if (req.method === 'POST' && req.url?.match(/^\/api\/plugins\/disable(?:\?|$)/)) {
4352
- await (0, plugins_1.handlePluginsDisable)(req, res, pluginHubDeps);
4353
- return;
4354
- }
4355
- if (req.method === 'POST' && req.url?.match(/^\/api\/plugins\/delete(?:\?|$)/)) {
4356
- await (0, plugins_1.handlePluginsDelete)(req, res, pluginHubDeps);
4357
- return;
4358
- }
4295
+ // ── /api/runtime routes P5 Wave 2 起由 registry dispatch 接管
4296
+ // (runtimeDeps/restartAgentDeps 构造上移至私有方法;内联块对未知
4297
+ // /api/runtime/* 悬空 return 吞请求的行为随之终结——现落回 legacy 链)
4298
+ // ── Plugin Hub (all four plugin types) — P5 Wave 2 起由 registry dispatch
4299
+ // 接管(pluginHubDeps 构造上移至私有方法;cleanupExamples 改为命中
4300
+ // /api/plugins 路由时才执行——内联时代每个到达此处的请求都跑)。
4359
4301
  // GET /api/orchestration/outcomes — goal outcome query
4360
4302
  if (req.url?.match(/^\/api\/orchestration\/outcomes(?:\?|$)/) && req.method === 'GET') {
4361
4303
  const u = new URL(req.url, 'http://localhost');
@@ -4818,13 +4760,7 @@ class MafwScheduler {
4818
4760
  }
4819
4761
  return;
4820
4762
  }
4821
- // DELETE/PATCH /api/sessions/:id — true forwards (rename / delete)
4822
- if (await (0, session_mutations_1.handleSessionMutations)(req, res, {
4823
- getCapabilities: () => this.runtimeCaps,
4824
- getClient: () => (this.opencodeClient ?? null),
4825
- })) {
4826
- return;
4827
- }
4763
+ // DELETE/PATCH /api/sessions/:id — P5 Wave 1 起由 registry dispatch 接管
4828
4764
  // GET /api/sessions — list sessions (optional ?projectID=xxx)
4829
4765
  if (req.url?.match(/^\/api\/sessions(?:\?|$)/) && req.method === 'GET') {
4830
4766
  try {
@@ -4988,44 +4924,7 @@ class MafwScheduler {
4988
4924
  }
4989
4925
  return;
4990
4926
  }
4991
- // GET /api/usage/plugins — plugin state list (+templates for the wizard)
4992
- if (req.url?.match(/^\/api\/usage\/plugins(?:\?|$)/) && req.method === 'GET') {
4993
- await (0, usage_plugins_1.handleUsagePluginsList)(req, res, this.usagePluginsDeps());
4994
- return;
4995
- }
4996
- // POST /api/usage/plugins/create — template wizard or raw source
4997
- if (req.url?.match(/^\/api\/usage\/plugins\/create$/) && req.method === 'POST') {
4998
- await (0, usage_plugins_1.handleUsagePluginCreate)(req, res, this.usagePluginsDeps());
4999
- return;
5000
- }
5001
- // GET/PUT /api/usage/plugins/:name/source — user plugin code editor
5002
- const mSource = req.url?.match(/^\/api\/usage\/plugins\/([^/]+)\/source$/);
5003
- if (mSource && req.method === 'GET') {
5004
- await (0, usage_plugins_1.handleUsagePluginSourceGet)(req, res, this.usagePluginsDeps(), decodeURIComponent(mSource[1]));
5005
- return;
5006
- }
5007
- if (mSource && req.method === 'PUT') {
5008
- await (0, usage_plugins_1.handleUsagePluginSourcePut)(req, res, this.usagePluginsDeps(), decodeURIComponent(mSource[1]));
5009
- return;
5010
- }
5011
- // POST /api/usage/plugins/:name/test — one-shot adapter fetch
5012
- const mTest = req.url?.match(/^\/api\/usage\/plugins\/([^/]+)\/test$/);
5013
- if (mTest && req.method === 'POST') {
5014
- await (0, usage_plugins_1.handleUsagePluginTest)(req, res, this.usagePluginsDeps(), decodeURIComponent(mTest[1]));
5015
- return;
5016
- }
5017
- // DELETE /api/usage/plugins/:name — remove user plugin file
5018
- const mDel = req.url?.match(/^\/api\/usage\/plugins\/([^/]+)$/);
5019
- if (mDel && req.method === 'DELETE') {
5020
- await (0, usage_plugins_1.handleUsagePluginDelete)(req, res, this.usagePluginsDeps(), decodeURIComponent(mDel[1]));
5021
- return;
5022
- }
5023
- // POST /api/usage/plugins/reload — manual reload
5024
- if (req.url?.match(/^\/api\/usage\/plugins\/reload$/) && req.method === 'POST') {
5025
- await this.pluginLoader?.reload();
5026
- await (0, usage_plugins_1.handleUsagePluginsList)(req, res, this.usagePluginsDeps());
5027
- return;
5028
- }
4927
+ // /api/usage/plugins*P5 Wave 1 起由 registry dispatch 接管(7 条)
5029
4928
  // GET /api/media/plugins — media engine plugin state list
5030
4929
  if (req.url?.match(/^\/api\/media\/plugins(?:\?|$)/) && req.method === 'GET') {
5031
4930
  const plugins = (this.mediaPluginLoader?.getState() ?? []).map(s => ({
@@ -5053,43 +4952,10 @@ class MafwScheduler {
5053
4952
  res.end(JSON.stringify({ ok: true, plugins }));
5054
4953
  return;
5055
4954
  }
5056
- // POST /api/media/switch — switch media engine per modality
5057
- if (req.url?.match(/^\/api\/media\/switch(?:\?|$)/) && req.method === 'POST') {
5058
- await (0, media_switch_1.handleMediaSwitch)(req, res, {
5059
- persist: (o) => config_1.config.persistOverrides(o),
5060
- reloadPlugins: async () => { await this.mediaPluginLoader?.reload(); },
5061
- availableEngines: () => (this.mediaPluginLoader?.getState().map(s => s.name).filter((n) => !!n) ?? []),
5062
- currentMedia: () => ({
5063
- engine: config_1.config.raw.media.engine,
5064
- image: config_1.config.raw.media.image,
5065
- video: config_1.config.raw.media.video,
5066
- audio: config_1.config.raw.media.audio,
5067
- }),
5068
- });
5069
- return;
5070
- }
5071
- // GET/POST /api/model-config — recall worker model + media models (hot-apply)
5072
- if (req.url?.match(/^\/api\/model-config(?:\?|$)/) && req.method === 'GET') {
5073
- await (0, model_config_1.handleModelConfigGet)(req, res, this.modelConfigDeps());
5074
- return;
5075
- }
5076
- if (req.url?.match(/^\/api\/model-config(?:\?|$)/) && req.method === 'POST') {
5077
- await (0, model_config_1.handleModelConfigUpdate)(req, res, this.modelConfigDeps());
5078
- return;
5079
- }
5080
- // POST /api/sessions/:id/fork|revert|unrevert — session branch primitives
5081
- // (capability-gated: sessionBranchApi; unrevert is opencode-only → 404 on pi)
5082
- if (req.url?.match(/^\/api\/sessions\/[^/]+\/(fork|revert|unrevert)(?:\?|$)/) && req.method === 'POST') {
5083
- const handled = await (0, session_branch_1.handleSessionBranch)(req, res, req.url, { getRuntime: () => this.opencodeClient ?? null });
5084
- if (handled)
5085
- return;
5086
- }
5087
- // POST /api/session/:id/summarize — 手动压缩会话(TUI /compact;runtime 薄代理)
5088
- if (req.url?.match(/^\/api\/session\/[^/]+\/summarize(?:\?|$)/) && req.method === 'POST') {
5089
- const handled = await (0, session_summarize_1.handleSessionSummarize)(req, res, req.url, { getRuntime: () => this.opencodeClient ?? null });
5090
- if (handled)
5091
- return;
5092
- }
4955
+ // POST /api/media/switch — P5 Wave 1 起由 registry dispatch 接管
4956
+ // GET/POST /api/model-config P5 Wave 1 起由 registry dispatch 接管
4957
+ // POST /api/sessions/:id/fork|revert|unrevert + /api/session/:id/summarize
4958
+ // P5 Wave 1 起由 registry dispatch 接管
5093
4959
  // GET /api/usage?sessionID=xxx&projectID=xxx — consolidated usage (summary + providers)
5094
4960
  if (req.url?.match(/^\/api\/usage(?:\?|$)/) && req.method === 'GET') {
5095
4961
  try {
@@ -5352,11 +5218,7 @@ class MafwScheduler {
5352
5218
  });
5353
5219
  return;
5354
5220
  }
5355
- // POST /api/events —— 事件发布(插件/外部程序 UI 通道;契约见 routes/event-publish.ts)
5356
- if (req.url && req.url.startsWith('/api/events') && req.method === 'POST') {
5357
- await (0, event_publish_1.handleEventPublish)({ broadcast: (e) => this.broadcast(e) }, req, res);
5358
- return;
5359
- }
5221
+ // POST /api/events P5 Wave 1 起由 registry dispatch 接管
5360
5222
  // SSE 事件(→ Dashboard / Chat)
5361
5223
  if (req.url && req.url.startsWith('/api/events') && req.method === 'GET') {
5362
5224
  const parsedUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
@@ -5385,8 +5247,9 @@ class MafwScheduler {
5385
5247
  res.end(JSON.stringify({ status: 'ok' }));
5386
5248
  return;
5387
5249
  }
5388
- // Reverse proxy to opencode server for non-MAFW routes
5389
- const serveUrl = config_1.config.server.serveUrl;
5250
+ // Reverse proxy to the agent backend for non-MAFW routes
5251
+ // target from the runtime contract (getBaseUrl), config 仅 bootstrap 兜底
5252
+ const serveUrl = this.opencodeClient?.getBaseUrl?.() ?? config_1.config.server.serveUrl;
5390
5253
  try {
5391
5254
  const proxyUrl = new URL(req.url || '/', serveUrl);
5392
5255
  const proxyReq = http.request(proxyUrl, {
@@ -5443,7 +5306,7 @@ class MafwScheduler {
5443
5306
  server.listen(this.apiPort, () => {
5444
5307
  logger_1.log.info(`[Scheduler] HTTP API on port ${this.apiPort}`);
5445
5308
  logger_1.log.info(`[Scheduler] - POST /register { projectDir, mafwDir }`);
5446
- logger_1.log.info(`[Scheduler] - POST /control { action, goalId, ... }`);
5309
+ logger_1.log.info(`[Scheduler] - POST /control | /api/goals/control { action, goalId, ... }`);
5447
5310
  logger_1.log.info(`[Scheduler] - GET /health`);
5448
5311
  logger_1.log.info(`[Scheduler] - GET /mcp (MCP legacy SSE / StreamableHTTP 405)`);
5449
5312
  logger_1.log.info(`[Scheduler] - POST /mcp (MCP StreamableHTTP stateless + legacy SSE messages)`);