@jack200714/mafw 4.8.0 → 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.
Files changed (42) hide show
  1. package/README.md +27 -3
  2. package/gateway/dist/core/manager/goal-snapshot.js +2 -2
  3. package/gateway/dist/core/manager/manager-session-runtime.js +59 -0
  4. package/gateway/dist/core/manager/milestone-push.js +25 -10
  5. package/gateway/dist/index.js +465 -440
  6. package/gateway/dist/media/media-plugin-loader.js +25 -1
  7. package/gateway/dist/media/resolve-prompt.js +20 -0
  8. package/gateway/dist/memory/gateway-db.js +23 -0
  9. package/gateway/dist/opencode-adapter.js +53 -4
  10. package/gateway/dist/plugins/package-context.js +24 -0
  11. package/gateway/dist/plugins/package-host.js +331 -0
  12. package/gateway/dist/plugins/package-types.js +2 -0
  13. package/gateway/dist/recall/gateway-db-migrate.js +5 -2
  14. package/gateway/dist/recall/redact.js +53 -0
  15. package/gateway/dist/recall/turn-pipeline.js +2 -0
  16. package/gateway/dist/routes/event-publish.js +44 -0
  17. package/gateway/dist/routes/plugins.js +4 -1
  18. package/gateway/dist/routes/registry.js +125 -0
  19. package/gateway/dist/routes/route-catalog.js +192 -0
  20. package/gateway/dist/routes/triage-dismiss.js +32 -0
  21. package/gateway/dist/routes/waitwhat-command.js +43 -0
  22. package/gateway/dist/routes/wave1-handlers.js +62 -0
  23. package/gateway/dist/routes/wave2-handlers.js +47 -0
  24. package/gateway/dist/runtime/contract.js +4 -1
  25. package/gateway/dist/runtime/event-broadcast.js +8 -0
  26. package/gateway/dist/runtime/loader.js +22 -1
  27. package/gateway/dist/runtime/normalize.js +13 -0
  28. package/gateway/dist/runtime/opencode-runtime.js +18 -2
  29. package/gateway/dist/runtime/pi/pi-approval-bridge.js +12 -2
  30. package/gateway/dist/runtime/pi/pi-approval-extension.js +11 -3
  31. package/gateway/dist/runtime/pi/pi-session.js +21 -3
  32. package/gateway/dist/runtime/plugins/pi-runtime.js +6 -3
  33. package/gateway/dist/runtime/serve-sidecar.js +4 -1
  34. package/gateway/dist/runtime/serve-supervisor.js +19 -20
  35. package/gateway/dist/runtime/validate.js +39 -0
  36. package/gateway/dist/skills/manager-identity.js +6 -1
  37. package/gateway/dist/usage/builtin-plugins/gateway.js +91 -36
  38. package/gateway/dist/usage/plugin-context.js +42 -2
  39. package/gateway/dist/usage/plugin-loader.js +32 -2
  40. package/gateway/package.json +2 -1
  41. package/package.json +3 -1
  42. package/packages/tui/dist/cli.js +114 -51
@@ -51,7 +51,10 @@ async function guarded(res, fn) {
51
51
  }
52
52
  }
53
53
  async function handlePluginsList(_req, res, deps) {
54
- await guarded(res, async () => ({ plugins: (0, hub_1.listPlugins)(deps.hub) }));
54
+ await guarded(res, async () => ({
55
+ plugins: (0, hub_1.listPlugins)(deps.hub),
56
+ packages: deps.hub.getPackages?.() ?? [],
57
+ }));
55
58
  }
