@jack200714/mafw 4.5.2 → 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 +315 -139
- package/gateway/dist/media/media-plugin-loader.js +25 -13
- 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/hub.js +153 -19
- 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 +19 -6
- package/gateway/dist/routes/waitwhat-command.js +43 -0
- package/gateway/dist/runtime/contract.js +4 -1
- package/gateway/dist/runtime/event-broadcast.js +41 -0
- package/gateway/dist/runtime/loader.js +45 -14
- 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 +103 -26
- package/gateway/dist/usage/plugin-context.js +42 -2
- package/gateway/dist/usage/plugin-loader.js +32 -13
- package/gateway/package.json +2 -2
- package/package.json +3 -1
- package/packages/tui/dist/cli.js +28 -5
|
@@ -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;
|
|
@@ -60,7 +62,6 @@ class MediaPluginLoader {
|
|
|
60
62
|
if (!fs.existsSync(this.pluginsDir)) {
|
|
61
63
|
fs.mkdirSync(this.pluginsDir, { recursive: true });
|
|
62
64
|
fs.writeFileSync(path.join(this.pluginsDir, 'README.md'), README_CONTENT);
|
|
63
|
-
fs.writeFileSync(path.join(this.pluginsDir, 'example.js.disabled'), EXAMPLE_CONTENT);
|
|
64
65
|
logger_1.log.info(`[MediaPluginLoader] Created ${this.pluginsDir}`);
|
|
65
66
|
}
|
|
66
67
|
}
|
|
@@ -147,8 +148,30 @@ class MediaPluginLoader {
|
|
|
147
148
|
logger_1.log.warn(`[MediaPluginLoader] ${file} load error: ${err.message}`);
|
|
148
149
|
}
|
|
149
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
|
+
}
|
|
150
166
|
getEngines() {
|
|
151
|
-
|
|
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;
|
|
152
175
|
}
|
|
153
176
|
getState() {
|
|
154
177
|
return [...this.state.values()];
|
|
@@ -241,14 +264,3 @@ media:
|
|
|
241
264
|
engine: qwen-vl # override for video
|
|
242
265
|
\`\`\`
|
|
243
266
|
`;
|
|
244
|
-
const EXAMPLE_CONTENT = `// Rename to example.js to activate
|
|
245
|
-
module.exports = {
|
|
246
|
-
name: "example",
|
|
247
|
-
modalities: ["image"],
|
|
248
|
-
async createPrompt(ctx) {
|
|
249
|
-
return async (parts, opts) => {
|
|
250
|
-
return "Example analysis result";
|
|
251
|
-
};
|
|
252
|
-
},
|
|
253
|
-
};
|
|
254
|
-
`;
|
|
@@ -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() {
|
|
@@ -36,11 +36,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.HubError = void 0;
|
|
37
37
|
exports.parseType = parseType;
|
|
38
38
|
exports.listPlugins = listPlugins;
|
|
39
|
+
exports.cleanupExamples = cleanupExamples;
|
|
39
40
|
exports.installPlugin = installPlugin;
|
|
40
41
|
exports.setPluginEnabled = setPluginEnabled;
|
|
41
42
|
exports.deletePlugin = deletePlugin;
|
|
42
43
|
const fs = __importStar(require("fs"));
|
|
44
|
+
const os = __importStar(require("os"));
|
|
43
45
|
const path = __importStar(require("path"));
|
|
46
|
+
const logger_1 = require("../core/utils/logger");
|
|
44
47
|
class HubError extends Error {
|
|
45
48
|
status;
|
|
46
49
|
constructor(status, message) {
|
|
@@ -87,6 +90,19 @@ function statEntry(type, dir, file) {
|
|
|
87
90
|
};
|
|
88
91
|
}
|
|
89
92
|
function listPlugins(deps) {
|
|
93
|
+
const userEntries = collectUserEntries(deps);
|
|
94
|
+
const builtins = (deps.builtinEntries?.() ?? []).map((e) => ({ ...e, builtin: true }));
|
|
95
|
+
const userKeys = new Set(userEntries.map((e) => `${e.type}/${e.name}`));
|
|
96
|
+
const configDisabled = deps.configDisabledUsage?.() ?? new Set();
|
|
97
|
+
for (const b of builtins) {
|
|
98
|
+
if (userKeys.has(`${b.type}/${b.name}`))
|
|
99
|
+
b.overridden = true;
|
|
100
|
+
if (b.type === 'usage' && configDisabled.has(b.name) && b.status === 'enabled')
|
|
101
|
+
b.status = 'config-disabled';
|
|
102
|
+
}
|
|
103
|
+
return [...builtins, ...userEntries];
|
|
104
|
+
}
|
|
105
|
+
function collectUserEntries(deps) {
|
|
90
106
|
const entries = [];
|
|
91
107
|
for (const type of PLUGIN_TYPES) {
|
|
92
108
|
const dir = deps.dirs[type];
|
|
@@ -130,24 +146,52 @@ function listPlugins(deps) {
|
|
|
130
146
|
}
|
|
131
147
|
return entries;
|
|
132
148
|
}
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
149
|
+
function cleanupExamples(deps) {
|
|
150
|
+
const removed = [];
|
|
151
|
+
const failed = [];
|
|
152
|
+
for (const dir of Object.values(deps.dirs)) {
|
|
153
|
+
const target = path.join(dir, 'example.js.disabled');
|
|
154
|
+
try {
|
|
155
|
+
if (fs.existsSync(target)) {
|
|
156
|
+
fs.unlinkSync(target);
|
|
157
|
+
removed.push(target);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
failed.push(target);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { removed, failed };
|
|
145
165
|
}
|
|
146
166
|
async function installPlugin(deps, input) {
|
|
147
|
-
const type = parseType(input.type);
|
|
148
167
|
const filename = validateFilename(input.filename);
|
|
149
168
|
if (!filename.endsWith('.js'))
|
|
150
169
|
throw new HubError(400, `install filename must end with .js: ${filename}`);
|
|
170
|
+
const maxBytes = deps.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
171
|
+
if (!Buffer.isBuffer(input.bytes) || input.bytes.length === 0)
|
|
172
|
+
throw new HubError(400, 'empty content');
|
|
173
|
+
if (input.bytes.length > maxBytes)
|
|
174
|
+
throw new HubError(413, `content exceeds ${maxBytes} bytes`);
|
|
175
|
+
const explicitType = input.type ? parseType(input.type) : undefined;
|
|
176
|
+
const { matches, mod } = inspectPlugin(filename, input.bytes);
|
|
177
|
+
validateName(mod, filename);
|
|
178
|
+
let type;
|
|
179
|
+
if (matches.length === 0)
|
|
180
|
+
throw new HubError(400, 'unrecognized plugin interface: export createRuntime / createPrompt / fetch / tools');
|
|
181
|
+
if (explicitType) {
|
|
182
|
+
if (!matches.includes(explicitType)) {
|
|
183
|
+
const listing = matches.map((m) => `${m} (${MATCH_IFACE[m]})`).join('/');
|
|
184
|
+
throw new HubError(400, `plugin interface mismatch: selected ${explicitType}, exports ${listing}`);
|
|
185
|
+
}
|
|
186
|
+
type = explicitType;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
if (matches.length > 1)
|
|
190
|
+
throw new HubError(400, `ambiguous plugin interface: ${matches.join('/')}`);
|
|
191
|
+
type = matches[0];
|
|
192
|
+
}
|
|
193
|
+
if (type === 'media')
|
|
194
|
+
await validateMediaActivation(mod);
|
|
151
195
|
const dir = deps.dirs[type];
|
|
152
196
|
fs.mkdirSync(dir, { recursive: true });
|
|
153
197
|
const target = resolveInDir(dir, filename);
|
|
@@ -156,23 +200,113 @@ async function installPlugin(deps, input) {
|
|
|
156
200
|
if (!input.overwrite && (fs.existsSync(dup) || fs.existsSync(dupDisabled))) {
|
|
157
201
|
throw new HubError(409, `plugin already exists: ${filename}`);
|
|
158
202
|
}
|
|
159
|
-
const buf = decodeContent(input.contentBase64, deps.maxBytes ?? DEFAULT_MAX_BYTES);
|
|
160
203
|
const tmp = `${target}.tmp`;
|
|
161
|
-
fs.writeFileSync(tmp,
|
|
204
|
+
fs.writeFileSync(tmp, input.bytes);
|
|
162
205
|
fs.renameSync(tmp, target);
|
|
163
206
|
await deps.reload?.(type);
|
|
164
207
|
return statEntry(type, dir, filename);
|
|
165
208
|
}
|
|
209
|
+
const MATCH_IFACE = {
|
|
210
|
+
runtime: 'createRuntime',
|
|
211
|
+
media: 'createPrompt',
|
|
212
|
+
usage: 'fetch',
|
|
213
|
+
ui: 'tools',
|
|
214
|
+
};
|
|
215
|
+
function computeMatches(mod) {
|
|
216
|
+
const matches = [];
|
|
217
|
+
if (typeof mod?.createRuntime === 'function')
|
|
218
|
+
matches.push('runtime');
|
|
219
|
+
if (typeof mod?.createPrompt === 'function' || typeof mod?.fixPayload === 'function'
|
|
220
|
+
|| typeof mod?.engine === 'string' || Array.isArray(mod?.modalities))
|
|
221
|
+
matches.push('media');
|
|
222
|
+
if (typeof mod?.fetch === 'function'
|
|
223
|
+
&& (mod.type === undefined || mod.type === 'api' || mod.type === 'token-plan' || mod.type === 'local'))
|
|
224
|
+
matches.push('usage');
|
|
225
|
+
if (mod?.tools && typeof mod.tools === 'object' && Object.keys(mod.tools).length > 0)
|
|
226
|
+
matches.push('ui');
|
|
227
|
+
return matches;
|
|
228
|
+
}
|
|
229
|
+
function inspectPlugin(filename, bytes) {
|
|
230
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mafw-hub-sniff-'));
|
|
231
|
+
const tmpFile = path.join(tmpDir, filename);
|
|
232
|
+
try {
|
|
233
|
+
fs.writeFileSync(tmpFile, bytes);
|
|
234
|
+
try {
|
|
235
|
+
delete require.cache[require.resolve(tmpFile)];
|
|
236
|
+
}
|
|
237
|
+
catch { /* first load */ }
|
|
238
|
+
let mod;
|
|
239
|
+
try {
|
|
240
|
+
mod = require(tmpFile);
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
throw new HubError(400, `plugin failed to load: ${err.message}`);
|
|
244
|
+
}
|
|
245
|
+
return { matches: computeMatches(mod), mod };
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
try {
|
|
249
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
250
|
+
}
|
|
251
|
+
catch { /* best effort */ }
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function validateName(mod, filename) {
|
|
255
|
+
const name = mod?.name;
|
|
256
|
+
if (typeof name !== 'string' || name.length === 0)
|
|
257
|
+
throw new HubError(400, 'missing plugin name');
|
|
258
|
+
const stem = baseName(filename);
|
|
259
|
+
if (name !== stem) {
|
|
260
|
+
throw new HubError(400, `plugin name mismatch: exports '${name}', filename '${filename}' (must match)`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const VALID_MODALITIES = new Set(['image', 'video', 'audio']);
|
|
264
|
+
const STUB_MEDIA_CTX = {
|
|
265
|
+
apiKey: () => null,
|
|
266
|
+
fetch: (url, opts) => fetch(url, opts),
|
|
267
|
+
pluginConfig: () => null,
|
|
268
|
+
log: logger_1.log,
|
|
269
|
+
};
|
|
270
|
+
async function validateMediaActivation(mod) {
|
|
271
|
+
const modalities = mod?.modalities;
|
|
272
|
+
if (!Array.isArray(modalities) || modalities.length === 0
|
|
273
|
+
|| !modalities.every((m) => VALID_MODALITIES.has(m))) {
|
|
274
|
+
throw new HubError(400, 'invalid modalities');
|
|
275
|
+
}
|
|
276
|
+
const hasCreatePrompt = typeof mod?.createPrompt === 'function';
|
|
277
|
+
const hasPiEngine = mod?.engine === 'pi';
|
|
278
|
+
if (!hasCreatePrompt && !hasPiEngine)
|
|
279
|
+
throw new HubError(400, 'missing createPrompt() or engine:"pi"');
|
|
280
|
+
if (hasCreatePrompt && hasPiEngine)
|
|
281
|
+
throw new HubError(400, 'createPrompt and engine:"pi" are mutually exclusive');
|
|
282
|
+
if (hasCreatePrompt) {
|
|
283
|
+
const fn = await mod.createPrompt(STUB_MEDIA_CTX);
|
|
284
|
+
if (typeof fn !== 'function')
|
|
285
|
+
throw new HubError(400, 'createPrompt did not return a function');
|
|
286
|
+
}
|
|
287
|
+
}
|
|
166
288
|
async function setPluginEnabled(deps, input) {
|
|
167
289
|
const type = parseType(input.type);
|
|
168
290
|
const filename = validateFilename(input.filename);
|
|
169
291
|
const dir = deps.dirs[type];
|
|
170
|
-
|
|
171
|
-
|
|
292
|
+
// Accept either on-disk state: a stale client snapshot (double-toggled
|
|
293
|
+
// switch, unrefreshed list) sends the opposite-state filename, which must
|
|
294
|
+
// resolve idempotently instead of 404ing ("激活 not found").
|
|
295
|
+
const primary = resolveInDir(dir, filename);
|
|
296
|
+
const variant = /\.js\.disabled$/.test(filename)
|
|
297
|
+
? filename.replace(/\.js\.disabled$/, '.js')
|
|
298
|
+
: filename.replace(/\.js$/, '.js.disabled');
|
|
299
|
+
const variantPath = resolveInDir(dir, variant);
|
|
300
|
+
const current = fs.existsSync(primary) ? primary : fs.existsSync(variantPath) ? variantPath : null;
|
|
301
|
+
if (!current)
|
|
172
302
|
throw new HubError(404, `plugin file not found: ${filename}`);
|
|
173
|
-
const
|
|
303
|
+
const currentName = path.basename(current);
|
|
304
|
+
const nextName = input.enabled
|
|
305
|
+
? currentName.replace(/\.js\.disabled$/, '.js')
|
|
306
|
+
: currentName.replace(/\.js$/, '.js.disabled');
|
|
174
307
|
const next = resolveInDir(dir, nextName);
|
|
175
|
-
|
|
308
|
+
if (next !== current)
|
|
309
|
+
fs.renameSync(current, next);
|
|
176
310
|
await deps.reload?.(type);
|
|
177
311
|
return statEntry(type, dir, nextName);
|
|
178
312
|
}
|
|
@@ -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
|
+
}
|