@jack200714/mafw 4.8.0 → 4.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -3
- package/gateway/dist/core/manager/goal-snapshot.js +2 -2
- package/gateway/dist/core/manager/manager-session-runtime.js +59 -0
- package/gateway/dist/core/manager/milestone-push.js +25 -10
- package/gateway/dist/index.js +265 -103
- package/gateway/dist/media/media-plugin-loader.js +25 -1
- package/gateway/dist/media/resolve-prompt.js +20 -0
- package/gateway/dist/memory/gateway-db.js +23 -0
- package/gateway/dist/opencode-adapter.js +34 -0
- package/gateway/dist/plugins/package-context.js +24 -0
- package/gateway/dist/plugins/package-host.js +331 -0
- package/gateway/dist/plugins/package-types.js +2 -0
- package/gateway/dist/recall/gateway-db-migrate.js +5 -2
- package/gateway/dist/recall/redact.js +53 -0
- package/gateway/dist/recall/turn-pipeline.js +2 -0
- package/gateway/dist/routes/event-publish.js +44 -0
- package/gateway/dist/routes/plugins.js +4 -1
- package/gateway/dist/routes/waitwhat-command.js +43 -0
- package/gateway/dist/runtime/contract.js +4 -1
- package/gateway/dist/runtime/event-broadcast.js +8 -0
- package/gateway/dist/runtime/loader.js +22 -1
- package/gateway/dist/runtime/normalize.js +13 -0
- package/gateway/dist/runtime/pi/pi-approval-bridge.js +12 -2
- package/gateway/dist/runtime/pi/pi-approval-extension.js +11 -3
- package/gateway/dist/runtime/pi/pi-session.js +21 -3
- package/gateway/dist/runtime/plugins/pi-runtime.js +6 -3
- package/gateway/dist/runtime/serve-sidecar.js +4 -1
- package/gateway/dist/runtime/serve-supervisor.js +12 -0
- package/gateway/dist/runtime/validate.js +39 -0
- package/gateway/dist/skills/manager-identity.js +6 -1
- package/gateway/dist/usage/builtin-plugins/gateway.js +91 -36
- package/gateway/dist/usage/plugin-context.js +42 -2
- package/gateway/dist/usage/plugin-loader.js +32 -2
- package/gateway/package.json +1 -1
- package/package.json +3 -1
- package/packages/tui/dist/cli.js +20 -2
|
@@ -44,6 +44,8 @@ class MediaPluginLoader {
|
|
|
44
44
|
pluginsDir;
|
|
45
45
|
state = new Map();
|
|
46
46
|
engines = new Map();
|
|
47
|
+
builtinEngines = new Map();
|
|
48
|
+
packageEngines = new Map();
|
|
47
49
|
watcher;
|
|
48
50
|
debounceTimer;
|
|
49
51
|
getCredentials;
|
|
@@ -146,8 +148,30 @@ class MediaPluginLoader {
|
|
|
146
148
|
logger_1.log.warn(`[MediaPluginLoader] ${file} load error: ${err.message}`);
|
|
147
149
|
}
|
|
148
150
|
}
|
|
151
|
+
/** 内置引擎登记(如 pi)。prompt 是占位——真正的内置路径由 resolveMediaPrompt 在调用点解析。 */
|
|
152
|
+
registerBuiltinEngine(name, modalities) {
|
|
153
|
+
this.builtinEngines.set(name, {
|
|
154
|
+
prompt: (async () => { throw new Error('builtin engine prompt is resolved at call site'); }),
|
|
155
|
+
modalities,
|
|
156
|
+
builtin: true,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
getBuiltinEngineNames() {
|
|
160
|
+
return [...this.builtinEngines.keys()];
|
|
161
|
+
}
|
|
162
|
+
/** PluginHost 推送的包贡献。同名覆盖 legacy 与内置。 */
|
|
163
|
+
setPackageEntries(entries) {
|
|
164
|
+
this.packageEngines = new Map(entries.map((e) => [e.name, e]));
|
|
165
|
+
}
|
|
149
166
|
getEngines() {
|
|
150
|
-
|
|
167
|
+
// 合并顺序:内置 → legacy 文件 → 包(后者覆盖前者同名)
|
|
168
|
+
const merged = new Map(this.builtinEngines);
|
|
169
|
+
for (const [name, e] of this.engines)
|
|
170
|
+
merged.set(name, e);
|
|
171
|
+
for (const [name, e] of this.packageEngines) {
|
|
172
|
+
merged.set(name, { prompt: e.prompt, modalities: e.modalities, source: e.source });
|
|
173
|
+
}
|
|
174
|
+
return merged;
|
|
151
175
|
}
|
|
152
176
|
getState() {
|
|
153
177
|
return [...this.state.values()];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveMediaPrompt = resolveMediaPrompt;
|
|
4
|
+
/** media 引擎解析(deps 注入可单测)。用户引擎可同名覆盖内置 pi。 */
|
|
5
|
+
const logger_1 = require("../core/utils/logger");
|
|
6
|
+
function resolveMediaPrompt(engineName, kind, deps) {
|
|
7
|
+
const engine = deps.engines.get(engineName);
|
|
8
|
+
if (!engine) {
|
|
9
|
+
if (engineName !== 'pi')
|
|
10
|
+
logger_1.log.warn(`[MediaService] engine '${engineName}' not found, falling back to pi`);
|
|
11
|
+
return deps.builtinPi();
|
|
12
|
+
}
|
|
13
|
+
if (engine.builtin)
|
|
14
|
+
return deps.builtinPi();
|
|
15
|
+
if (!engine.modalities.includes(kind)) {
|
|
16
|
+
logger_1.log.warn(`[MediaService] engine '${engineName}' does not support modality '${kind}', falling back to pi`);
|
|
17
|
+
return deps.builtinPi();
|
|
18
|
+
}
|
|
19
|
+
return engine.prompt;
|
|
20
|
+
}
|
|
@@ -371,6 +371,12 @@ class GatewayDatabase {
|
|
|
371
371
|
return this.db.prepare('SELECT COUNT(*) AS c FROM t1_observations').get().c;
|
|
372
372
|
}
|
|
373
373
|
// ── KV store (small critical state) ─────────────────────────────────────
|
|
374
|
+
// Scope 归属规范(新增 scope 必须声明一类):
|
|
375
|
+
// - runtime-scoped:生命周期绑定当前 agent runtime(会话 id 属于 runtime 存储),
|
|
376
|
+
// 切换时由 Scheduler.invalidateRuntimeScopedKv 统一失效——
|
|
377
|
+
// manager-session / internal-session / reflect-cursor
|
|
378
|
+
// - durable:跨 runtime 有效——registry/snapshot / milestone-notified
|
|
379
|
+
// runtime-scoped 条目的 value 必须携带 `at`(ISO 日期)供 TTL/审计。
|
|
374
380
|
kvGet(scope, key) {
|
|
375
381
|
const row = this.db.prepare('SELECT value FROM kv_store WHERE scope = ? AND key = ?').get(scope, key);
|
|
376
382
|
if (!row)
|
|
@@ -402,6 +408,23 @@ class GatewayDatabase {
|
|
|
402
408
|
}
|
|
403
409
|
});
|
|
404
410
|
}
|
|
411
|
+
kvClearScope(scope) {
|
|
412
|
+
const info = this.db.prepare('DELETE FROM kv_store WHERE scope = ?').run(scope);
|
|
413
|
+
return info.changes;
|
|
414
|
+
}
|
|
415
|
+
kvPruneOlderThan(scope, ttlDays) {
|
|
416
|
+
const now = Date.now();
|
|
417
|
+
let removed = 0;
|
|
418
|
+
for (const { key, value } of this.kvAll(scope)) {
|
|
419
|
+
const at = value && typeof value === 'object' ? value.at : undefined;
|
|
420
|
+
const ts = typeof at === 'string' ? Date.parse(at) : NaN;
|
|
421
|
+
if (!isNaN(ts) && now - ts > ttlDays * 86_400_000) {
|
|
422
|
+
this.kvDelete(scope, key);
|
|
423
|
+
removed++;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return removed;
|
|
427
|
+
}
|
|
405
428
|
// ── Goal outcomes (RSI Phase 1) ─────────────────────────────────────────
|
|
406
429
|
upsertGoalOutcome(o) {
|
|
407
430
|
this.db.prepare(`
|
|
@@ -27,6 +27,28 @@ function messageToParts(message, parts) {
|
|
|
27
27
|
async function createOpencodeAdapter(config) {
|
|
28
28
|
const { createOpencodeClient } = await import('@opencode-ai/sdk/v2');
|
|
29
29
|
const client = createOpencodeClient(config);
|
|
30
|
+
// opencode 原生 V1 路由的 workspace 路由细节收敛在 adapter 内(契约保持
|
|
31
|
+
// runtime 中立)——带 directory 的调用直连 fetch(V2 SDK 调用不带 workspace 语义)。
|
|
32
|
+
const directNative = async (path, method, body, directory) => {
|
|
33
|
+
const url = `${config.baseUrl}${path}${directory ? `?directory=${encodeURIComponent(directory)}` : ''}`;
|
|
34
|
+
const res = await fetch(url, {
|
|
35
|
+
method,
|
|
36
|
+
headers: {
|
|
37
|
+
...(body !== undefined ? { 'content-type': 'application/json' } : {}),
|
|
38
|
+
...(directory ? { 'x-opencode-directory': encodeURIComponent(directory) } : {}),
|
|
39
|
+
...(config.headers ?? {}),
|
|
40
|
+
},
|
|
41
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
42
|
+
});
|
|
43
|
+
if (!res.ok)
|
|
44
|
+
throw new Error(`opencode native ${path} failed: ${res.status}`);
|
|
45
|
+
try {
|
|
46
|
+
return await res.json();
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
30
52
|
return {
|
|
31
53
|
session: {
|
|
32
54
|
async create(opts) {
|
|
@@ -192,6 +214,10 @@ async function createOpencodeAdapter(config) {
|
|
|
192
214
|
return Array.isArray(data) ? data : data?.items ?? [];
|
|
193
215
|
},
|
|
194
216
|
async reply(opts) {
|
|
217
|
+
if (opts.directory) {
|
|
218
|
+
await directNative(`/question/${opts.requestID}/reply`, 'POST', { answers: opts.answers }, opts.directory);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
195
221
|
const result = await client.session.question.reply({
|
|
196
222
|
requestID: opts.requestID,
|
|
197
223
|
answers: opts.answers,
|
|
@@ -201,12 +227,20 @@ async function createOpencodeAdapter(config) {
|
|
|
201
227
|
}
|
|
202
228
|
},
|
|
203
229
|
async reject(opts) {
|
|
230
|
+
if (opts.directory) {
|
|
231
|
+
await directNative(`/question/${opts.requestID}/reject`, 'POST', undefined, opts.directory);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
204
234
|
const result = await client.session.question.reject({ requestID: opts.requestID });
|
|
205
235
|
if (result && typeof result === 'object' && 'error' in result && result.error) {
|
|
206
236
|
throw new Error(String(result.error));
|
|
207
237
|
}
|
|
208
238
|
},
|
|
209
239
|
},
|
|
240
|
+
async permissionList(opts) {
|
|
241
|
+
const data = await directNative('/permission', 'GET', undefined, opts?.directory);
|
|
242
|
+
return Array.isArray(data) ? data : data?.items ?? [];
|
|
243
|
+
},
|
|
210
244
|
},
|
|
211
245
|
global: {
|
|
212
246
|
event() {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createPluginPackageContext = createPluginPackageContext;
|
|
4
|
+
const config_1 = require("../config");
|
|
5
|
+
const logger_1 = require("../core/utils/logger");
|
|
6
|
+
const auth_1 = require("../runtime/auth");
|
|
7
|
+
const EMPTY_USAGE = { modelStats: () => [] };
|
|
8
|
+
function createPluginPackageContext(name, deps) {
|
|
9
|
+
return {
|
|
10
|
+
log: logger_1.log,
|
|
11
|
+
fetch: (url, opts) => fetch(url, { ...opts, signal: opts?.signal ?? AbortSignal.timeout(60_000) }),
|
|
12
|
+
apiKey: (provider) => (0, auth_1.getProviderApiKey)(provider, undefined, deps.getCredentials?.()) ?? null,
|
|
13
|
+
pluginConfig: () => config_1.config.raw?.plugins?.[name]?.config
|
|
14
|
+
?? config_1.config.raw?.usage?.pluginConfig?.[name]
|
|
15
|
+
?? config_1.config.raw?.media?.pluginConfig?.[name]
|
|
16
|
+
?? config_1.config.raw?.runtime?.pluginConfig?.[name]
|
|
17
|
+
?? {},
|
|
18
|
+
credentials: deps.getCredentials?.(),
|
|
19
|
+
projectDir: deps.projectDir,
|
|
20
|
+
gatewayPort: deps.gatewayPort,
|
|
21
|
+
usage: deps.usageStats?.() ?? EMPTY_USAGE,
|
|
22
|
+
emit: (event) => deps.emit?.(event),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.PluginHost = void 0;
|
|
37
|
+
/**
|
|
38
|
+
* 统一插件包宿主 —— 扫 ~/.mafw/plugins/(单文件包 *.js / 目录包 plugin.json+入口),
|
|
39
|
+
* 激活后把贡献分发给三个 legacy loader(setPackageEntries)。fail-open。
|
|
40
|
+
* 优先级(同名):包 > legacy 目录文件 > 内置。
|
|
41
|
+
*/
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const logger_1 = require("../core/utils/logger");
|
|
45
|
+
const pi_adapter_1 = require("../media/pi-adapter");
|
|
46
|
+
const VALID_MODALITIES = new Set(['image', 'video', 'audio']);
|
|
47
|
+
class PluginHost {
|
|
48
|
+
dir;
|
|
49
|
+
createContext;
|
|
50
|
+
state = new Map();
|
|
51
|
+
watcher;
|
|
52
|
+
dirWatchers = new Map();
|
|
53
|
+
debounce;
|
|
54
|
+
/** 当前目录包的子目录绝对路径(refreshDirWatchers 用) */
|
|
55
|
+
pkgDirs = [];
|
|
56
|
+
runtimeCb;
|
|
57
|
+
mediaCb;
|
|
58
|
+
usageCb;
|
|
59
|
+
lastRuntime = [];
|
|
60
|
+
lastMedia = [];
|
|
61
|
+
lastUsage = [];
|
|
62
|
+
constructor(dir, createContext) {
|
|
63
|
+
this.dir = dir;
|
|
64
|
+
this.createContext = createContext;
|
|
65
|
+
}
|
|
66
|
+
bindRuntime(cb) { this.runtimeCb = cb; cb(this.lastRuntime); }
|
|
67
|
+
bindMedia(cb) { this.mediaCb = cb; cb(this.lastMedia); }
|
|
68
|
+
bindUsage(cb) { this.usageCb = cb; cb(this.lastUsage); }
|
|
69
|
+
async init() {
|
|
70
|
+
this.ensureDir();
|
|
71
|
+
await this.reload();
|
|
72
|
+
this.startWatch();
|
|
73
|
+
}
|
|
74
|
+
getState() {
|
|
75
|
+
return [...this.state.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
76
|
+
}
|
|
77
|
+
stop() {
|
|
78
|
+
this.watcher?.close();
|
|
79
|
+
for (const w of this.dirWatchers.values())
|
|
80
|
+
w.close();
|
|
81
|
+
this.dirWatchers.clear();
|
|
82
|
+
if (this.debounce)
|
|
83
|
+
clearTimeout(this.debounce);
|
|
84
|
+
}
|
|
85
|
+
ensureDir() {
|
|
86
|
+
if (!fs.existsSync(this.dir)) {
|
|
87
|
+
fs.mkdirSync(this.dir, { recursive: true });
|
|
88
|
+
fs.writeFileSync(path.join(this.dir, 'README.md'), README_CONTENT);
|
|
89
|
+
logger_1.log.info(`[PluginHost] Created ${this.dir}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** 全量重扫 + 重新激活 + 一次性推送(跨面原子:先全部激活完再推各 loader)。 */
|
|
93
|
+
async reload() {
|
|
94
|
+
const next = new Map();
|
|
95
|
+
const runtime = [];
|
|
96
|
+
const media = [];
|
|
97
|
+
const usage = [];
|
|
98
|
+
const pkgDirs = [];
|
|
99
|
+
for (const desc of this.scanDescriptors(next)) {
|
|
100
|
+
if (desc.pkgDir)
|
|
101
|
+
pkgDirs.push(desc.pkgDir);
|
|
102
|
+
try {
|
|
103
|
+
const c = await this.activate(desc);
|
|
104
|
+
if (c.runtime)
|
|
105
|
+
runtime.push(c.runtime);
|
|
106
|
+
if (c.media)
|
|
107
|
+
media.push(c.media);
|
|
108
|
+
if (c.usage)
|
|
109
|
+
usage.push(c.usage);
|
|
110
|
+
next.set(desc.name, {
|
|
111
|
+
name: desc.name, source: desc.mainFile, status: 'ok',
|
|
112
|
+
contributions: c.contributions, version: desc.manifest?.version,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
next.set(desc.name, { name: desc.name, source: desc.mainFile, status: 'error', error: err.message, contributions: [] });
|
|
117
|
+
logger_1.log.warn(`[PluginHost] ${desc.name} activate error: ${err.message}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
this.state = next;
|
|
121
|
+
this.pkgDirs = pkgDirs;
|
|
122
|
+
this.lastRuntime = runtime;
|
|
123
|
+
this.lastMedia = media;
|
|
124
|
+
this.lastUsage = usage;
|
|
125
|
+
this.runtimeCb?.(runtime);
|
|
126
|
+
this.mediaCb?.(media);
|
|
127
|
+
this.usageCb?.(usage);
|
|
128
|
+
this.refreshDirWatchers();
|
|
129
|
+
}
|
|
130
|
+
/** 每个目录包一个 watcher(跨平台——不用 recursive)。reload 后按当前包集合增删。 */
|
|
131
|
+
refreshDirWatchers() {
|
|
132
|
+
if (!this.watcher)
|
|
133
|
+
return; // 顶层 watch 未建立(init 前手动 reload)则不建
|
|
134
|
+
const wanted = new Set(this.pkgDirs);
|
|
135
|
+
for (const [dir, w] of this.dirWatchers) {
|
|
136
|
+
if (!wanted.has(dir)) {
|
|
137
|
+
w.close();
|
|
138
|
+
this.dirWatchers.delete(dir);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
for (const dir of wanted) {
|
|
142
|
+
if (this.dirWatchers.has(dir))
|
|
143
|
+
continue;
|
|
144
|
+
try {
|
|
145
|
+
const w = fs.watch(dir, () => {
|
|
146
|
+
if (this.debounce)
|
|
147
|
+
clearTimeout(this.debounce);
|
|
148
|
+
this.debounce = setTimeout(() => {
|
|
149
|
+
void this.reload().catch((err) => logger_1.log.warn(`[PluginHost] reload error: ${err.message}`));
|
|
150
|
+
}, 300);
|
|
151
|
+
});
|
|
152
|
+
this.dirWatchers.set(dir, w);
|
|
153
|
+
}
|
|
154
|
+
catch { /* 单个目录不可 watch → 跳过,fail-open */ }
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/** 枚举候选包;逐条错误记入 errorStates(不抛出)。 */
|
|
158
|
+
scanDescriptors(errorStates) {
|
|
159
|
+
const out = [];
|
|
160
|
+
if (!fs.existsSync(this.dir))
|
|
161
|
+
return out;
|
|
162
|
+
const entries = fs.readdirSync(this.dir, { withFileTypes: true })
|
|
163
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
164
|
+
for (const entry of entries) {
|
|
165
|
+
try {
|
|
166
|
+
if (entry.isFile() && entry.name.endsWith('.js')) {
|
|
167
|
+
out.push({ name: entry.name.replace(/\.js$/, ''), mainFile: path.join(this.dir, entry.name) });
|
|
168
|
+
}
|
|
169
|
+
else if (entry.isDirectory()) {
|
|
170
|
+
const pkgDir = path.join(this.dir, entry.name);
|
|
171
|
+
const manifestPath = path.join(pkgDir, 'plugin.json');
|
|
172
|
+
let manifest;
|
|
173
|
+
if (fs.existsSync(manifestPath)) {
|
|
174
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
175
|
+
}
|
|
176
|
+
const mainFile = path.join(pkgDir, manifest?.main ?? 'index.js');
|
|
177
|
+
if (!fs.existsSync(mainFile))
|
|
178
|
+
throw new Error(`main file not found: ${manifest?.main ?? 'index.js'}`);
|
|
179
|
+
out.push({ name: manifest?.name ?? entry.name, mainFile, manifest, pkgDir });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
errorStates.set(entry.name, { name: entry.name, source: path.join(this.dir, entry.name), status: 'error', error: err.message, contributions: [] });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
async activate(desc) {
|
|
189
|
+
try {
|
|
190
|
+
delete require.cache[require.resolve(desc.mainFile)];
|
|
191
|
+
}
|
|
192
|
+
catch { /* first load */ }
|
|
193
|
+
const mod = require(desc.mainFile);
|
|
194
|
+
const modName = mod?.name;
|
|
195
|
+
if (typeof modName !== 'string' || !modName)
|
|
196
|
+
throw new Error('missing name');
|
|
197
|
+
if (modName !== desc.name)
|
|
198
|
+
throw new Error(`name mismatch: exports '${modName}', package '${desc.name}'`);
|
|
199
|
+
const ctx = this.createContext(modName);
|
|
200
|
+
const contribs = typeof mod.activate === 'function' ? ((await mod.activate(ctx)) ?? {}) : pickContributions(mod);
|
|
201
|
+
const kinds = [];
|
|
202
|
+
let runtime;
|
|
203
|
+
let media;
|
|
204
|
+
let usage;
|
|
205
|
+
if (contribs.usage) {
|
|
206
|
+
const u = contribs.usage;
|
|
207
|
+
if (typeof u.name !== 'string' || !u.name)
|
|
208
|
+
throw new Error('usage contribution missing name');
|
|
209
|
+
if (typeof u.fetch !== 'function')
|
|
210
|
+
throw new Error('usage contribution missing fetch(ctx)');
|
|
211
|
+
usage = { mod: u, source: desc.mainFile };
|
|
212
|
+
kinds.push('usage');
|
|
213
|
+
}
|
|
214
|
+
if (contribs.media) {
|
|
215
|
+
const spec = contribs.media;
|
|
216
|
+
if (!Array.isArray(spec.modalities) || spec.modalities.length === 0
|
|
217
|
+
|| !spec.modalities.every((m) => VALID_MODALITIES.has(m)))
|
|
218
|
+
throw new Error('media: invalid modalities');
|
|
219
|
+
const hasCreatePrompt = typeof spec.createPrompt === 'function';
|
|
220
|
+
const hasPiEngine = spec.engine === 'pi';
|
|
221
|
+
if (hasCreatePrompt === hasPiEngine)
|
|
222
|
+
throw new Error('media: createPrompt and engine:"pi" are mutually exclusive');
|
|
223
|
+
const prompt = hasCreatePrompt
|
|
224
|
+
? await spec.createPrompt(ctx)
|
|
225
|
+
: (0, pi_adapter_1.createPiPromptAdapter)({ fixPayload: spec.fixPayload, credentials: ctx.credentials });
|
|
226
|
+
if (typeof prompt !== 'function')
|
|
227
|
+
throw new Error('media: createPrompt did not return a function');
|
|
228
|
+
media = { name: spec.name ?? modName, prompt, modalities: spec.modalities, source: desc.mainFile };
|
|
229
|
+
kinds.push('media');
|
|
230
|
+
}
|
|
231
|
+
if (contribs.runtime) {
|
|
232
|
+
const spec = contribs.runtime;
|
|
233
|
+
if (typeof spec.createRuntime !== 'function')
|
|
234
|
+
throw new Error('runtime: missing createRuntime(ctx)');
|
|
235
|
+
runtime = {
|
|
236
|
+
name: spec.name ?? modName,
|
|
237
|
+
// 包 runtime 一律拿统一 ctx(忽略 loader 传入的 legacy ctx)
|
|
238
|
+
createRuntime: (_ignored) => spec.createRuntime(ctx),
|
|
239
|
+
capabilities: spec.capabilities ?? {},
|
|
240
|
+
external: spec.external !== false,
|
|
241
|
+
source: desc.mainFile,
|
|
242
|
+
};
|
|
243
|
+
kinds.push('runtime');
|
|
244
|
+
}
|
|
245
|
+
if (contribs.uiTools) {
|
|
246
|
+
// 声明期字段级校验:桌面 validateCard 是运行时兜底,形状错位应在激活期暴露
|
|
247
|
+
if (typeof contribs.uiTools !== 'object' || Array.isArray(contribs.uiTools)) {
|
|
248
|
+
throw new Error(`uiTools must be an object (got ${typeof contribs.uiTools})`);
|
|
249
|
+
}
|
|
250
|
+
for (const [tool, def] of Object.entries(contribs.uiTools)) {
|
|
251
|
+
if (!def || typeof def !== 'object' || Array.isArray(def)) {
|
|
252
|
+
throw new Error(`uiTools.${tool} must be an object (got ${Array.isArray(def) ? 'array' : typeof def})`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
kinds.push('uiTools');
|
|
256
|
+
}
|
|
257
|
+
if (kinds.length === 0)
|
|
258
|
+
throw new Error('no contributions (usage/media/runtime/uiTools)');
|
|
259
|
+
return { contributions: kinds, runtime, media, usage };
|
|
260
|
+
}
|
|
261
|
+
startWatch() {
|
|
262
|
+
if (!fs.existsSync(this.dir))
|
|
263
|
+
return;
|
|
264
|
+
try {
|
|
265
|
+
this.watcher = fs.watch(this.dir, () => {
|
|
266
|
+
if (this.debounce)
|
|
267
|
+
clearTimeout(this.debounce);
|
|
268
|
+
this.debounce = setTimeout(() => {
|
|
269
|
+
if (!fs.existsSync(this.dir)) {
|
|
270
|
+
this.watcher?.close();
|
|
271
|
+
this.watcher = undefined;
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
void this.reload().catch((err) => logger_1.log.warn(`[PluginHost] reload error: ${err.message}`));
|
|
275
|
+
}, 300);
|
|
276
|
+
});
|
|
277
|
+
// init 的 reload 跑在本方法之前(当时 watcher 未建,refreshDirWatchers 早退)
|
|
278
|
+
// ——这里补建目录包子目录的 watcher。
|
|
279
|
+
this.refreshDirWatchers();
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
logger_1.log.warn(`[PluginHost] watch failed: ${err.message}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
exports.PluginHost = PluginHost;
|
|
287
|
+
function pickContributions(mod) {
|
|
288
|
+
const out = {};
|
|
289
|
+
if (mod.usage)
|
|
290
|
+
out.usage = mod.usage;
|
|
291
|
+
if (mod.media)
|
|
292
|
+
out.media = mod.media;
|
|
293
|
+
if (mod.runtime)
|
|
294
|
+
out.runtime = mod.runtime;
|
|
295
|
+
if (mod.uiTools)
|
|
296
|
+
out.uiTools = mod.uiTools;
|
|
297
|
+
return out;
|
|
298
|
+
}
|
|
299
|
+
const README_CONTENT = `# MAFW Plugin Packages
|
|
300
|
+
|
|
301
|
+
一个插件包可以同时贡献多种能力(usage 配额 / media 引擎 / runtime / UI 工具卡)。
|
|
302
|
+
|
|
303
|
+
## 形态
|
|
304
|
+
|
|
305
|
+
- 单文件包:\`my-vendor.js\`(包名 = 文件名)
|
|
306
|
+
- 目录包:\`my-vendor/plugin.json\`(\`{ "name", "version"?, "main"? }\`,main 缺省 index.js)
|
|
307
|
+
|
|
308
|
+
## 模块形状(二选一)
|
|
309
|
+
|
|
310
|
+
\`\`\`js
|
|
311
|
+
module.exports = {
|
|
312
|
+
name: "my-vendor",
|
|
313
|
+
usage: { name: "my-vendor", type: "api", plan: "P", async fetch(ctx) {} },
|
|
314
|
+
media: { modalities: ["image"], engine: "pi", fixPayload(p) {} },
|
|
315
|
+
runtime: { capabilities: {}, async createRuntime(ctx) {} },
|
|
316
|
+
uiTools: {}, // v1 仅在插件中心展示登记,桌面侧加载仍走 ~/.mafw/ui-plugins/
|
|
317
|
+
};
|
|
318
|
+
// 或:module.exports = { name, async activate(ctx) { return { usage: {...} }; } };
|
|
319
|
+
\`\`\`
|
|
320
|
+
|
|
321
|
+
## ctx(三个 legacy ctx 的超集)
|
|
322
|
+
|
|
323
|
+
- \`ctx.apiKey(provider)\` / \`ctx.fetch(url, opts)\`(60s)/ \`ctx.log\`
|
|
324
|
+
- \`ctx.pluginConfig()\` —— 读 config.yaml \`plugins.<name>.config\`(回退 legacy 三段 pluginConfig)
|
|
325
|
+
- \`ctx.projectDir\` / \`ctx.gatewayPort\` / \`ctx.usage.modelStats({sinceMs?, provider?})\`
|
|
326
|
+
|
|
327
|
+
## 优先级
|
|
328
|
+
|
|
329
|
+
同名时:**包 > legacy 目录文件(usage-plugins/ media-plugins/ runtime-plugins/)> 内置**。
|
|
330
|
+
legacy 目录行为不变。reload 只清主文件的 require.cache——改包内 lib/ 共享代码需 \`mafw restart\`。
|
|
331
|
+
`;
|
|
@@ -112,9 +112,12 @@ function migrateGatewayDb(memoryDir, managerSessions, cursorFile, registrySnapsh
|
|
|
112
112
|
logger_1.log.warn(`[gateway-db] cursor import failed: ${err.message}`);
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
-
// ④ registry snapshot (refreshed every start)
|
|
115
|
+
// ④ registry snapshot (refreshed every start) — scope/key aligned with the
|
|
116
|
+
// live read/write path (index.ts 'registry/snapshot'/'default'); the legacy
|
|
117
|
+
// 'registry' scope is removed idempotently.
|
|
116
118
|
if (registrySnapshot !== null) {
|
|
117
|
-
db.kvSet('registry', '
|
|
119
|
+
db.kvSet('registry/snapshot', 'default', registrySnapshot);
|
|
120
|
+
db.kvDelete('registry', 'snapshot');
|
|
118
121
|
result.registrySnapshot = true;
|
|
119
122
|
}
|
|
120
123
|
db.close();
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Secret redaction for the observation pipeline. Applied once at capture time
|
|
3
|
+
// (before t1_observations insert) so every downstream consumer (turnCompress
|
|
4
|
+
// transcripts, worker prompts, archives) only ever sees redacted content.
|
|
5
|
+
// Deterministic regex pass — no LLM, no false-negative risk beyond pattern
|
|
6
|
+
// coverage; deliberately conservative about false positives (prose like
|
|
7
|
+
// "token budget" or "the secret sauce" must survive).
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.redactSecrets = redactSecrets;
|
|
10
|
+
const PLACEHOLDER = '[REDACTED]';
|
|
11
|
+
// Ordered: specific token formats first, generic key=value forms last.
|
|
12
|
+
const PATTERNS = [
|
|
13
|
+
// Bearer tokens in Authorization-style headers
|
|
14
|
+
/(Bearer\s+)[A-Za-z0-9._~+/=-]{8,}/g,
|
|
15
|
+
// OpenAI / DashScope / OpenRouter / Anthropic style: sk-..., sk-or-v1-..., sk-ant-...
|
|
16
|
+
/\bsk-[A-Za-z0-9_-]{16,}\b/g,
|
|
17
|
+
// GitHub tokens
|
|
18
|
+
/\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{16,}\b/g,
|
|
19
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g,
|
|
20
|
+
// Google API keys
|
|
21
|
+
/\bAIza[0-9A-Za-z_-]{35}\b/g,
|
|
22
|
+
// AWS access key ids
|
|
23
|
+
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
24
|
+
// Slack tokens
|
|
25
|
+
/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g,
|
|
26
|
+
// JWTs (header.payload.signature)
|
|
27
|
+
/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}\b/g,
|
|
28
|
+
// Generic key=value / "key": "value" secret forms. Keys are explicit
|
|
29
|
+
// (optionally wrapped in quotes); values must be >= 8 non-delimiter chars so
|
|
30
|
+
// prose ("password length", short numerics) is untouched. Quoted values
|
|
31
|
+
// keep their quotes.
|
|
32
|
+
// group1 keeps everything through the opening value quote; the closing
|
|
33
|
+
// quote stays outside the match so it survives in place.
|
|
34
|
+
/((?:"?)\b(?:api[_-]?key|apikey|api[_-]?secret|secret[_-]?key|access[_-]?token|auth[_-]?token|private[_-]?key|client[_-]?secret|password|passwd)\b"?\s*[=:]\s*")[^"\s]{8,}/gi,
|
|
35
|
+
// "[" excluded from values so an already-written [REDACTED] placeholder is
|
|
36
|
+
// never re-matched by this pass.
|
|
37
|
+
/((?:"?)\b(?:api[_-]?key|apikey|api[_-]?secret|secret[_-]?key|access[_-]?token|auth[_-]?token|private[_-]?key|client[_-]?secret|password|passwd)\b"?\s*[=:]\s*)[^\s"',\][]{8,}/gi,
|
|
38
|
+
];
|
|
39
|
+
function redactSecrets(content) {
|
|
40
|
+
if (!content)
|
|
41
|
+
return content;
|
|
42
|
+
let out = content;
|
|
43
|
+
for (const pattern of PATTERNS) {
|
|
44
|
+
out = out.replace(pattern, (match, ...rest) => {
|
|
45
|
+
// Group-free patterns: first rest arg is the numeric offset, replace
|
|
46
|
+
// wholesale. Patterns with one capture group keep the captured prefix
|
|
47
|
+
// (e.g. "Bearer ", "api_key=").
|
|
48
|
+
const group1 = rest[0];
|
|
49
|
+
return typeof group1 === 'string' ? `${group1}${PLACEHOLDER}` : PLACEHOLDER;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
@@ -14,6 +14,8 @@ Rules:
|
|
|
14
14
|
- every cue_anchors list MUST include the topic entity names (project, module, person, API, feature) so the memory can be retrieved across sessions
|
|
15
15
|
- for preferences or constraints, include a machine-readable anchor like "pref:<dimension>=<value>" (e.g., "pref:ui-language=chinese") in addition to the entity
|
|
16
16
|
- skip redundant or trivial content; do not repeat entries that are obviously already known
|
|
17
|
+
- pointer over fulltext: if the durable content is already carried by a repo artifact (code, docs, ADR, issue, config), record a POINTER (path/URL + one-line gist) instead of restating the full content — the artifact is the source of truth, the memory is only its index
|
|
18
|
+
- for procedural memories, end memory_value with a next-time pointer when applicable: which skill, tool, or command the next agent should reach for (e.g., "→ next time: use the /tdd skill") — a memory should be a signpost, not just an archive
|
|
17
19
|
- for every memory you write, include an explicit importance score via the mafw_add_memory importance parameter: 1=trivial routine, 5=ordinary fact, 9-10=architecture-level decision or serious incident
|
|
18
20
|
- if nothing is worth saving, do not call the tool
|
|
19
21
|
Division of labor: your job is the FACT LAYER of this session — concrete facts, decisions, preferences, event outcomes, and specific technical pitfalls (which API does what). Do NOT attempt cross-session pattern generalization — that is the daily reflection pipeline's job.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleEventPublish = handleEventPublish;
|
|
4
|
+
const event_broadcast_1 = require("../runtime/event-broadcast");
|
|
5
|
+
function readBody(req) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
let body = '';
|
|
8
|
+
req.on('data', (c) => body += c);
|
|
9
|
+
req.on('end', () => resolve(body));
|
|
10
|
+
req.on('error', reject);
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
async function handleEventPublish(deps, req, res) {
|
|
14
|
+
const send = (status, payload) => {
|
|
15
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
16
|
+
res.end(JSON.stringify(payload));
|
|
17
|
+
};
|
|
18
|
+
try {
|
|
19
|
+
const body = JSON.parse(await readBody(req));
|
|
20
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
21
|
+
send(400, { error: 'body must be a JSON object' });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (typeof body.type !== 'string' || !body.type) {
|
|
25
|
+
send(400, { error: "body.type must be a non-empty string (recommended namespace: 'plugin:<name>:<event>')" });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (body.type === 'opencode_event') {
|
|
29
|
+
// 信封旁路:data 形状守卫(data.type 缺失 → 400,不发半成品事件)
|
|
30
|
+
if (!body.data || typeof body.data !== 'object' || typeof body.data.type !== 'string' || !body.data.type) {
|
|
31
|
+
send(400, { error: 'opencode_event envelope requires data.type (non-empty string)' });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
deps.broadcast((0, event_broadcast_1.opencodeBroadcast)(body.data, body.data.internal === true));
|
|
35
|
+
send(200, { ok: true });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
deps.broadcast(body);
|
|
39
|
+
send(200, { ok: true });
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
send(400, { error: `invalid JSON body: ${err.message}` });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -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 () => ({
|
|
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 () => {
|