@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.
@@ -26,7 +26,21 @@ function messageToParts(message, parts) {
26
26
  // ─── 适配层实现 ─────────────────────────────────────────────
27
27
  async function createOpencodeAdapter(config) {
28
28
  const { createOpencodeClient } = await import('@opencode-ai/sdk/v2');
29
- const client = createOpencodeClient(config);
29
+ let current = createOpencodeClient(config);
30
+ let clientBaseUrl = config.baseUrl;
31
+ // URL 吸收:runtime 的 spawnServe 会把 sidecar 实际地址写回 config.baseUrl
32
+ // (动态端口前提)。SDK 客户端内部持有构造期 config 快照,感知不到变更——
33
+ // 每次访问按 baseUrl 惰性重建(幂等,无变更零开销)。directNative/
34
+ // healthCheck 直读 config.baseUrl,天然跟随。
35
+ const client = new Proxy(current, {
36
+ get(_t, prop) {
37
+ if (config.baseUrl !== clientBaseUrl) {
38
+ clientBaseUrl = config.baseUrl;
39
+ current = createOpencodeClient({ ...config, baseUrl: config.baseUrl });
40
+ }
41
+ return Reflect.get(current, prop, current);
42
+ },
43
+ });
30
44
  // opencode 原生 V1 路由的 workspace 路由细节收敛在 adapter 内(契约保持
31
45
  // runtime 中立)——带 directory 的调用直连 fetch(V2 SDK 调用不带 workspace 语义)。
32
46
  const directNative = async (path, method, body, directory) => {
@@ -209,7 +223,8 @@ async function createOpencodeAdapter(config) {
209
223
  },
210
224
  question: {
211
225
  async list(opts) {
212
- const result = await client.session.question.list(opts?.directory ? { directory: opts.directory } : undefined);
226
+ // SDK 1.18.x:question OpencodeClient 顶层命名空间(session 上没有)
227
+ const result = await client.question.list(opts?.directory ? { directory: opts.directory } : undefined);
213
228
  const data = unwrap(result);
214
229
  return Array.isArray(data) ? data : data?.items ?? [];
215
230
  },
@@ -218,7 +233,7 @@ async function createOpencodeAdapter(config) {
218
233
  await directNative(`/question/${opts.requestID}/reply`, 'POST', { answers: opts.answers }, opts.directory);
219
234
  return;
220
235
  }
221
- const result = await client.session.question.reply({
236
+ const result = await client.question.reply({
222
237
  requestID: opts.requestID,
223
238
  answers: opts.answers,
224
239
  });
@@ -231,7 +246,7 @@ async function createOpencodeAdapter(config) {
231
246
  await directNative(`/question/${opts.requestID}/reject`, 'POST', undefined, opts.directory);
232
247
  return;
233
248
  }
234
- const result = await client.session.question.reject({ requestID: opts.requestID });
249
+ const result = await client.question.reject({ requestID: opts.requestID });
235
250
  if (result && typeof result === 'object' && 'error' in result && result.error) {
236
251
  throw new Error(String(result.error));
237
252
  }
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ /**
3
+ * P4-A 声明式路由注册表(Shadow Registry 架构)。
4
+ *
5
+ * RouteDef 是 gateway HTTP 端点的声明式描述。Phase 2"shadow 登记"模式下 catalog
6
+ * 只贡献 OpenAPI spec(`toOpenApiPaths()` → emit-openapi 脚本),不参与 dispatch;
7
+ * Phase 3 逐步给 def 挂 handler 并接线 index.ts 主循环,路由器行为由本文件单测锁定。
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.RouteRegistry = void 0;
11
+ exports.toOpenApiPath = toOpenApiPath;
12
+ const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
13
+ /** ':param' 形式 → OpenAPI '{param}' 形式。 */
14
+ function toOpenApiPath(path) {
15
+ return path.replace(/:([A-Za-z0-9_]+)/g, '{$1}');
16
+ }
17
+ class RouteRegistry {
18
+ defs = [];
19
+ operationIds = new Set();
20
+ compiled = null;
21
+ register(...defs) {
22
+ for (const def of defs) {
23
+ if (!def.path.startsWith('/'))
24
+ throw new Error(`RouteDef.path must start with '/': ${def.path}`);
25
+ if (!HTTP_METHODS.includes(def.method))
26
+ throw new Error(`RouteDef.method invalid: ${def.method}`);
27
+ if (!def.operationId)
28
+ throw new Error(`RouteDef.operationId required: ${def.method} ${def.path}`);
29
+ if (this.operationIds.has(def.operationId)) {
30
+ throw new Error(`Duplicate operationId: ${def.operationId}`);
31
+ }
32
+ this.operationIds.add(def.operationId);
33
+ this.defs.push(def);
34
+ this.compiled = null;
35
+ }
36
+ return this;
37
+ }
38
+ list() {
39
+ return this.defs;
40
+ }
41
+ /**
42
+ * P5 Wave 迁移:给已 shadow 登记的 operationId 挂真实 handler。
43
+ * 幂等禁止——重复 attach 抛错(防两个迁移波次互相覆盖)。
44
+ */
45
+ attachHandler(operationId, handler) {
46
+ const def = this.defs.find((d) => d.operationId === operationId);
47
+ if (!def)
48
+ throw new Error(`attachHandler: unknown operationId '${operationId}'(catalog 未登记?)`);
49
+ if (def.handler)
50
+ throw new Error(`attachHandler: '${operationId}' already has a handler`);
51
+ def.handler = handler;
52
+ return this;
53
+ }
54
+ /** 已挂 handler 的 operationId 集合(迁移覆盖率测试用)。 */
55
+ attachedOperationIds() {
56
+ return new Set(this.defs.filter((d) => d.handler).map((d) => d.operationId));
57
+ }
58
+ compile() {
59
+ if (this.compiled)
60
+ return this.compiled;
61
+ this.compiled = this.defs.map((def) => {
62
+ const segments = def.path.split('/').filter((s) => s.length > 0);
63
+ const staticSegments = segments.filter((s) => !s.startsWith(':')).length;
64
+ return { def, segments, staticSegments };
65
+ });
66
+ return this.compiled;
67
+ }
68
+ /**
69
+ * 匹配:静态段多的优先(/api/automations/draft 先于 /api/automations/:id),
70
+ * 同特异度按注册序。pathname 只取 url 的 path 部分(剥 query)。
71
+ */
72
+ match(method, url) {
73
+ const pathname = url.split('?')[0].replace(/\/+$/, '') || '/';
74
+ const pathnameSegments = pathname.split('/').filter((s) => s.length > 0);
75
+ const routes = this.compile()
76
+ .map((r, i) => ({ r, i }))
77
+ .sort((a, b) => b.r.staticSegments - a.r.staticSegments || a.i - b.i);
78
+ for (const { r } of routes) {
79
+ if (r.def.method !== method)
80
+ continue;
81
+ if (r.segments.length !== pathnameSegments.length)
82
+ continue;
83
+ const params = {};
84
+ let ok = true;
85
+ for (let i = 0; i < r.segments.length; i++) {
86
+ const seg = r.segments[i];
87
+ if (seg.startsWith(':')) {
88
+ params[seg.slice(1)] = decodeURIComponent(pathnameSegments[i]);
89
+ }
90
+ else if (seg !== pathnameSegments[i]) {
91
+ ok = false;
92
+ break;
93
+ }
94
+ }
95
+ if (ok)
96
+ return { def: r.def, params };
97
+ }
98
+ return null;
99
+ }
100
+ /** 生成 OpenAPI paths 片段(':param' → '{param}',同 path 多 method 合并)。 */
101
+ toOpenApiPaths() {
102
+ const out = {};
103
+ for (const def of this.defs) {
104
+ const key = toOpenApiPath(def.path);
105
+ const entry = (out[key] ??= {});
106
+ const op = { operationId: def.operationId };
107
+ if (def.tags?.length)
108
+ op.tags = def.tags;
109
+ if (def.summary)
110
+ op.summary = def.summary;
111
+ const params = def.path.split('/').filter((s) => s.startsWith(':')).map((s) => ({
112
+ name: s.slice(1),
113
+ in: 'path',
114
+ required: true,
115
+ schema: { type: 'string' },
116
+ }));
117
+ if (params.length)
118
+ op.parameters = params;
119
+ op.responses = { 200: { description: 'OK' } };
120
+ entry[def.method.toLowerCase()] = op;
121
+ }
122
+ return out;
123
+ }
124
+ }
125
+ exports.RouteRegistry = RouteRegistry;
@@ -0,0 +1,192 @@
1
+ "use strict";
2
+ /**
3
+ * P4-B Shadow Route Catalog —— gateway 全量 HTTP 端点的声明式登记(无 handler)。
4
+ *
5
+ * 两个来源:
6
+ * - SDK_FACING_ROUTES:机械转录自 packages/gateway-sdk/contract/openapi.json
7
+ * (path+method+operationId+tags 与 openapi 逐字一致,'{param}' → ':param'),
8
+ * 漂移由 gateway-sdk/src/contract.test.ts 与 tests/unit/api-contract.test.ts 双向锁定。
9
+ * - NON_SDK_ROUTES:非 SDK 暴露面的补充登记(desktop/TUI/worker/dashboard 专用端点),
10
+ * operationId 自拟(ns.action 风格),tags 统一 'internal'。
11
+ *
12
+ * 刻意不登记:/mcp(传输端点非 REST)、/api/ws(websocket upgrade)、dashboard SPA
13
+ * fallback 与静态资源(/index.html、/assets/*、/static/*)、dashboard 委托内部子路由
14
+ * (/api/memory/{tier}/{id} 等,见 dashboard/api.ts)、/api/events?stream=true(与
15
+ * /api/events 同路径,不单独登记)。P5 Wave 2 反向审计后补齐:devices/mobile 全家、
16
+ * automations history、goals loops :n、A2A agent-card、runtime reload、permissions
17
+ * runtime 代理、legacy singular session delete。
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.buildRouteCatalog = buildRouteCatalog;
21
+ /** SDK-facing 端点(openapi.json 全量 94 条 operation,权威转录,勿手改——改请先改 openapi.json 再同步)。 */
22
+ const SDK_FACING_ROUTES = [
23
+ { method: 'POST', path: '/api/session', operationId: 'session.create', tags: ['session'] },
24
+ { method: 'GET', path: '/api/sessions', operationId: 'session.list', tags: ['session'] },
25
+ { method: 'GET', path: '/api/sessions/:id', operationId: 'session.get', tags: ['session'] },
26
+ { method: 'DELETE', path: '/api/sessions/:id', operationId: 'session.delete', tags: ['session'] },
27
+ { method: 'PATCH', path: '/api/sessions/:id', operationId: 'session.rename', tags: ['session'] },
28
+ { method: 'GET', path: '/api/sessions/:id/messages', operationId: 'session.messages', tags: ['session'] },
29
+ { method: 'GET', path: '/api/sessions/:id/todo', operationId: 'session.todo', tags: ['session'] },
30
+ { method: 'GET', path: '/api/sessions/:id/children', operationId: 'session.children', tags: ['session'] },
31
+ { method: 'GET', path: '/api/sessions/:id/trajectory', operationId: 'session.trajectory', tags: ['session'] },
32
+ { method: 'GET', path: '/api/sessions/:id/token-summary', operationId: 'session.tokenSummary', tags: ['session'] },
33
+ { method: 'POST', path: '/api/sessions/:id/fork', operationId: 'session.fork', tags: ['session'] },
34
+ { method: 'POST', path: '/api/sessions/:id/revert', operationId: 'session.revert', tags: ['session'] },
35
+ { method: 'POST', path: '/api/sessions/:id/unrevert', operationId: 'session.unrevert', tags: ['session'] },
36
+ { method: 'POST', path: '/api/session/:id/abort', operationId: 'session.abort', tags: ['session'] },
37
+ { method: 'POST', path: '/api/session/:id/summarize', operationId: 'session.summarize', tags: ['session'] },
38
+ { method: 'POST', path: '/api/session/:id/prompt', operationId: 'session.prompt', tags: ['session'] },
39
+ { method: 'POST', path: '/api/session/:id/promptAsync', operationId: 'session.promptAsync', tags: ['session'] },
40
+ { method: 'POST', path: '/api/session/:id/command', operationId: 'session.command', tags: ['session'] },
41
+ { method: 'GET', path: '/api/usage', operationId: 'session.usage', tags: ['session'] },
42
+ { method: 'GET', path: '/api/usage/summary', operationId: 'session.usageSummary', tags: ['session'] },
43
+ { method: 'GET', path: '/api/usage/plugins', operationId: 'session.usagePluginsList', tags: ['session'] },
44
+ { method: 'POST', path: '/api/usage/plugins/reload', operationId: 'session.usagePluginsReload', tags: ['session'] },
45
+ { method: 'POST', path: '/api/usage/plugins/create', operationId: 'session.usagePluginsCreate', tags: ['session'] },
46
+ { method: 'GET', path: '/api/usage/plugins/:name/source', operationId: 'session.usagePluginSourceGet', tags: ['session'] },
47
+ { method: 'PUT', path: '/api/usage/plugins/:name/source', operationId: 'session.usagePluginSourcePut', tags: ['session'] },
48
+ { method: 'DELETE', path: '/api/usage/plugins/:name', operationId: 'session.usagePluginsDelete', tags: ['session'] },
49
+ { method: 'POST', path: '/api/usage/plugins/:name/test', operationId: 'session.usagePluginTest', tags: ['session'] },
50
+ { method: 'GET', path: '/command', operationId: 'command.list', tags: ['command'] },
51
+ { method: 'GET', path: '/skill', operationId: 'skill.list', tags: ['skill'] },
52
+ { method: 'POST', path: '/api/mafw-commands/run', operationId: 'mafwCommands.run', tags: ['mafwCommands'] },
53
+ { method: 'GET', path: '/api/manager/session', operationId: 'manager.session', tags: ['manager'] },
54
+ { method: 'POST', path: '/api/manager/session/rotate', operationId: 'manager.rotate', tags: ['manager'] },
55
+ { method: 'GET', path: '/api/projects', operationId: 'project.list', tags: ['project'] },
56
+ { method: 'GET', path: '/api/projects/current', operationId: 'project.current', tags: ['project'] },
57
+ { method: 'POST', path: '/register', operationId: 'project.setCurrent', tags: ['project'] },
58
+ { method: 'GET', path: '/api/events', operationId: 'event.subscribe', tags: ['event'] },
59
+ { method: 'POST', path: '/api/events', operationId: 'event.publish', tags: ['event'] },
60
+ { method: 'GET', path: '/api/runtime', operationId: 'runtime.get', tags: ['runtime'] },
61
+ { method: 'POST', path: '/api/runtime/switch', operationId: 'runtime.switch', tags: ['runtime'] },
62
+ { method: 'POST', path: '/api/runtime/restart-agent', operationId: 'runtime.restartAgent', tags: ['runtime'] },
63
+ // runtime.reload(POST /api/runtime/reload)在下方 NON_SDK 补遗块登记(SDK 不调用 → internal)
64
+ { method: 'GET', path: '/api/plugins', operationId: 'plugins.list', tags: ['plugins'] },
65
+ { method: 'POST', path: '/api/plugins/install', operationId: 'plugins.install', tags: ['plugins'] },
66
+ { method: 'POST', path: '/api/plugins/enable', operationId: 'plugins.enable', tags: ['plugins'] },
67
+ { method: 'POST', path: '/api/plugins/disable', operationId: 'plugins.disable', tags: ['plugins'] },
68
+ { method: 'POST', path: '/api/plugins/delete', operationId: 'plugins.delete', tags: ['plugins'] },
69
+ { method: 'GET', path: '/api/config', operationId: 'config.get', tags: ['config'] },
70
+ { method: 'PUT', path: '/api/config', operationId: 'config.set', tags: ['config'] },
71
+ { method: 'GET', path: '/api/opencode-config', operationId: 'opencodeConfig.get', tags: ['opencodeConfig'] },
72
+ { method: 'PATCH', path: '/api/opencode-config', operationId: 'opencodeConfig.update', tags: ['opencodeConfig'] },
73
+ { method: 'GET', path: '/api/goals', operationId: 'goals.list', tags: ['goals'] },
74
+ { method: 'GET', path: '/api/goals/:id', operationId: 'goals.get', tags: ['goals'] },
75
+ { method: 'GET', path: '/api/goals/:id/sessions', operationId: 'goals.sessions', tags: ['goals'] },
76
+ { method: 'POST', path: '/api/goals/:goalId/questions/:questionId/respond', operationId: 'goals.respondQuestion', tags: ['goals'] },
77
+ { method: 'POST', path: '/api/work/:goalId/validate', operationId: 'goals.validate', tags: ['goals'] },
78
+ { method: 'POST', path: '/api/goals/control', operationId: 'goals.control', tags: ['goals'] },
79
+ { method: 'GET', path: '/api/memory/search', operationId: 'memory.search', tags: ['memory'] },
80
+ { method: 'GET', path: '/api/memory/merged-search', operationId: 'memory.mergedSearch', tags: ['memory'] },
81
+ { method: 'GET', path: '/api/memory/energy-distribution', operationId: 'memory.getEnergyDistribution', tags: ['memory'] },
82
+ { method: 'GET', path: '/api/l5/axioms', operationId: 'memory.getL5Axioms', tags: ['memory'] },
83
+ { method: 'DELETE', path: '/api/memory/:id', operationId: 'memory.delete', tags: ['memory'] },
84
+ { method: 'GET', path: '/api/memory/sticky', operationId: 'memory.listSticky', tags: ['memory'] },
85
+ { method: 'POST', path: '/api/memory/pin', operationId: 'memory.setSticky', tags: ['memory'] },
86
+ { method: 'GET', path: '/api/memory/embedding-config', operationId: 'embedding.get', tags: ['embedding'] },
87
+ { method: 'POST', path: '/api/memory/embedding-config', operationId: 'embedding.update', tags: ['embedding'] },
88
+ { method: 'GET', path: '/api/approvals', operationId: 'approvals.list', tags: ['approvals'] },
89
+ { method: 'POST', path: '/api/approvals/:id/respond', operationId: 'approvals.respond', tags: ['approvals'] },
90
+ { method: 'GET', path: '/api/triage', operationId: 'triage.list', tags: ['triage'] },
91
+ { method: 'POST', path: '/api/triage/:id/dismiss', operationId: 'triage.dismiss', tags: ['triage'] },
92
+ { method: 'POST', path: '/api/triage/:id/confirm', operationId: 'triage.confirm', tags: ['triage'] },
93
+ { method: 'POST', path: '/api/triage/:id/reject', operationId: 'triage.reject', tags: ['triage'] },
94
+ { method: 'POST', path: '/api/triage/:id/propose', operationId: 'triage.propose', tags: ['triage'] },
95
+ { method: 'GET', path: '/api/questions', operationId: 'questions.list', tags: ['questions'] },
96
+ { method: 'POST', path: '/api/questions/:id/reply', operationId: 'questions.reply', tags: ['questions'] },
97
+ { method: 'POST', path: '/api/questions/:id/reject', operationId: 'questions.reject', tags: ['questions'] },
98
+ { method: 'GET', path: '/api/permissions', operationId: 'permissions.list', tags: ['permissions'] },
99
+ { method: 'POST', path: '/api/permissions/:id/reply', operationId: 'permissions.reply', tags: ['permissions'] },
100
+ { method: 'POST', path: '/api/chat', operationId: 'chat.send', tags: ['chat'] },
101
+ { method: 'POST', path: '/api/chat/enriched', operationId: 'chat.sendEnriched', tags: ['chat'] },
102
+ { method: 'GET', path: '/api/media/plugins', operationId: 'media.plugins', tags: ['media'] },
103
+ { method: 'POST', path: '/api/media/switch', operationId: 'media.switch', tags: ['media'] },
104
+ { method: 'POST', path: '/api/media/upload', operationId: 'media.upload', tags: ['media'] },
105
+ { method: 'POST', path: '/api/media/upload-and-create', operationId: 'media.uploadAndCreate', tags: ['media'] },
106
+ { method: 'POST', path: '/a2a', operationId: 'media.createTask', tags: ['media'] },
107
+ { method: 'GET', path: '/a2a/artifacts/:id', operationId: 'media.artifact', tags: ['media'] },
108
+ { method: 'POST', path: '/api/tts', operationId: 'tts.speak', tags: ['tts'] },
109
+ { method: 'GET', path: '/api/tts/voices', operationId: 'tts.voices', tags: ['tts'] },
110
+ { method: 'POST', path: '/api/tts/stream', operationId: 'tts.speakStream', tags: ['tts'] },
111
+ { method: 'GET', path: '/api/provider', operationId: 'providers.list', tags: ['providers'] },
112
+ { method: 'GET', path: '/api/agents', operationId: 'agents.list', tags: ['agents'] },
113
+ { method: 'GET', path: '/api/model-config', operationId: 'models.get', tags: ['models'] },
114
+ { method: 'POST', path: '/api/model-config', operationId: 'models.update', tags: ['models'] },
115
+ { method: 'GET', path: '/api/automations', operationId: 'automations.list', tags: ['automations'] },
116
+ { method: 'PUT', path: '/api/automations/:id', operationId: 'automations.toggle', tags: ['automations'] },
117
+ { method: 'POST', path: '/api/automations/draft', operationId: 'automations.draft', tags: ['automations'] },
118
+ ];
119
+ /** 非 SDK 端点补充登记(desktop/TUI/worker/dashboard 专用面),operationId 自拟,tags 'internal'。 */
120
+ const NON_SDK_ROUTES = [
121
+ // Python 内核(§5.17)
122
+ { method: 'POST', path: '/api/python/execute', operationId: 'python.execute', tags: ['internal'] },
123
+ { method: 'POST', path: '/api/python/restart', operationId: 'python.restart', tags: ['internal'] },
124
+ { method: 'GET', path: '/api/python/status', operationId: 'python.status', tags: ['internal'] },
125
+ // 观察捕获 / 边界 recall / pinned 披露层(§5.11/§5.13)
126
+ { method: 'POST', path: '/api/obs/capture', operationId: 'obs.capture', tags: ['internal'] },
127
+ { method: 'GET', path: '/api/recall/context', operationId: 'recall.context', tags: ['internal'] },
128
+ { method: 'GET', path: '/api/recall/pinned', operationId: 'recall.pinned', tags: ['internal'] },
129
+ // 记忆补充(memory.mergedSearch 已在 SDK-facing,不重复)
130
+ { method: 'POST', path: '/api/memory/add', operationId: 'memory.add', tags: ['internal'] },
131
+ { method: 'GET', path: '/api/memory/get', operationId: 'memory.get', tags: ['internal'] },
132
+ { method: 'GET', path: '/api/memory/stats', operationId: 'memory.stats', tags: ['internal'] },
133
+ { method: 'POST', path: '/api/memory/embeddings/backfill', operationId: 'memory.embeddingsBackfill', tags: ['internal'] },
134
+ { method: 'GET', path: '/api/l5/heuristics', operationId: 'memory.getL5Heuristics', tags: ['internal'] },
135
+ // Goal / 编排(§5.20)
136
+ { method: 'GET', path: '/api/goals/:id/loops', operationId: 'goals.loops', tags: ['internal'] },
137
+ { method: 'POST', path: '/api/work/:goalId/complete', operationId: 'goals.complete', tags: ['internal'] },
138
+ { method: 'POST', path: '/control', operationId: 'goals.controlLegacy', summary: 'MCP 兼容别名,与 /api/goals/control 同 handler', tags: ['internal'] },
139
+ { method: 'GET', path: '/api/orchestration/outcomes', operationId: 'orchestration.outcomes', tags: ['internal'] },
140
+ // 媒体补充(media.upload / uploadAndCreate / switch / plugins GET / a2a / artifacts GET 已在 SDK-facing)
141
+ { method: 'POST', path: '/api/media/create-task', operationId: 'media.createTaskApi', tags: ['internal'] },
142
+ { method: 'GET', path: '/api/media/resolve-task/:id', operationId: 'media.resolveTask', tags: ['internal'] },
143
+ { method: 'POST', path: '/api/media/analyze-audio', operationId: 'media.analyzeAudio', tags: ['internal'] },
144
+ { method: 'POST', path: '/api/media/plugins/reload', operationId: 'media.pluginsReload', tags: ['internal'] },
145
+ // 用量 dashboard / 健康
146
+ { method: 'GET', path: '/api/stats', operationId: 'dashboard.stats', tags: ['internal'] },
147
+ { method: 'GET', path: '/api/costs/:window', operationId: 'dashboard.costs', tags: ['internal'] },
148
+ { method: 'GET', path: '/api/alignment', operationId: 'dashboard.alignment', tags: ['internal'] },
149
+ { method: 'POST', path: '/api/feedback', operationId: 'feedback.record', tags: ['internal'] },
150
+ { method: 'GET', path: '/api/user-answers/:id', operationId: 'dashboard.userAnswer', tags: ['internal'] },
151
+ { method: 'GET', path: '/api/sessions/:id/metrics', operationId: 'session.metrics', tags: ['internal'] },
152
+ { method: 'GET', path: '/api/health', operationId: 'health.check', tags: ['internal'] },
153
+ // Gateway 控制(goal PAUSE/ABORT 等控制面)
154
+ { method: 'POST', path: '/api/gateway/pause', operationId: 'gateway.pause', tags: ['internal'] },
155
+ { method: 'POST', path: '/api/gateway/resume', operationId: 'gateway.resume', tags: ['internal'] },
156
+ { method: 'POST', path: '/api/gateway/cancel', operationId: 'gateway.cancel', tags: ['internal'] },
157
+ { method: 'POST', path: '/api/gateway/checkpoint', operationId: 'gateway.checkpoint', tags: ['internal'] },
158
+ { method: 'POST', path: '/api/gateway/rollback', operationId: 'gateway.rollback', tags: ['internal'] },
159
+ // 移动端(P5 Wave 2 反向审计后全量登记)
160
+ { method: 'POST', path: '/api/mobile/media/tasks/:id/ask', operationId: 'mobile.mediaTaskAsk', tags: ['internal'] },
161
+ { method: 'POST', path: '/api/mobile/media/tasks', operationId: 'mobile.mediaTaskUpload', tags: ['internal'] },
162
+ { method: 'GET', path: '/api/mobile/tts/artifacts/:id', operationId: 'mobile.ttsArtifact', tags: ['internal'] },
163
+ { method: 'GET', path: '/api/mobile/devices', operationId: 'mobile.devicesList', tags: ['internal'] },
164
+ { method: 'DELETE', path: '/api/mobile/devices/:id', operationId: 'mobile.devicesDelete', tags: ['internal'] },
165
+ { method: 'POST', path: '/api/mobile/devices/register', operationId: 'mobile.devicesRegister', tags: ['internal'] },
166
+ { method: 'GET', path: '/api/mobile/pairing-code', operationId: 'mobile.pairingCode', tags: ['internal'] },
167
+ { method: 'POST', path: '/api/mobile/pairing/verify', operationId: 'mobile.pairingVerify', tags: ['internal'] },
168
+ // 设备注册(mobile 的 legacy 别名族)
169
+ { method: 'POST', path: '/api/devices', operationId: 'devices.register', tags: ['internal'] },
170
+ { method: 'GET', path: '/api/devices', operationId: 'devices.list', tags: ['internal'] },
171
+ { method: 'DELETE', path: '/api/devices/:id', operationId: 'devices.delete', tags: ['internal'] },
172
+ // P5 Wave 2 反向审计补遗
173
+ { method: 'GET', path: '/.well-known/agent-card.json', operationId: 'a2a.agentCard', tags: ['internal'] },
174
+ { method: 'GET', path: '/api/automations/:id/history', operationId: 'automations.history', tags: ['internal'] },
175
+ { method: 'GET', path: '/api/goals/:id/loops/:n', operationId: 'goals.loopsAt', tags: ['internal'] },
176
+ { method: 'POST', path: '/api/runtime/reload', operationId: 'runtime.reload', tags: ['internal'] },
177
+ { method: 'POST', path: '/api/sessions/:sid/permissions/:rid', operationId: 'permissions.replyRuntime', tags: ['internal'] },
178
+ { method: 'DELETE', path: '/api/session/:id', operationId: 'session.deleteLegacy', tags: ['internal'] },
179
+ // Eval / LLM 压缩
180
+ { method: 'POST', path: '/api/eval/chat/completions', operationId: 'eval.chatCompletions', tags: ['internal'] },
181
+ { method: 'POST', path: '/api/llm/compress', operationId: 'llm.compress', tags: ['internal'] },
182
+ // 跨 worktree 记忆融合(§5.3)
183
+ { method: 'POST', path: '/api/merge-memory', operationId: 'memory.merge', tags: ['internal'] },
184
+ // 根健康检查(dashboard/桌面探测用)
185
+ { method: 'GET', path: '/health', operationId: 'health.root', tags: ['internal'] },
186
+ ];
187
+ /** gateway 全量 HTTP 端点 shadow 登记(无 handler;Phase 3 逐条挂 handler 前不参与 dispatch)。
188
+ * 每次调用返回全新 def 对象(浅拷贝)——attachHandler 会原地改写 def.handler,
189
+ * 共享模块级常量会让第二次 build 的 registry 带上一次的 handler(测试已抓过)。 */
190
+ function buildRouteCatalog() {
191
+ return [...SDK_FACING_ROUTES, ...NON_SDK_ROUTES].map((d) => ({ ...d }));
192
+ }
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleTriageDismiss = handleTriageDismiss;
4
+ function json(res, status, payload) {
5
+ res.writeHead(status, { 'Content-Type': 'application/json' });
6
+ res.end(JSON.stringify(payload));
7
+ }
8
+ /**
9
+ * POST /api/triage/:id/dismiss — 忽略 triage 项(SDK triage.dismiss 的服务端落地)。
10
+ * 语义 = reject(state REJECTED),仅账本 reason 区分(triage_dismissed),
11
+ * 供 SDK contract 对齐:此前该路由不存在,调用会落入 dashboard 兜底返回 HTML。
12
+ * 返回 handled=false 表示路径不匹配,交给后续路由。
13
+ */
14
+ async function handleTriageDismiss(req, res, url, deps) {
15
+ const m = url.match(/^\/api\/triage\/([^/]+)\/dismiss(?:\?|$)/);
16
+ if (!m || req.method !== 'POST')
17
+ return false;
18
+ const triageId = decodeURIComponent(m[1]);
19
+ const ok = deps.rejectTriage(triageId);
20
+ try {
21
+ deps.appendLedger?.({
22
+ timestamp: new Date().toISOString(),
23
+ event: 'AUTOMATION_TRIGGERED',
24
+ source: 'user',
25
+ reason: 'triage_dismissed',
26
+ details: { triageId },
27
+ });
28
+ }
29
+ catch { /* 账本失败不阻塞 */ }
30
+ json(res, 200, { status: ok ? 'dismissed' : 'not_found' });
31
+ return true;
32
+ }
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.attachWave1Handlers = attachWave1Handlers;
4
+ const config_1 = require("../config");
5
+ const goal_sessions_1 = require("./goal-sessions");
6
+ const triage_dismiss_1 = require("./triage-dismiss");
7
+ const manager_rotate_1 = require("./manager-rotate");
8
+ const embedding_config_1 = require("./embedding-config");
9
+ const model_config_1 = require("./model-config");
10
+ const session_branch_1 = require("./session-branch");
11
+ const session_mutations_1 = require("./session-mutations");
12
+ const session_summarize_1 = require("./session-summarize");
13
+ const event_publish_1 = require("./event-publish");
14
+ const media_switch_1 = require("./media-switch");
15
+ const usage_plugins_1 = require("./usage-plugins");
16
+ /** 绑定 Wave 1 的 22 条路由。index.ts 在 createServer 之前调用一次。 */
17
+ function attachWave1Handlers(registry, gw) {
18
+ const H = (fn) => fn;
19
+ registry.attachHandler('goals.sessions', H(async (req, res, params) => (0, goal_sessions_1.handleGoalSessions)(req, res, req.url || '', {
20
+ listGoalSessions: (goalId) => gw.getGatewayDb().listGoalSessions(goalId),
21
+ getSession: (sessionID) => gw.opencodeClient?.session.get({ sessionID }).catch(() => null),
22
+ })));
23
+ registry.attachHandler('triage.dismiss', H(async (req, res, params) => (0, triage_dismiss_1.handleTriageDismiss)(req, res, req.url || '', {
24
+ rejectTriage: (id) => gw.automationEngine?.rejectTriage(id) ?? false,
25
+ appendLedger: (entry) => gw.ledger?.append(entry),
26
+ })));
27
+ registry.attachHandler('manager.rotate', H(async (req, res) => (0, manager_rotate_1.handleManagerRotate)(req, res, gw.rotateDeps())));
28
+ registry.attachHandler('embedding.get', H(async (req, res) => (0, embedding_config_1.handleEmbeddingConfigGet)(req, res, gw.embeddingConfigDeps())));
29
+ registry.attachHandler('embedding.update', H(async (req, res) => (0, embedding_config_1.handleEmbeddingConfigUpdate)(req, res, gw.embeddingConfigDeps())));
30
+ registry.attachHandler('models.get', H(async (req, res) => (0, model_config_1.handleModelConfigGet)(req, res, gw.modelConfigDeps())));
31
+ registry.attachHandler('models.update', H(async (req, res) => (0, model_config_1.handleModelConfigUpdate)(req, res, gw.modelConfigDeps())));
32
+ registry.attachHandler('session.fork', H(async (req, res, params) => (0, session_branch_1.handleSessionBranch)(req, res, req.url || '', { getRuntime: () => gw.opencodeClient })));
33
+ registry.attachHandler('session.revert', H(async (req, res, params) => (0, session_branch_1.handleSessionBranch)(req, res, req.url || '', { getRuntime: () => gw.opencodeClient })));
34
+ registry.attachHandler('session.unrevert', H(async (req, res, params) => (0, session_branch_1.handleSessionBranch)(req, res, req.url || '', { getRuntime: () => gw.opencodeClient })));
35
+ registry.attachHandler('session.delete', H(async (req, res) => (0, session_mutations_1.handleSessionMutations)(req, res, {
36
+ getCapabilities: () => gw.runtimeCaps,
37
+ getClient: () => (gw.opencodeClient ?? null),
38
+ })));
39
+ registry.attachHandler('session.rename', H(async (req, res) => (0, session_mutations_1.handleSessionMutations)(req, res, {
40
+ getCapabilities: () => gw.runtimeCaps,
41
+ getClient: () => (gw.opencodeClient ?? null),
42
+ })));
43
+ registry.attachHandler('session.summarize', H(async (req, res) => (0, session_summarize_1.handleSessionSummarize)(req, res, req.url || '', { getRuntime: () => gw.opencodeClient })));
44
+ registry.attachHandler('event.publish', H(async (req, res) => (0, event_publish_1.handleEventPublish)({ broadcast: (e) => gw.broadcast(e) }, req, res)));
45
+ registry.attachHandler('media.switch', H(async (req, res) => (0, media_switch_1.handleMediaSwitch)(req, res, {
46
+ persist: (o) => config_1.config.persistOverrides(o),
47
+ reloadPlugins: async () => { await gw.mediaPluginLoader?.reload(); },
48
+ availableEngines: () => (gw.mediaPluginLoader?.getState().map((s) => s.name).filter((n) => !!n) ?? []),
49
+ currentMedia: () => config_1.config.raw.media,
50
+ })));
51
+ const usageDeps = () => gw.usagePluginsDeps();
52
+ registry.attachHandler('session.usagePluginsList', H(async (req, res) => (0, usage_plugins_1.handleUsagePluginsList)(req, res, usageDeps())));
53
+ registry.attachHandler('session.usagePluginsReload', H(async (req, res) => {
54
+ await gw.pluginLoader?.reload();
55
+ await (0, usage_plugins_1.handleUsagePluginsList)(req, res, usageDeps());
56
+ }));
57
+ registry.attachHandler('session.usagePluginsCreate', H(async (req, res) => (0, usage_plugins_1.handleUsagePluginCreate)(req, res, usageDeps())));
58
+ registry.attachHandler('session.usagePluginSourceGet', H(async (req, res, params) => (0, usage_plugins_1.handleUsagePluginSourceGet)(req, res, usageDeps(), decodeURIComponent(params.name))));
59
+ registry.attachHandler('session.usagePluginSourcePut', H(async (req, res, params) => (0, usage_plugins_1.handleUsagePluginSourcePut)(req, res, usageDeps(), decodeURIComponent(params.name))));
60
+ registry.attachHandler('session.usagePluginTest', H(async (req, res, params) => (0, usage_plugins_1.handleUsagePluginTest)(req, res, usageDeps(), decodeURIComponent(params.name))));
61
+ registry.attachHandler('session.usagePluginsDelete', H(async (req, res, params) => (0, usage_plugins_1.handleUsagePluginDelete)(req, res, usageDeps(), decodeURIComponent(params.name))));
62
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.attachWave2Handlers = attachWave2Handlers;
4
+ const runtime_switch_1 = require("./runtime-switch");
5
+ const restart_agent_1 = require("./restart-agent");
6
+ const plugins_1 = require("./plugins");
7
+ const hub_1 = require("../plugins/hub");
8
+ const logger_1 = require("../core/utils/logger");
9
+ /** 绑定 Wave 2 的 9 条路由。index.ts 在 createServer 之前调用一次。 */
10
+ function attachWave2Handlers(registry, gw) {
11
+ const H = (fn) => fn;
12
+ registry.attachHandler('runtime.get', H(async (req, res) => (0, runtime_switch_1.handleRuntimeGet)(req, res, gw.runtimeDeps())));
13
+ registry.attachHandler('runtime.switch', H(async (req, res) => {
14
+ if (gw.runtimeSwitchBlocked()) {
15
+ res.writeHead(409, { 'Content-Type': 'application/json' });
16
+ res.end(JSON.stringify({ error: 'Cannot switch runtime during agent restart' }));
17
+ return;
18
+ }
19
+ gw.beginRuntimeSwitch();
20
+ try {
21
+ await (0, runtime_switch_1.handleRuntimeSwitch)(req, res, gw.runtimeDeps());
22
+ }
23
+ finally {
24
+ gw.endRuntimeSwitch();
25
+ }
26
+ }));
27
+ registry.attachHandler('runtime.reload', H(async (req, res) => (0, runtime_switch_1.handleRuntimeReload)(req, res, gw.runtimeDeps())));
28
+ registry.attachHandler('runtime.restartAgent', H(async (req, res) => (0, restart_agent_1.handleRestartAgent)(req, res, gw.restartAgentDeps())));
29
+ const withCleanup = async (req, fn) => {
30
+ try {
31
+ const cleaned = (0, hub_1.cleanupExamples)(gw.pluginHubDeps().hub);
32
+ if (cleaned.removed.length)
33
+ logger_1.log.info(`[PluginsHub] removed stale examples: ${cleaned.removed.length}`);
34
+ if (cleaned.failed.length)
35
+ logger_1.log.warn(`[PluginsHub] cleanupExamples failed: ${cleaned.failed.join(', ')}`);
36
+ }
37
+ catch (err) {
38
+ logger_1.log.warn(`[PluginsHub] cleanupExamples error: ${err.message}`);
39
+ }
40
+ await fn();
41
+ };
42
+ registry.attachHandler('plugins.list', H(async (req, res) => withCleanup(req, () => (0, plugins_1.handlePluginsList)(req, res, gw.pluginHubDeps()))));
43
+ registry.attachHandler('plugins.install', H(async (req, res) => withCleanup(req, () => (0, plugins_1.handlePluginsInstall)(req, res, gw.pluginHubDeps()))));
44
+ registry.attachHandler('plugins.enable', H(async (req, res) => withCleanup(req, () => (0, plugins_1.handlePluginsEnable)(req, res, gw.pluginHubDeps()))));
45
+ registry.attachHandler('plugins.disable', H(async (req, res) => withCleanup(req, () => (0, plugins_1.handlePluginsDisable)(req, res, gw.pluginHubDeps()))));
46
+ registry.attachHandler('plugins.delete', H(async (req, res) => withCleanup(req, () => (0, plugins_1.handlePluginsDelete)(req, res, gw.pluginHubDeps()))));
47
+ }
@@ -259,7 +259,9 @@ async function createOpencodeRuntime(config) {
259
259
  },
260
260
  },
261
261
  getBaseUrl() {
262
- return process.env.MAFW_SERVER_SERVE_URL || config_1.config.server.serveUrl;
262
+ // 地址归 runtime 所有:bootstrap 取配置,spawnServe 吸收 sidecar 实际
263
+ // URL 后(含动态端口),gateway 一律经此读取,不直连配置。
264
+ return process.env.MAFW_SERVER_SERVE_URL || config.baseUrl;
263
265
  },
264
266
  async healthCheck() {
265
267
  try {
@@ -325,7 +327,21 @@ async function createOpencodeRuntime(config) {
325
327
  // Gateway core never hardcodes agent-specific spawn details.
326
328
  if (!rt.external) {
327
329
  rt.agentProcess = {
328
- spawnServe: (opts) => (0, serve_sidecar_1.startServeSidecar)(opts),
330
+ spawnServe: async (opts) => {
331
+ // host/port 缺省由 runtime 自定(serve 端口是实现细节,gateway 不传)
332
+ const sidecar = await (0, serve_sidecar_1.startServeSidecar)({
333
+ host: opts.host ?? config_1.config.server.serveHost ?? '127.0.0.1',
334
+ port: opts.port ?? config_1.config.server.servePort,
335
+ timeoutMs: opts.timeoutMs,
336
+ onOutput: opts.onOutput,
337
+ onExit: opts.onExit,
338
+ });
339
+ // 吸收 sidecar 报告的实际地址(可能与请求端口不同,如动态分配):
340
+ // 之后 getBaseUrl/healthCheck/adapter 请求全部跟随实际 URL。
341
+ config.baseUrl = sidecar.url;
342
+ return sidecar;
343
+ },
344
+ killServe: () => (0, serve_sidecar_1.killServePort)(config_1.config.server.servePort),
329
345
  async restart() {
330
346
  logger_1.log.info('[Runtime] agentProcess.restart() — killing serve (gateway orchestrates respawn)');
331
347
  (0, serve_sidecar_1.killServePort)(config_1.config.server.servePort);