56
59
  async function handlePluginsInstall(req, res, deps) {
57
60
  await guarded(res, async () => {
@@ -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,43 @@
1
+ "use strict";
2
+ // /waitwhat command (UI-driven, via /api/mafw-commands/run): the user signals
3
+ // "that last reply didn't land" and the agent re-pitches its own last message
4
+ // in simplified language, using the project's CONTEXT.md glossary when one
5
+ // exists. Inspired by mattpocock/skills wait-what (MIT).
6
+ //
7
+ // The re-pitch is prompted INTO THE SAME session so every connected client
8
+ // (desktop / TUI / opencode) sees the answer inline via the event stream.
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.buildWaitwhatPrompt = buildWaitwhatPrompt;
11
+ exports.runWaitwhat = runWaitwhat;
12
+ function buildWaitwhatPrompt(original) {
13
+ return [
14
+ '[/waitwhat] 用户没看懂你上一条回复。请把它重述一遍:',
15
+ '1. 先用一两句补上"我们在做什么、刚才说到哪"的上下文定位;',
16
+ '2. 然后用简明语言重述:短句、一次一个概念、避免嵌套行话(STE100 简化技术英语的风格);',
17
+ '3. 如果项目根目录有 CONTEXT.md,先读它并用其中的项目术语(ubiquitous language)来表达;',
18
+ '4. 只是重述,不要新增内容、不引入新话题。',
19
+ '',
20
+ '—— 需要重述的上一条回复原文 ——',
21
+ original,
22
+ ].join('\n');
23
+ }
24
+ /**
25
+ * Re-pitch the last assistant message of the session. Returns ok:false with
26
+ * an error string (without prompting) when there is nothing to re-pitch.
27
+ */
28
+ async function runWaitwhat(sessionID, deps) {
29
+ const messages = await deps.listMessages(sessionID);
30
+ const lastAssistant = [...(Array.isArray(messages) ? messages : [])]
31
+ .reverse()
32
+ .find((m) => m?.info?.role === 'assistant');
33
+ const original = (lastAssistant?.parts || [])
34
+ .filter((p) => p?.type === 'text' && typeof p?.text === 'string')
35
+ .map((p) => p.text)
36
+ .join('\n')
37
+ .trim();
38
+ if (!original) {
39
+ return { ok: false, error: 'no assistant message to re-pitch' };
40
+ }
41
+ await deps.promptAsync(sessionID, buildWaitwhatPrompt(original));
42
+ return { ok: true };
43
+ }
@@ -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
+ }
@@ -15,7 +15,10 @@ function fullCapabilities() {
15
15
  agentProcessApi: true,
16
16
  completionApi: true,
17
17
  sessionBranchApi: true,
18
- turnBudgetApi: true,
18
+ // opencode 适配器不转发 maxTurns/maxCostUsd(SDK 无对应字段)——预算由
19
+ // gateway 侧 BudgetGuard 承担;声明 true 会让 attachBudgetGuardForGoal
20
+ // 跳过挂载,goal 预算在默认 runtime 上完全失效。
21
+ turnBudgetApi: false,
19
22
  questionApi: true,
20
23
  };
21
24
  }
@@ -12,6 +12,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.opencodeBroadcast = opencodeBroadcast;
13
13
  exports.projectRegisteredEvent = projectRegisteredEvent;
14
14
  function opencodeBroadcast(data, internal) {
15
+ if (typeof data?.type !== 'string' || !data.type) {
16
+ // 形状守卫(fail-open):信封照常返回,但给出可定位诊断——畸形事件
17
+ // 在桌面/TUI 是"静默忽略",没有这行日志时插件作者无从排查。
18
+ // eslint-disable-next-line no-console
19
+ console.error(`[EventBroadcast] malformed opencode_event data: 'type' must be a non-empty string ` +
20
+ `(got ${JSON.stringify(data?.type)}); payload keys: ${data ? Object.keys(data).join(',') : '(null)'} ` +
21
+ `— downstream (desktop/TUI) will ignore this event`);
22
+ }
15
23
  return {
16
24
  type: 'opencode_event',
17
25
  data: {
@@ -54,6 +54,7 @@ class RuntimePluginLoader {
54
54
  meta = new Map();
55
55
  state = new Map();
56
56
  builtins = new Map();
57
+ packageEntries = new Map();
57
58
  constructor(pluginsDir) {
58
59
  this.pluginsDir = pluginsDir;
59
60
  }
@@ -149,12 +150,24 @@ class RuntimePluginLoader {
149
150
  logger_1.log.warn(`[RuntimePluginLoader] ${file} load error: ${err.message}`);
150
151
  }
151
152
  }
153
+ /** PluginHost 推送的包贡献。查找顺序:legacy 文件 > 包 > 内置。scan() 不影响。 */
154
+ setPackageEntries(entries) {
155
+ this.packageEntries = new Map(entries.map((e) => [e.name, e]));
156
+ }
152
157
  /** 插件不存在或未通过校验时返回 undefined(调用方回退内置 opencode)。文件插件优先于内置。 */
153
158
  get(name) {
154
159
  const createRuntime = this.factories.get(name);
155
160
  const meta = this.meta.get(name);
156
161
  if (createRuntime && meta)
157
162
  return { createRuntime, ...meta };
163
+ const pkg = this.packageEntries.get(name);
164
+ if (pkg) {
165
+ return {
166
+ createRuntime: pkg.createRuntime,
167
+ capabilities: { ...(0, contract_1.minimalCapabilities)(), ...pkg.capabilities },
168
+ external: pkg.external,
169
+ };
170
+ }
158
171
  const builtin = this.builtins.get(name);
159
172
  if (builtin)
160
173
  return { createRuntime: builtin.factory, ...builtin };
@@ -168,16 +181,24 @@ class RuntimePluginLoader {
168
181
  for (const [name, b] of this.builtins) {
169
182
  this.state.set(`builtin:${name}`, { file: `builtin:${name}`, name, status: 'ok', capabilities: b.capabilities });
170
183
  }
184
+ for (const [name, p] of this.packageEntries) {
185
+ this.state.set(`package:${name}`, {
186
+ file: `package:${name}`, name, status: 'ok',
187
+ capabilities: { ...(0, contract_1.minimalCapabilities)(), ...p.capabilities },
188
+ });
189
+ }
171
190
  return [...this.state.values()];
172
191
  }
173
192
  }
174
193
  exports.RuntimePluginLoader = RuntimePluginLoader;
175
- function createRuntimePluginContext(credentials) {
194
+ function createRuntimePluginContext(credentials, extra) {
176
195
  return {
177
196
  fetch: (url, opts) => fetch(url, { ...opts, signal: opts?.signal ?? AbortSignal.timeout(60000) }),
178
197
  log: logger_1.log,
179
198
  pluginConfig: (name) => config_1.config.raw?.runtime?.pluginConfig?.[name] ?? {},
180
199
  credentials,
200
+ projectDir: extra?.projectDir,
201
+ gatewayPort: extra?.gatewayPort,
181
202
  };
182
203
  }
183
204
  const README_CONTENT = `# Runtime Plugins