@zhin.js/host-http 1.1.13 → 1.1.15
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/lib/console-rest-pages-helpers.d.ts +23 -0
- package/lib/console-rest-pages-helpers.js +570 -0
- package/lib/console-rest-pages.js +10 -570
- package/lib/console-rpc-handlers.d.ts +21 -0
- package/lib/console-rpc-handlers.js +659 -0
- package/lib/console-rpc.d.ts +9 -5
- package/lib/console-rpc.js +91 -584
- package/lib/http-host.js +3 -2
- package/package.json +4 -4
- package/src/console-rest-pages-helpers.ts +619 -0
- package/src/console-rest-pages.ts +12 -612
- package/src/console-rpc-handlers.ts +608 -0
- package/src/console-rpc.ts +82 -556
- package/src/http-host.ts +3 -2
package/lib/console-rpc.js
CHANGED
|
@@ -1,20 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CONFIG_PATCH_LIMITS, CONFIG_RPC, assertDemoConsoleRpcAllowed, } from '@zhin.js/console-protocol';
|
|
2
2
|
import { dispatchExtendedConsoleRpc } from './console-rpc-extended/index.js';
|
|
3
3
|
import { dispatchPluginConsoleRpc } from './console-plugin-rpc.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const kind = channelType === 'group' || channelType === 'channel' ? channelType : 'private';
|
|
7
|
-
const wire = parent && typeof parent === 'object'
|
|
8
|
-
? parent
|
|
9
|
-
: undefined;
|
|
10
|
-
const parentKind = wire?.type === 'group' || wire?.type === 'channel' ? wire.type : undefined;
|
|
11
|
-
const parentId = typeof wire?.id === 'string' && wire.id ? wire.id : undefined;
|
|
12
|
-
return {
|
|
13
|
-
kind,
|
|
14
|
-
id: channelId,
|
|
15
|
-
...(parentKind && parentId ? { parent: { kind: parentKind, id: parentId } } : {}),
|
|
16
|
-
};
|
|
17
|
-
}
|
|
4
|
+
import { configurationSaveMessage, dispatchHostResourceConsoleRpc, invalidProjectPatchOperations, } from './console-rpc-handlers.js';
|
|
5
|
+
export { isDemoHttpAllowed } from './console-rpc-handlers.js';
|
|
18
6
|
export function pickRpcReply(message, payloads) {
|
|
19
7
|
const requestId = message.requestId;
|
|
20
8
|
return ((requestId != null ? payloads.find((payload) => payload.requestId === requestId) : undefined)
|
|
@@ -175,6 +163,30 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
|
|
|
175
163
|
}
|
|
176
164
|
return payloads;
|
|
177
165
|
}
|
|
166
|
+
case CONFIG_RPC.APPLY: {
|
|
167
|
+
if (ctx.authScope !== 'full') {
|
|
168
|
+
emit({ requestId, error: 'Full Console scope is required' });
|
|
169
|
+
return payloads;
|
|
170
|
+
}
|
|
171
|
+
if (typeof message.expectedRevision !== 'string' || !/^[a-f0-9]{64}$/u.test(message.expectedRevision)) {
|
|
172
|
+
emit({ requestId, error: 'expectedRevision is required' });
|
|
173
|
+
return payloads;
|
|
174
|
+
}
|
|
175
|
+
if (!ctx.applyConfig) {
|
|
176
|
+
emit({ requestId, error: 'Config application is not configured' });
|
|
177
|
+
return payloads;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const result = await ctx.applyConfig(message.expectedRevision);
|
|
181
|
+
emit({ requestId, data: result });
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// Source reads can fail before an operation is admitted. Do not echo a
|
|
185
|
+
// YAML parser's source excerpt or turn unavailable evidence into success.
|
|
186
|
+
emit({ requestId, error: 'Configuration application is unavailable; read the source and runtime status before retrying' });
|
|
187
|
+
}
|
|
188
|
+
return payloads;
|
|
189
|
+
}
|
|
178
190
|
case CONFIG_RPC.REPLACE_SOURCE: {
|
|
179
191
|
try {
|
|
180
192
|
const source = message.source;
|
|
@@ -198,9 +210,7 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
|
|
|
198
210
|
data: {
|
|
199
211
|
success: true,
|
|
200
212
|
...result,
|
|
201
|
-
message: result
|
|
202
|
-
? '配置已保存,需重启进程才能生效'
|
|
203
|
-
: '配置已保存',
|
|
213
|
+
message: configurationSaveMessage(result),
|
|
204
214
|
},
|
|
205
215
|
});
|
|
206
216
|
}
|
|
@@ -212,12 +222,46 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
|
|
|
212
222
|
}
|
|
213
223
|
return payloads;
|
|
214
224
|
}
|
|
225
|
+
case CONFIG_RPC.GET_PROJECT: {
|
|
226
|
+
if (ctx.authScope !== 'full') {
|
|
227
|
+
emit({ requestId, error: 'Full scope required' });
|
|
228
|
+
return payloads;
|
|
229
|
+
}
|
|
230
|
+
const { path, reveal } = message;
|
|
231
|
+
if (path !== undefined && (!Array.isArray(path) || path.length > CONFIG_PATCH_LIMITS.maxPathDepth
|
|
232
|
+
|| path.some(segment => typeof segment !== 'string' || !segment || segment.length > CONFIG_PATCH_LIMITS.maxSegmentLength))) {
|
|
233
|
+
emit({ requestId, error: 'path 必须是非空字符串数组' });
|
|
234
|
+
return payloads;
|
|
235
|
+
}
|
|
236
|
+
if (reveal !== undefined && typeof reveal !== 'boolean') {
|
|
237
|
+
emit({ requestId, error: 'reveal 必须是布尔值' });
|
|
238
|
+
return payloads;
|
|
239
|
+
}
|
|
240
|
+
if (!ctx.readProjectConfig) {
|
|
241
|
+
emit({ requestId, error: 'Config project read is not configured' });
|
|
242
|
+
return payloads;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
emit({ requestId, data: await ctx.readProjectConfig({ path: (path ?? []), reveal: reveal === true }) });
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
// Parser/IO messages can quote document source (and therefore secrets): never echo them.
|
|
249
|
+
emit({ requestId, error: 'Configuration read is unavailable; read the project file locally to diagnose it' });
|
|
250
|
+
}
|
|
251
|
+
return payloads;
|
|
252
|
+
}
|
|
253
|
+
case CONFIG_RPC.PATCH_PROJECT:
|
|
215
254
|
case CONFIG_RPC.PATCH: {
|
|
216
255
|
if (ctx.authScope !== 'full') {
|
|
217
256
|
emit({ requestId, error: 'Full scope required' });
|
|
218
257
|
return payloads;
|
|
219
258
|
}
|
|
259
|
+
const projectPatch = message.type === CONFIG_RPC.PATCH_PROJECT;
|
|
220
260
|
const { instanceKey, expectedRevision, patches } = message;
|
|
261
|
+
if (projectPatch && instanceKey !== undefined) {
|
|
262
|
+
emit({ requestId, error: 'Project patches must not specify instanceKey' });
|
|
263
|
+
return payloads;
|
|
264
|
+
}
|
|
221
265
|
if (instanceKey !== undefined && (typeof instanceKey !== 'string' || !instanceKey || instanceKey.length > 128)) {
|
|
222
266
|
emit({ requestId, error: 'instanceKey 必须是插件实例名,或省略以使用 Host 白名单' });
|
|
223
267
|
return payloads;
|
|
@@ -230,17 +274,36 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
|
|
|
230
274
|
emit({ requestId, error: 'patches 必须是数组' });
|
|
231
275
|
return payloads;
|
|
232
276
|
}
|
|
233
|
-
if (
|
|
277
|
+
if (projectPatch) {
|
|
278
|
+
const invalid = invalidProjectPatchOperations(patches);
|
|
279
|
+
if (invalid) {
|
|
280
|
+
emit({ requestId, error: invalid });
|
|
281
|
+
return payloads;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const patch = projectPatch ? ctx.patchProjectConfig : ctx.patchConfig;
|
|
285
|
+
if (!patch) {
|
|
234
286
|
emit({ requestId, error: 'Config patch is not configured' });
|
|
235
287
|
return payloads;
|
|
236
288
|
}
|
|
289
|
+
let result;
|
|
237
290
|
try {
|
|
238
|
-
|
|
291
|
+
result = await patch.call(ctx, {
|
|
239
292
|
...(instanceKey === undefined ? {} : { instanceKey }),
|
|
240
293
|
expectedRevision,
|
|
241
294
|
patches,
|
|
242
295
|
});
|
|
243
|
-
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
emit({
|
|
299
|
+
requestId,
|
|
300
|
+
error: `Failed to patch config: ${error instanceof Error ? error.message : String(error)}`,
|
|
301
|
+
});
|
|
302
|
+
return payloads;
|
|
303
|
+
}
|
|
304
|
+
if (result.status === 'saved') {
|
|
305
|
+
// The write has committed: a notification failure must not turn the receipt into a rejection.
|
|
306
|
+
try {
|
|
244
307
|
ctx.publishEvent?.('config:updated', {
|
|
245
308
|
pluginName: instanceKey ?? null,
|
|
246
309
|
keys: [...new Set(result.paths.map(path => path[0]))],
|
|
@@ -248,14 +311,9 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
|
|
|
248
311
|
revision: result.revision,
|
|
249
312
|
});
|
|
250
313
|
}
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
catch (error) {
|
|
254
|
-
emit({
|
|
255
|
-
requestId,
|
|
256
|
-
error: `Failed to patch config: ${error instanceof Error ? error.message : String(error)}`,
|
|
257
|
-
});
|
|
314
|
+
catch { /* subscribers resynchronise from the next read */ }
|
|
258
315
|
}
|
|
316
|
+
emit({ requestId, data: result });
|
|
259
317
|
return payloads;
|
|
260
318
|
}
|
|
261
319
|
case CONFIG_RPC.SET: {
|
|
@@ -291,10 +349,9 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
|
|
|
291
349
|
requestId,
|
|
292
350
|
data: {
|
|
293
351
|
success: true,
|
|
352
|
+
...result,
|
|
294
353
|
reloaded: false,
|
|
295
|
-
message: result
|
|
296
|
-
? '配置已保存,需重启进程才能生效'
|
|
297
|
-
: '配置已保存',
|
|
354
|
+
message: configurationSaveMessage(result),
|
|
298
355
|
},
|
|
299
356
|
});
|
|
300
357
|
}
|
|
@@ -306,503 +363,10 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
|
|
|
306
363
|
}
|
|
307
364
|
return payloads;
|
|
308
365
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const tree = ctx.listProjectFiles ? await ctx.listProjectFiles() : [];
|
|
312
|
-
emit({ requestId, data: { tree } });
|
|
313
|
-
}
|
|
314
|
-
catch (error) {
|
|
315
|
-
emit({
|
|
316
|
-
requestId,
|
|
317
|
-
error: `Failed to build file tree: ${error instanceof Error ? error.message : String(error)}`,
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
return payloads;
|
|
321
|
-
}
|
|
322
|
-
case 'files:read': {
|
|
323
|
-
try {
|
|
324
|
-
const filePath = String(message.filePath ?? '');
|
|
325
|
-
if (!filePath) {
|
|
326
|
-
emit({ requestId, error: 'filePath is required' });
|
|
327
|
-
return payloads;
|
|
328
|
-
}
|
|
329
|
-
if (!ctx.readProjectFile) {
|
|
330
|
-
emit({ requestId, error: 'File read is not configured' });
|
|
331
|
-
return payloads;
|
|
332
|
-
}
|
|
333
|
-
const file = await ctx.readProjectFile(filePath);
|
|
334
|
-
emit({ requestId, data: file });
|
|
335
|
-
}
|
|
336
|
-
catch (error) {
|
|
337
|
-
emit({
|
|
338
|
-
requestId,
|
|
339
|
-
error: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
return payloads;
|
|
343
|
-
}
|
|
344
|
-
case 'files:save': {
|
|
345
|
-
try {
|
|
346
|
-
const filePath = String(message.filePath ?? '');
|
|
347
|
-
const content = message.content;
|
|
348
|
-
if (!filePath) {
|
|
349
|
-
emit({ requestId, error: 'filePath is required' });
|
|
350
|
-
return payloads;
|
|
351
|
-
}
|
|
352
|
-
if (typeof content !== 'string') {
|
|
353
|
-
emit({ requestId, error: 'content field is required' });
|
|
354
|
-
return payloads;
|
|
355
|
-
}
|
|
356
|
-
if (!ctx.saveProjectFile) {
|
|
357
|
-
emit({ requestId, error: 'File write is not configured' });
|
|
358
|
-
return payloads;
|
|
359
|
-
}
|
|
360
|
-
await ctx.saveProjectFile(filePath, content);
|
|
361
|
-
emit({ requestId, data: { success: true, message: `文件已保存: ${filePath}` } });
|
|
362
|
-
}
|
|
363
|
-
catch (error) {
|
|
364
|
-
emit({
|
|
365
|
-
requestId,
|
|
366
|
-
error: `Failed to save file: ${error instanceof Error ? error.message : String(error)}`,
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
return payloads;
|
|
370
|
-
}
|
|
371
|
-
case 'env:list': {
|
|
372
|
-
try {
|
|
373
|
-
const files = ctx.listEnvFiles ? await ctx.listEnvFiles() : [];
|
|
374
|
-
emit({ requestId, data: { files } });
|
|
375
|
-
}
|
|
376
|
-
catch (error) {
|
|
377
|
-
emit({
|
|
378
|
-
requestId,
|
|
379
|
-
error: `Failed to list env files: ${error instanceof Error ? error.message : String(error)}`,
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
return payloads;
|
|
383
|
-
}
|
|
384
|
-
case 'env:get': {
|
|
385
|
-
try {
|
|
386
|
-
const filename = String(message.filename ?? '');
|
|
387
|
-
if (!filename) {
|
|
388
|
-
emit({ requestId, error: 'filename is required' });
|
|
389
|
-
return payloads;
|
|
390
|
-
}
|
|
391
|
-
if (!ctx.readEnvFile) {
|
|
392
|
-
emit({ requestId, error: 'Env read is not configured' });
|
|
393
|
-
return payloads;
|
|
394
|
-
}
|
|
395
|
-
const content = await ctx.readEnvFile(filename);
|
|
396
|
-
emit({ requestId, data: { content } });
|
|
397
|
-
}
|
|
398
|
-
catch (error) {
|
|
399
|
-
emit({
|
|
400
|
-
requestId,
|
|
401
|
-
error: `Failed to read env file: ${error instanceof Error ? error.message : String(error)}`,
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
return payloads;
|
|
405
|
-
}
|
|
406
|
-
case 'env:save': {
|
|
407
|
-
try {
|
|
408
|
-
const filename = String(message.filename ?? '');
|
|
409
|
-
const content = message.content;
|
|
410
|
-
if (!filename) {
|
|
411
|
-
emit({ requestId, error: 'filename is required' });
|
|
412
|
-
return payloads;
|
|
413
|
-
}
|
|
414
|
-
if (typeof content !== 'string') {
|
|
415
|
-
emit({ requestId, error: 'content field is required' });
|
|
416
|
-
return payloads;
|
|
417
|
-
}
|
|
418
|
-
if (!ctx.writeEnvFile) {
|
|
419
|
-
emit({ requestId, error: 'Env write is not configured' });
|
|
420
|
-
return payloads;
|
|
421
|
-
}
|
|
422
|
-
await ctx.writeEnvFile(filename, content);
|
|
423
|
-
emit({ requestId, data: { success: true, message: `环境文件已保存: ${filename}` } });
|
|
424
|
-
}
|
|
425
|
-
catch (error) {
|
|
426
|
-
emit({
|
|
427
|
-
requestId,
|
|
428
|
-
error: `Failed to save env file: ${error instanceof Error ? error.message : String(error)}`,
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
return payloads;
|
|
432
|
-
}
|
|
433
|
-
case 'schema:get': {
|
|
434
|
-
try {
|
|
435
|
-
const pluginName = typeof message.pluginName === 'string'
|
|
436
|
-
? message.pluginName
|
|
437
|
-
: undefined;
|
|
438
|
-
const schema = ctx.getSchema ? await ctx.getSchema(pluginName) : null;
|
|
439
|
-
emit({ requestId, data: schema });
|
|
440
|
-
}
|
|
441
|
-
catch (error) {
|
|
442
|
-
emit({
|
|
443
|
-
requestId,
|
|
444
|
-
error: `Failed to get schema: ${error instanceof Error ? error.message : String(error)}`,
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
return payloads;
|
|
448
|
-
}
|
|
449
|
-
case 'schema:get-all': {
|
|
450
|
-
try {
|
|
451
|
-
const schemas = ctx.getAllSchemas ? await ctx.getAllSchemas() : {};
|
|
452
|
-
emit({ requestId, data: schemas });
|
|
453
|
-
}
|
|
454
|
-
catch (error) {
|
|
455
|
-
emit({
|
|
456
|
-
requestId,
|
|
457
|
-
error: `Failed to get all schemas: ${error instanceof Error ? error.message : String(error)}`,
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
return payloads;
|
|
461
|
-
}
|
|
462
|
-
case 'db:info': {
|
|
463
|
-
if (ctx.database) {
|
|
464
|
-
emit({ requestId, data: ctx.database.info() });
|
|
465
|
-
return payloads;
|
|
466
|
-
}
|
|
467
|
-
if (ctx.dbInfo) {
|
|
468
|
-
try {
|
|
469
|
-
emit({ requestId, data: await ctx.dbInfo() });
|
|
470
|
-
}
|
|
471
|
-
catch (error) {
|
|
472
|
-
emit({
|
|
473
|
-
requestId,
|
|
474
|
-
error: error instanceof Error ? error.message : String(error),
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
return payloads;
|
|
478
|
-
}
|
|
479
|
-
emit({
|
|
480
|
-
requestId,
|
|
481
|
-
data: { dialect: null, connected: false, message: 'Database not configured on Plugin Runtime Host' },
|
|
482
|
-
});
|
|
483
|
-
return payloads;
|
|
484
|
-
}
|
|
485
|
-
case 'db:tables': {
|
|
486
|
-
try {
|
|
487
|
-
if (ctx.database) {
|
|
488
|
-
emit({ requestId, data: { tables: ctx.database.tables() } });
|
|
489
|
-
return payloads;
|
|
490
|
-
}
|
|
491
|
-
const tables = ctx.dbTables ? await ctx.dbTables() : [];
|
|
492
|
-
emit({ requestId, data: tables });
|
|
493
|
-
}
|
|
494
|
-
catch (error) {
|
|
495
|
-
emit({
|
|
496
|
-
requestId,
|
|
497
|
-
error: error instanceof Error ? error.message : String(error),
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
return payloads;
|
|
501
|
-
}
|
|
502
|
-
case 'db:select': {
|
|
503
|
-
const table = stringField(message, 'table');
|
|
504
|
-
if (!table)
|
|
505
|
-
return errorReply(payloads, requestId, 'table is required');
|
|
506
|
-
const page = positiveInteger(message.page, 1);
|
|
507
|
-
const pageSize = Math.min(positiveInteger(message.pageSize, 50), 500);
|
|
508
|
-
const where = recordField(message.where);
|
|
509
|
-
if (!ctx.database) {
|
|
510
|
-
emit({ requestId, data: { rows: [], total: 0, page, pageSize } });
|
|
511
|
-
return payloads;
|
|
512
|
-
}
|
|
513
|
-
try {
|
|
514
|
-
emit({ requestId, data: await ctx.database.select(table, page, pageSize, where) });
|
|
515
|
-
}
|
|
516
|
-
catch (error) {
|
|
517
|
-
emit({ requestId, error: `Failed to select: ${errorMessage(error)}` });
|
|
518
|
-
}
|
|
519
|
-
return payloads;
|
|
520
|
-
}
|
|
521
|
-
case 'db:insert': {
|
|
522
|
-
if (!ctx.database)
|
|
523
|
-
return errorReply(payloads, requestId, 'Database RPC is not available on Plugin Runtime Host');
|
|
524
|
-
const table = stringField(message, 'table');
|
|
525
|
-
const row = recordField(message.row);
|
|
526
|
-
if (!table || !row)
|
|
527
|
-
return errorReply(payloads, requestId, 'table and row are required');
|
|
528
|
-
try {
|
|
529
|
-
await ctx.database.insert(table, row);
|
|
530
|
-
emit({ requestId, data: { success: true } });
|
|
531
|
-
}
|
|
532
|
-
catch (error) {
|
|
533
|
-
emit({ requestId, error: `Failed to insert: ${errorMessage(error)}` });
|
|
534
|
-
}
|
|
535
|
-
return payloads;
|
|
536
|
-
}
|
|
537
|
-
case 'db:update': {
|
|
538
|
-
if (!ctx.database)
|
|
539
|
-
return errorReply(payloads, requestId, 'Database RPC is not available on Plugin Runtime Host');
|
|
540
|
-
const table = stringField(message, 'table');
|
|
541
|
-
const row = recordField(message.row);
|
|
542
|
-
const where = recordField(message.where);
|
|
543
|
-
if (!table || !row || !where) {
|
|
544
|
-
return errorReply(payloads, requestId, 'table, row, and where are required');
|
|
545
|
-
}
|
|
546
|
-
try {
|
|
547
|
-
const affected = await ctx.database.update(table, row, where);
|
|
548
|
-
emit({ requestId, data: { success: true, affected } });
|
|
549
|
-
}
|
|
550
|
-
catch (error) {
|
|
551
|
-
emit({ requestId, error: `Failed to update: ${errorMessage(error)}` });
|
|
552
|
-
}
|
|
553
|
-
return payloads;
|
|
554
|
-
}
|
|
555
|
-
case 'db:delete': {
|
|
556
|
-
if (!ctx.database)
|
|
557
|
-
return errorReply(payloads, requestId, 'Database RPC is not available on Plugin Runtime Host');
|
|
558
|
-
const table = stringField(message, 'table');
|
|
559
|
-
const where = recordField(message.where);
|
|
560
|
-
if (!table || !where)
|
|
561
|
-
return errorReply(payloads, requestId, 'table and where are required');
|
|
562
|
-
try {
|
|
563
|
-
const deleted = await ctx.database.delete(table, where);
|
|
564
|
-
emit({ requestId, data: { success: true, deleted } });
|
|
565
|
-
}
|
|
566
|
-
catch (error) {
|
|
567
|
-
emit({ requestId, error: `Failed to delete: ${errorMessage(error)}` });
|
|
568
|
-
}
|
|
569
|
-
return payloads;
|
|
570
|
-
}
|
|
571
|
-
case 'db:drop-table': {
|
|
572
|
-
if (!ctx.database)
|
|
573
|
-
return errorReply(payloads, requestId, 'Database RPC is not available on Plugin Runtime Host');
|
|
574
|
-
const table = stringField(message, 'table');
|
|
575
|
-
if (!table)
|
|
576
|
-
return errorReply(payloads, requestId, 'table is required');
|
|
577
|
-
try {
|
|
578
|
-
await ctx.database.dropTable(table);
|
|
579
|
-
emit({ requestId, data: { success: true } });
|
|
580
|
-
}
|
|
581
|
-
catch (error) {
|
|
582
|
-
emit({ requestId, error: `Failed to drop table: ${errorMessage(error)}` });
|
|
583
|
-
}
|
|
584
|
-
return payloads;
|
|
585
|
-
}
|
|
586
|
-
case 'db:kv:get': {
|
|
587
|
-
const table = stringField(message, 'table');
|
|
588
|
-
const key = stringField(message, 'key');
|
|
589
|
-
if (!table || !key)
|
|
590
|
-
return errorReply(payloads, requestId, 'table and key are required');
|
|
591
|
-
if (!ctx.database) {
|
|
592
|
-
emit({ requestId, data: { key, value: null } });
|
|
593
|
-
return payloads;
|
|
594
|
-
}
|
|
595
|
-
try {
|
|
596
|
-
emit({ requestId, data: { key, value: await ctx.database.kvGet(table, key) } });
|
|
597
|
-
}
|
|
598
|
-
catch (error) {
|
|
599
|
-
emit({ requestId, error: `Failed to get kv: ${errorMessage(error)}` });
|
|
600
|
-
}
|
|
601
|
-
return payloads;
|
|
602
|
-
}
|
|
603
|
-
case 'db:kv:set': {
|
|
604
|
-
if (!ctx.database)
|
|
605
|
-
return errorReply(payloads, requestId, 'Database RPC is not available on Plugin Runtime Host');
|
|
606
|
-
const table = stringField(message, 'table');
|
|
607
|
-
const key = stringField(message, 'key');
|
|
608
|
-
if (!table || !key)
|
|
609
|
-
return errorReply(payloads, requestId, 'table and key are required');
|
|
610
|
-
try {
|
|
611
|
-
const ttl = typeof message.ttl === 'number' && Number.isFinite(message.ttl)
|
|
612
|
-
? message.ttl
|
|
613
|
-
: undefined;
|
|
614
|
-
await ctx.database.kvSet(table, key, message.value, ttl);
|
|
615
|
-
emit({ requestId, data: { success: true } });
|
|
616
|
-
}
|
|
617
|
-
catch (error) {
|
|
618
|
-
emit({ requestId, error: `Failed to set kv: ${errorMessage(error)}` });
|
|
619
|
-
}
|
|
620
|
-
return payloads;
|
|
621
|
-
}
|
|
622
|
-
case 'db:kv:delete': {
|
|
623
|
-
if (!ctx.database)
|
|
624
|
-
return errorReply(payloads, requestId, 'Database RPC is not available on Plugin Runtime Host');
|
|
625
|
-
const table = stringField(message, 'table');
|
|
626
|
-
const key = stringField(message, 'key');
|
|
627
|
-
if (!table || !key)
|
|
628
|
-
return errorReply(payloads, requestId, 'table and key are required');
|
|
629
|
-
try {
|
|
630
|
-
await ctx.database.kvDelete(table, key);
|
|
631
|
-
emit({ requestId, data: { success: true } });
|
|
632
|
-
}
|
|
633
|
-
catch (error) {
|
|
634
|
-
emit({ requestId, error: `Failed to delete kv: ${errorMessage(error)}` });
|
|
635
|
-
}
|
|
636
|
-
return payloads;
|
|
637
|
-
}
|
|
638
|
-
case 'db:kv:entries': {
|
|
639
|
-
const table = stringField(message, 'table');
|
|
640
|
-
if (!table)
|
|
641
|
-
return errorReply(payloads, requestId, 'table is required');
|
|
642
|
-
if (!ctx.database) {
|
|
643
|
-
emit({ requestId, data: { entries: [] } });
|
|
644
|
-
return payloads;
|
|
645
|
-
}
|
|
646
|
-
try {
|
|
647
|
-
emit({ requestId, data: { entries: await ctx.database.kvEntries(table) } });
|
|
648
|
-
}
|
|
649
|
-
catch (error) {
|
|
650
|
-
emit({ requestId, error: `Failed to get entries: ${errorMessage(error)}` });
|
|
651
|
-
}
|
|
652
|
-
return payloads;
|
|
653
|
-
}
|
|
654
|
-
case 'endpoint.list': {
|
|
655
|
-
try {
|
|
656
|
-
const endpoints = ctx.listEndpoints ? await ctx.listEndpoints() : [];
|
|
657
|
-
emit({ requestId, data: { endpoints } });
|
|
658
|
-
}
|
|
659
|
-
catch (error) {
|
|
660
|
-
emit({
|
|
661
|
-
requestId,
|
|
662
|
-
error: error instanceof Error ? error.message : String(error),
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
return payloads;
|
|
666
|
-
}
|
|
667
|
-
case 'endpoint.info': {
|
|
668
|
-
try {
|
|
669
|
-
const data = recordField(message.data) ?? message;
|
|
670
|
-
const adapter = String(data.adapter ?? '');
|
|
671
|
-
const endpointKey = String(data.endpointKey ?? '');
|
|
672
|
-
if (!adapter || !endpointKey) {
|
|
673
|
-
emit({ requestId, error: 'adapter and endpointKey are required' });
|
|
674
|
-
return payloads;
|
|
675
|
-
}
|
|
676
|
-
if (!ctx.getEndpoint) {
|
|
677
|
-
emit({ requestId, error: 'Endpoint registry is not configured' });
|
|
678
|
-
return payloads;
|
|
679
|
-
}
|
|
680
|
-
const endpoint = await ctx.getEndpoint(adapter, endpointKey);
|
|
681
|
-
if (!endpoint) {
|
|
682
|
-
emit({ requestId, error: 'endpoint not found' });
|
|
683
|
-
return payloads;
|
|
684
|
-
}
|
|
685
|
-
emit({ requestId, data: endpoint });
|
|
686
|
-
}
|
|
687
|
-
catch (error) {
|
|
688
|
-
emit({
|
|
689
|
-
requestId,
|
|
690
|
-
error: error instanceof Error ? error.message : String(error),
|
|
691
|
-
});
|
|
692
|
-
}
|
|
693
|
-
return payloads;
|
|
694
|
-
}
|
|
695
|
-
case ENDPOINT_RPC.TEST: {
|
|
696
|
-
const data = recordField(message.data) ?? message;
|
|
697
|
-
const adapter = stringField(data, 'adapter');
|
|
698
|
-
const endpointKey = stringField(data, 'endpointKey');
|
|
699
|
-
if (!adapter || !endpointKey) {
|
|
700
|
-
emit({ requestId, error: 'adapter and endpointKey are required' });
|
|
366
|
+
default: {
|
|
367
|
+
if (await dispatchHostResourceConsoleRpc(type, message, ctx, requestId, payloads)) {
|
|
701
368
|
return payloads;
|
|
702
369
|
}
|
|
703
|
-
if (!ctx.getEndpoint) {
|
|
704
|
-
emit({ requestId, error: 'Endpoint registry is not configured' });
|
|
705
|
-
return payloads;
|
|
706
|
-
}
|
|
707
|
-
const startedAt = Date.now();
|
|
708
|
-
try {
|
|
709
|
-
const endpoint = await ctx.getEndpoint(adapter, endpointKey);
|
|
710
|
-
if (!endpoint) {
|
|
711
|
-
emit({ requestId, data: {
|
|
712
|
-
reachable: false,
|
|
713
|
-
connected: false,
|
|
714
|
-
phase: 'unconfigured',
|
|
715
|
-
latencyMs: Date.now() - startedAt,
|
|
716
|
-
message: 'Endpoint 未注册到当前 generation',
|
|
717
|
-
} });
|
|
718
|
-
return payloads;
|
|
719
|
-
}
|
|
720
|
-
emit({ requestId, data: {
|
|
721
|
-
reachable: endpoint.connected && endpoint.status === 'online',
|
|
722
|
-
connected: endpoint.connected,
|
|
723
|
-
status: endpoint.status,
|
|
724
|
-
admitted: endpoint.admitted,
|
|
725
|
-
transportState: endpoint.transportState,
|
|
726
|
-
phase: endpoint.phase ?? (endpoint.connected ? 'online' : 'pending'),
|
|
727
|
-
pendingLogin: endpoint.pendingLogin ?? false,
|
|
728
|
-
latencyMs: Date.now() - startedAt,
|
|
729
|
-
message: endpoint.connected
|
|
730
|
-
? 'Endpoint 已连接'
|
|
731
|
-
: endpoint.pendingLogin
|
|
732
|
-
? 'Endpoint 等待登录操作'
|
|
733
|
-
: 'Endpoint 当前离线',
|
|
734
|
-
} });
|
|
735
|
-
}
|
|
736
|
-
catch (error) {
|
|
737
|
-
emit({ requestId, data: {
|
|
738
|
-
reachable: false,
|
|
739
|
-
connected: false,
|
|
740
|
-
phase: 'failed',
|
|
741
|
-
latencyMs: Date.now() - startedAt,
|
|
742
|
-
message: error instanceof Error ? error.message : String(error),
|
|
743
|
-
} });
|
|
744
|
-
}
|
|
745
|
-
return payloads;
|
|
746
|
-
}
|
|
747
|
-
case 'endpoint.send_message': {
|
|
748
|
-
try {
|
|
749
|
-
const data = recordField(message.data) ?? message;
|
|
750
|
-
const adapter = String(data.adapter ?? '');
|
|
751
|
-
const endpointKey = String(data.endpointKey ?? '');
|
|
752
|
-
const channelId = String(data.channelId ?? data.id ?? '');
|
|
753
|
-
const channelType = String(data.channelType ?? data.type ?? '');
|
|
754
|
-
const content = data.content;
|
|
755
|
-
if (!adapter || !endpointKey || !channelId || !channelType || content === undefined) {
|
|
756
|
-
emit({
|
|
757
|
-
requestId,
|
|
758
|
-
error: 'adapter, endpointKey, channelId, channelType, and content are required',
|
|
759
|
-
});
|
|
760
|
-
return payloads;
|
|
761
|
-
}
|
|
762
|
-
if (!ctx.sendEndpointMessage) {
|
|
763
|
-
emit({ requestId, error: 'Endpoint send is not configured' });
|
|
764
|
-
return payloads;
|
|
765
|
-
}
|
|
766
|
-
const result = await ctx.sendEndpointMessage({
|
|
767
|
-
adapter,
|
|
768
|
-
endpointKey,
|
|
769
|
-
conversation: wireConversation(channelType, channelId, data.parent),
|
|
770
|
-
content,
|
|
771
|
-
});
|
|
772
|
-
emit({ requestId, data: { messageId: result.messageId } });
|
|
773
|
-
}
|
|
774
|
-
catch (error) {
|
|
775
|
-
emit({
|
|
776
|
-
requestId,
|
|
777
|
-
error: error instanceof Error ? error.message : String(error),
|
|
778
|
-
});
|
|
779
|
-
}
|
|
780
|
-
return payloads;
|
|
781
|
-
}
|
|
782
|
-
case 'system:restart': {
|
|
783
|
-
try {
|
|
784
|
-
if (!ctx.requestRestart) {
|
|
785
|
-
emit({ requestId, error: 'Process restart is not configured' });
|
|
786
|
-
return payloads;
|
|
787
|
-
}
|
|
788
|
-
ctx.publishEvent?.('system:restarting', { timestamp: Date.now() });
|
|
789
|
-
emit({ requestId, data: { success: true, message: '正在重启...' } });
|
|
790
|
-
// Delay so the HTTP response can flush (parity with legacy host-api).
|
|
791
|
-
setTimeout(() => {
|
|
792
|
-
void Promise.resolve(ctx.requestRestart?.()).catch(() => {
|
|
793
|
-
/* restart is best-effort */
|
|
794
|
-
});
|
|
795
|
-
}, 500);
|
|
796
|
-
}
|
|
797
|
-
catch (error) {
|
|
798
|
-
emit({
|
|
799
|
-
requestId,
|
|
800
|
-
error: error instanceof Error ? error.message : String(error),
|
|
801
|
-
});
|
|
802
|
-
}
|
|
803
|
-
return payloads;
|
|
804
|
-
}
|
|
805
|
-
default:
|
|
806
370
|
if (type.startsWith('db:')) {
|
|
807
371
|
emit({
|
|
808
372
|
requestId,
|
|
@@ -812,63 +376,6 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
|
|
|
812
376
|
}
|
|
813
377
|
emit({ requestId, error: `Unknown RPC type: ${type || '<empty>'}` });
|
|
814
378
|
return payloads;
|
|
379
|
+
}
|
|
815
380
|
}
|
|
816
381
|
}
|
|
817
|
-
function stringField(message, key) {
|
|
818
|
-
const value = message[key];
|
|
819
|
-
return typeof value === 'string' ? value.trim() : '';
|
|
820
|
-
}
|
|
821
|
-
function recordField(value) {
|
|
822
|
-
return value && typeof value === 'object' && !Array.isArray(value)
|
|
823
|
-
? value
|
|
824
|
-
: undefined;
|
|
825
|
-
}
|
|
826
|
-
function positiveInteger(value, fallback) {
|
|
827
|
-
const parsed = Number(value ?? fallback);
|
|
828
|
-
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
829
|
-
}
|
|
830
|
-
function errorMessage(error) {
|
|
831
|
-
return error instanceof Error ? error.message : String(error);
|
|
832
|
-
}
|
|
833
|
-
function errorReply(payloads, requestId, error) {
|
|
834
|
-
payloads.push({ requestId, error });
|
|
835
|
-
return payloads;
|
|
836
|
-
}
|
|
837
|
-
/** HTTP paths under apiBase that demo scope may call (ADR 0016). */
|
|
838
|
-
export function isDemoHttpAllowed(method, pathname, apiBase) {
|
|
839
|
-
const normalizedBase = apiBase.replace(/\/$/u, '') || '/api';
|
|
840
|
-
const m = method.toUpperCase();
|
|
841
|
-
// These handlers require an authenticated principal and verify its bound personal owner.
|
|
842
|
-
if ((m === 'GET' || m === 'PUT') && (pathname === `${normalizedBase}/agent/personal-models`
|
|
843
|
-
|| pathname === `${normalizedBase}/agent/personal-session`))
|
|
844
|
-
return true;
|
|
845
|
-
if (m === 'GET' && pathname === `${normalizedBase}/agent/personal-sessions`)
|
|
846
|
-
return true;
|
|
847
|
-
if (m === 'GET' && pathname === `${normalizedBase}/events`)
|
|
848
|
-
return true;
|
|
849
|
-
if (m === 'GET' && pathname === `${normalizedBase}/events/history`)
|
|
850
|
-
return true;
|
|
851
|
-
if (m === 'GET' && DEMO_INTROSPECTION_PATHS.has(pathname.slice(normalizedBase.length)))
|
|
852
|
-
return true;
|
|
853
|
-
if (m === 'POST' && pathname === `${normalizedBase}/console/request`)
|
|
854
|
-
return true;
|
|
855
|
-
// Read-only dashboard / plugin inspection routes (legacy host-api parity).
|
|
856
|
-
if (m === 'GET' && pathname === `${normalizedBase}/system/status`)
|
|
857
|
-
return true;
|
|
858
|
-
if (m === 'GET' && pathname === `${normalizedBase}/stats`)
|
|
859
|
-
return true;
|
|
860
|
-
if (m === 'GET' && (pathname === `${normalizedBase}/plugins`
|
|
861
|
-
|| pathname.startsWith(`${normalizedBase}/plugins/`)))
|
|
862
|
-
return true;
|
|
863
|
-
return false;
|
|
864
|
-
}
|
|
865
|
-
const DEMO_INTROSPECTION_PATHS = new Set([
|
|
866
|
-
'/introspection/commands',
|
|
867
|
-
'/introspection/middlewares',
|
|
868
|
-
'/introspection/components',
|
|
869
|
-
'/introspection/endpoints',
|
|
870
|
-
'/introspection/bindings',
|
|
871
|
-
'/introspection/tools',
|
|
872
|
-
'/introspection/prompt-sections',
|
|
873
|
-
'/introspection/mcp',
|
|
874
|
-
]);
|