@zhin.js/host-http 1.0.17 → 1.1.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 +6 -0
- package/lib/console-plugin-rpc.d.ts +3 -0
- package/lib/console-plugin-rpc.js +130 -0
- package/lib/console-rest-pages.d.ts +1 -1
- package/lib/console-rest-pages.js +11 -20
- package/lib/{console-rpc-extended.d.ts → console-rpc-extended/contracts.d.ts} +22 -21
- package/lib/console-rpc-extended/contracts.js +4 -0
- package/lib/console-rpc-extended/dispatcher.d.ts +2 -0
- package/lib/console-rpc-extended/dispatcher.js +92 -0
- package/lib/console-rpc-extended/endpoint-rpc.d.ts +24 -0
- package/lib/console-rpc-extended/endpoint-rpc.js +137 -0
- package/lib/console-rpc-extended/inbox-rpc.d.ts +23 -0
- package/lib/console-rpc-extended/inbox-rpc.js +292 -0
- package/lib/console-rpc-extended/index.d.ts +2 -0
- package/lib/console-rpc-extended/index.js +1 -0
- package/lib/console-rpc-extended/login-rpc.d.ts +4 -0
- package/lib/console-rpc-extended/login-rpc.js +46 -0
- package/lib/console-rpc-extended/rpc-values.d.ts +16 -0
- package/lib/console-rpc-extended/rpc-values.js +87 -0
- package/lib/console-rpc-extended/schedule-rpc.d.ts +6 -0
- package/lib/console-rpc-extended/schedule-rpc.js +109 -0
- package/lib/console-rpc-extended/workroom-rpc.d.ts +3 -0
- package/lib/console-rpc-extended/workroom-rpc.js +104 -0
- package/lib/console-rpc.d.ts +71 -15
- package/lib/console-rpc.js +116 -82
- package/lib/index.d.ts +3 -2
- package/lib/index.js +2 -1
- package/lib/marketplace-registry.d.ts +12 -0
- package/lib/marketplace-registry.js +45 -0
- package/package.json +5 -5
- package/src/console-plugin-rpc.ts +142 -0
- package/src/console-rest-pages.ts +14 -26
- package/src/console-rpc-extended/contracts.ts +155 -0
- package/src/console-rpc-extended/dispatcher.ts +114 -0
- package/src/console-rpc-extended/endpoint-rpc.ts +174 -0
- package/src/console-rpc-extended/inbox-rpc.ts +342 -0
- package/src/console-rpc-extended/index.ts +8 -0
- package/src/console-rpc-extended/login-rpc.ts +50 -0
- package/src/console-rpc-extended/rpc-values.ts +104 -0
- package/src/console-rpc-extended/schedule-rpc.ts +117 -0
- package/src/console-rpc-extended/workroom-rpc.ts +116 -0
- package/src/console-rpc.ts +189 -97
- package/src/index.ts +7 -2
- package/src/marketplace-registry.ts +58 -0
- package/lib/console-rpc-extended.js +0 -916
- package/src/console-rpc-extended.ts +0 -1192
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { PLUGIN_RPC } from '@zhin.js/console-protocol';
|
|
2
|
+
import type {
|
|
3
|
+
RuntimeConsoleRpcContext,
|
|
4
|
+
RuntimeConsoleRpcMessage,
|
|
5
|
+
RuntimeConsoleRpcReply,
|
|
6
|
+
} from './console-rpc.js';
|
|
7
|
+
|
|
8
|
+
/** Dispatches the plugin-management slice; undefined means the type belongs elsewhere. */
|
|
9
|
+
export async function dispatchPluginConsoleRpc(
|
|
10
|
+
type: string,
|
|
11
|
+
message: RuntimeConsoleRpcMessage,
|
|
12
|
+
ctx: RuntimeConsoleRpcContext,
|
|
13
|
+
): Promise<RuntimeConsoleRpcReply | undefined> {
|
|
14
|
+
const requestId = message.requestId as number | string | undefined;
|
|
15
|
+
try {
|
|
16
|
+
switch (type) {
|
|
17
|
+
case PLUGIN_RPC.SET_ENABLED: {
|
|
18
|
+
const instanceKey = stringField(message, 'instanceKey');
|
|
19
|
+
const enabled = message.enabled;
|
|
20
|
+
if (!instanceKey || typeof enabled !== 'boolean') {
|
|
21
|
+
return { requestId, error: 'instanceKey and boolean enabled are required' };
|
|
22
|
+
}
|
|
23
|
+
if (!ctx.setPluginEnabled) {
|
|
24
|
+
return { requestId, error: 'Plugin lifecycle management is not configured' };
|
|
25
|
+
}
|
|
26
|
+
const lifecycle = await ctx.setPluginEnabled(instanceKey, enabled);
|
|
27
|
+
ctx.publishEvent?.('plugin:lifecycle-updated', { instanceKey, enabled });
|
|
28
|
+
if (ctx.requestRestart) {
|
|
29
|
+
setTimeout(() => void Promise.resolve(ctx.requestRestart?.()).catch(() => undefined), 500);
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
requestId,
|
|
33
|
+
data: {
|
|
34
|
+
success: true,
|
|
35
|
+
instanceKey,
|
|
36
|
+
enabled,
|
|
37
|
+
disabled: lifecycle.disabled,
|
|
38
|
+
restartRequired: true,
|
|
39
|
+
message: `${instanceKey} 已${enabled ? '启用' : '停用'},Host 正在重启`,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
case PLUGIN_RPC.PLAN_INSTALL: {
|
|
44
|
+
const packageName = stringField(message, 'packageName');
|
|
45
|
+
if (!packageName) return { requestId, error: 'packageName is required' };
|
|
46
|
+
if (!ctx.pluginManagement) return { requestId, error: 'Plugin management is not configured' };
|
|
47
|
+
return { requestId, data: await ctx.pluginManagement.planInstall(packageName) };
|
|
48
|
+
}
|
|
49
|
+
case PLUGIN_RPC.INSTALL: {
|
|
50
|
+
const packageName = stringField(message, 'packageName');
|
|
51
|
+
if (!packageName) return { requestId, error: 'packageName is required' };
|
|
52
|
+
if (!ctx.pluginManagement?.install) {
|
|
53
|
+
return { requestId, error: 'Plugin installation is not configured' };
|
|
54
|
+
}
|
|
55
|
+
const result = await ctx.pluginManagement.install(
|
|
56
|
+
packageName,
|
|
57
|
+
optionalString(message.expectedRevision),
|
|
58
|
+
);
|
|
59
|
+
ctx.publishEvent?.('plugin:installed', { packageName, restartRequired: result.restartRequired });
|
|
60
|
+
return { requestId, data: { success: true, ...result } };
|
|
61
|
+
}
|
|
62
|
+
case PLUGIN_RPC.PLAN_UNINSTALL: {
|
|
63
|
+
const packageName = stringField(message, 'packageName');
|
|
64
|
+
if (!packageName) return { requestId, error: 'packageName is required' };
|
|
65
|
+
if (!ctx.pluginManagement?.planUninstall) {
|
|
66
|
+
return { requestId, error: 'Plugin uninstall planning is not configured' };
|
|
67
|
+
}
|
|
68
|
+
return { requestId, data: await ctx.pluginManagement.planUninstall(packageName) };
|
|
69
|
+
}
|
|
70
|
+
case PLUGIN_RPC.UNINSTALL: {
|
|
71
|
+
const packageName = stringField(message, 'packageName');
|
|
72
|
+
if (!packageName || stringField(message, 'confirmation') !== packageName) {
|
|
73
|
+
return { requestId, error: 'confirmation must exactly match packageName' };
|
|
74
|
+
}
|
|
75
|
+
if (!ctx.pluginManagement?.uninstall) {
|
|
76
|
+
return { requestId, error: 'Plugin uninstall is not configured' };
|
|
77
|
+
}
|
|
78
|
+
const result = await ctx.pluginManagement.uninstall(
|
|
79
|
+
packageName,
|
|
80
|
+
optionalString(message.expectedRevision),
|
|
81
|
+
);
|
|
82
|
+
ctx.publishEvent?.('plugin:uninstalled', { packageName, restartRequired: result.restartRequired });
|
|
83
|
+
return { requestId, data: { success: true, ...result } };
|
|
84
|
+
}
|
|
85
|
+
case PLUGIN_RPC.PLAN_UPDATE: {
|
|
86
|
+
const packageName = stringField(message, 'packageName');
|
|
87
|
+
const targetVersion = stringField(message, 'targetVersion');
|
|
88
|
+
if (!packageName || !targetVersion) {
|
|
89
|
+
return { requestId, error: 'packageName and targetVersion are required' };
|
|
90
|
+
}
|
|
91
|
+
if (!ctx.pluginManagement?.planUpdate) {
|
|
92
|
+
return { requestId, error: 'Plugin update planning is not configured' };
|
|
93
|
+
}
|
|
94
|
+
return { requestId, data: await ctx.pluginManagement.planUpdate(packageName, targetVersion) };
|
|
95
|
+
}
|
|
96
|
+
case PLUGIN_RPC.UPDATE: {
|
|
97
|
+
const packageName = stringField(message, 'packageName');
|
|
98
|
+
const targetVersion = stringField(message, 'targetVersion');
|
|
99
|
+
if (!packageName || !targetVersion) {
|
|
100
|
+
return { requestId, error: 'packageName and targetVersion are required' };
|
|
101
|
+
}
|
|
102
|
+
if (!ctx.pluginManagement?.update) return { requestId, error: 'Plugin update is not configured' };
|
|
103
|
+
const result = await ctx.pluginManagement.update(
|
|
104
|
+
packageName,
|
|
105
|
+
targetVersion,
|
|
106
|
+
optionalString(message.expectedRevision),
|
|
107
|
+
);
|
|
108
|
+
ctx.publishEvent?.('plugin:updated', {
|
|
109
|
+
packageName,
|
|
110
|
+
version: result.installedVersion,
|
|
111
|
+
restartRequired: result.restartRequired,
|
|
112
|
+
});
|
|
113
|
+
return { requestId, data: { success: true, ...result } };
|
|
114
|
+
}
|
|
115
|
+
case PLUGIN_RPC.VALIDATE_CONFIG: {
|
|
116
|
+
const pluginName = stringField(message, 'pluginName');
|
|
117
|
+
if (!pluginName) return { requestId, error: 'pluginName is required' };
|
|
118
|
+
if (!ctx.validatePluginConfig) return { requestId, error: 'Config validation is not configured' };
|
|
119
|
+
return { requestId, data: await ctx.validatePluginConfig(pluginName, message.data) };
|
|
120
|
+
}
|
|
121
|
+
case PLUGIN_RPC.DIAGNOSE: {
|
|
122
|
+
const pluginName = stringField(message, 'pluginName');
|
|
123
|
+
if (!pluginName) return { requestId, error: 'pluginName is required' };
|
|
124
|
+
if (!ctx.diagnosePlugin) return { requestId, error: 'Plugin diagnostics are not configured' };
|
|
125
|
+
return { requestId, data: await ctx.diagnosePlugin(pluginName) };
|
|
126
|
+
}
|
|
127
|
+
default:
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return { requestId, error: error instanceof Error ? error.message : String(error) };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function stringField(message: RuntimeConsoleRpcMessage, key: string): string {
|
|
136
|
+
const value = message[key];
|
|
137
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function optionalString(value: unknown): string | undefined {
|
|
141
|
+
return typeof value === 'string' ? value : undefined;
|
|
142
|
+
}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Console REST pages — logs / marketplace / introspection / agent sessions。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* - logs-rest-api.ts(SystemLog 模型)
|
|
6
|
-
* - marketplace-rest-api.ts(plugins.json + npmmirror)
|
|
7
|
-
* - introspection-rest-api.ts(分页内省列表)
|
|
8
|
-
* - agent-sessions-rest-api.ts(ADR 0010 D3 session tree)
|
|
4
|
+
* 统一提供日志、市场、Runtime snapshot 内省和 Agent session tree 页面。
|
|
9
5
|
*
|
|
10
6
|
* 数据源不可用时降级:读操作返回空数组 + `note` 说明,session tree 返回 503;
|
|
11
7
|
* 写操作要求 `ctx.fullScope && authScope === 'full'`,否则 403。
|
|
@@ -15,6 +11,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
15
11
|
import { join } from 'node:path';
|
|
16
12
|
import type { HttpHost } from './http-host.js';
|
|
17
13
|
import { HttpBodyError, readJsonBody } from './json-body.js';
|
|
14
|
+
import { MarketplaceRegistry } from './marketplace-registry.js';
|
|
18
15
|
import type { AuthScope } from './token-registry.js';
|
|
19
16
|
|
|
20
17
|
/** 与 basic/cli 接线方对齐的上下文约定。 */
|
|
@@ -48,7 +45,7 @@ export interface ConsoleRestCtx {
|
|
|
48
45
|
) => boolean | undefined | Promise<boolean | undefined>;
|
|
49
46
|
}
|
|
50
47
|
|
|
51
|
-
/** 内省数据门面 — 由
|
|
48
|
+
/** 内省数据门面 — 由 composition root 从当前 Runtime snapshot 装配。 */
|
|
52
49
|
export interface ConsoleAgentIntrospection {
|
|
53
50
|
commands?(): readonly unknown[];
|
|
54
51
|
middlewares?(): readonly unknown[];
|
|
@@ -135,7 +132,6 @@ const INTROSPECTION_PAGE_SIZES = {
|
|
|
135
132
|
|
|
136
133
|
type IntrospectionKind = keyof typeof INTROSPECTION_PAGE_SIZES;
|
|
137
134
|
|
|
138
|
-
let pluginsCache: { data: unknown[]; ts: number } | null = null;
|
|
139
135
|
const PLUGINS_CACHE_TTL = 5 * 60 * 1000;
|
|
140
136
|
|
|
141
137
|
/**
|
|
@@ -342,19 +338,11 @@ function registerMarketplaceRoutes(
|
|
|
342
338
|
pluginRegistryUrl: string,
|
|
343
339
|
npmRegistryUrl: string,
|
|
344
340
|
): void {
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
signal: AbortSignal.timeout(30_000),
|
|
351
|
-
});
|
|
352
|
-
if (!resp.ok) throw new Error(`plugins.json fetch failed: ${resp.status}`);
|
|
353
|
-
const json = (await resp.json()) as { plugins?: unknown[] };
|
|
354
|
-
const list = json.plugins || [];
|
|
355
|
-
pluginsCache = { data: list, ts: Date.now() };
|
|
356
|
-
return list;
|
|
357
|
-
};
|
|
341
|
+
const registry = new MarketplaceRegistry({
|
|
342
|
+
fetchFn,
|
|
343
|
+
url: pluginRegistryUrl,
|
|
344
|
+
ttlMs: PLUGINS_CACHE_TTL,
|
|
345
|
+
});
|
|
358
346
|
|
|
359
347
|
route('GET', '/pub/marketplace/search', async (_request, response, url) => {
|
|
360
348
|
const q = url.searchParams.get('q') ?? url.searchParams.get('keyword') ?? '';
|
|
@@ -368,7 +356,7 @@ function registerMarketplaceRoutes(
|
|
|
368
356
|
const searchKeyword = q.trim().toLowerCase();
|
|
369
357
|
|
|
370
358
|
try {
|
|
371
|
-
const allPlugins = (await
|
|
359
|
+
const allPlugins = (await registry.list()) as readonly Record<string, unknown>[];
|
|
372
360
|
let plugins = allPlugins.map((p) => ({
|
|
373
361
|
name: p.name,
|
|
374
362
|
displayName: p.displayName || '',
|
|
@@ -429,11 +417,11 @@ function registerMarketplaceRoutes(
|
|
|
429
417
|
try {
|
|
430
418
|
let cachedDownloads = { weekly: 0, monthly: 0 };
|
|
431
419
|
try {
|
|
432
|
-
const
|
|
420
|
+
const registryPlugins = (await registry.list()) as readonly {
|
|
433
421
|
name?: string;
|
|
434
422
|
downloads?: unknown;
|
|
435
|
-
}
|
|
436
|
-
const cached =
|
|
423
|
+
}[];
|
|
424
|
+
const cached = registryPlugins.find((p) => p.name === pkgName);
|
|
437
425
|
if (cached?.downloads && typeof cached.downloads === 'object') {
|
|
438
426
|
cachedDownloads = cached.downloads as { weekly: number; monthly: number };
|
|
439
427
|
}
|
|
@@ -562,7 +550,7 @@ async function listInstalledPluginPackages(
|
|
|
562
550
|
}
|
|
563
551
|
|
|
564
552
|
// ---------------------------------------------------------------------------
|
|
565
|
-
//
|
|
553
|
+
// Runtime snapshot introspection
|
|
566
554
|
// ---------------------------------------------------------------------------
|
|
567
555
|
|
|
568
556
|
function registerIntrospectionRoutes(
|
|
@@ -1030,7 +1018,7 @@ function parseSessionAction(
|
|
|
1030
1018
|
}
|
|
1031
1019
|
|
|
1032
1020
|
// ---------------------------------------------------------------------------
|
|
1033
|
-
// 内部分页 /
|
|
1021
|
+
// 内部分页 / 过滤
|
|
1034
1022
|
// ---------------------------------------------------------------------------
|
|
1035
1023
|
|
|
1036
1024
|
function paginateItems<T>(
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
export interface ConsoleRpcExtendedCtx {
|
|
2
|
+
/** full scope 才允许写操作;demo scope 只放行只读 RPC。 */
|
|
3
|
+
fullScope: boolean;
|
|
4
|
+
projectRoot: string;
|
|
5
|
+
/** Plugin Runtime ScheduleHost(basic/cli schedule-host-installer 提供)。 */
|
|
6
|
+
scheduleHost?: unknown;
|
|
7
|
+
/** 在固定 generation lease 内执行一次 Endpoint 管理操作。 */
|
|
8
|
+
withEndpointManagement?: <T>(
|
|
9
|
+
adapter: string,
|
|
10
|
+
endpointKey: string,
|
|
11
|
+
run: (management: EndpointManagementPort) => T | Promise<T>,
|
|
12
|
+
) => Promise<T | null>;
|
|
13
|
+
/** Plugin Runtime DatabaseHost 的 models 视图。 */
|
|
14
|
+
databaseHost?: { models: { get(name: string): unknown } };
|
|
15
|
+
/** Narrow generation-bound persistent-job port; null when Assistant is disabled. */
|
|
16
|
+
resolveScheduleEngine?: () => ConsoleScheduleEngine | null | undefined;
|
|
17
|
+
/** LoginAssist list/submit/cancel(刷新后仍可消费扫码/滑块待办)。 */
|
|
18
|
+
loginAssist?: {
|
|
19
|
+
listPending(): readonly {
|
|
20
|
+
id: string;
|
|
21
|
+
adapter: string;
|
|
22
|
+
endpointKey: string;
|
|
23
|
+
type: string;
|
|
24
|
+
payload: Record<string, unknown>;
|
|
25
|
+
createdAt: number;
|
|
26
|
+
expiresAt?: number;
|
|
27
|
+
}[];
|
|
28
|
+
submit(id: string, value: string | Record<string, unknown>): boolean;
|
|
29
|
+
cancel(id: string, reason?: string): boolean;
|
|
30
|
+
};
|
|
31
|
+
/** Authenticated Root adapter; mutation DTOs deliberately contain no principal/decision fields. */
|
|
32
|
+
workroomProfileControl?: ConsoleWorkroomProfileControlPort;
|
|
33
|
+
/** Authenticated HTTP principal; never accepted from the RPC body. */
|
|
34
|
+
authenticatedPrincipal?: Readonly<{ principalId: string }>;
|
|
35
|
+
workroomKnowledgeControl?: ConsoleWorkroomKnowledgeControlPort;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ConsoleWorkroomProfileControlPort {
|
|
39
|
+
getPlanningStatus(projectId: string,
|
|
40
|
+
authenticatedPrincipal?: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
41
|
+
bootstrapPlanning(command: Readonly<{
|
|
42
|
+
operationId: string;
|
|
43
|
+
projectId: string;
|
|
44
|
+
expectedRegistryRevision: number;
|
|
45
|
+
includeTools?: readonly string[];
|
|
46
|
+
includeSkills?: readonly string[];
|
|
47
|
+
}>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
48
|
+
publishPack(command: Readonly<{ operationId: string; pack: unknown }>,
|
|
49
|
+
authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
50
|
+
publishProfile(command: Readonly<{
|
|
51
|
+
operationId: string;
|
|
52
|
+
projectId: string;
|
|
53
|
+
expectedRegistryRevision: number;
|
|
54
|
+
overlay: unknown;
|
|
55
|
+
activate: boolean;
|
|
56
|
+
}>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
57
|
+
publishRollback(command: Readonly<{
|
|
58
|
+
operationId: string;
|
|
59
|
+
projectId: string;
|
|
60
|
+
expectedRegistryRevision: number;
|
|
61
|
+
restoredFromRevisionId: string;
|
|
62
|
+
overlay: unknown;
|
|
63
|
+
activate: boolean;
|
|
64
|
+
}>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
65
|
+
publishPlanningPolicy(command: Readonly<{
|
|
66
|
+
operationId: string;
|
|
67
|
+
projectId: string;
|
|
68
|
+
profileRevisionId: string;
|
|
69
|
+
revision: number;
|
|
70
|
+
expectedPreviousDigest?: string;
|
|
71
|
+
policy: unknown;
|
|
72
|
+
}>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ConsoleWorkroomKnowledgeControlPort {
|
|
76
|
+
read(projectId: string): Promise<unknown>;
|
|
77
|
+
publish(command: Readonly<{
|
|
78
|
+
operationId: string; projectId: string; expectedRevision: number; entries: readonly unknown[];
|
|
79
|
+
}>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
80
|
+
rollback(command: Readonly<{
|
|
81
|
+
operationId: string; projectId: string; expectedRevision: number; restoreRevision: number;
|
|
82
|
+
}>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ConsoleScheduleEngine {
|
|
86
|
+
addJob(job: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
87
|
+
removeJob(id: string): Promise<boolean>;
|
|
88
|
+
pauseJob(id: string): Promise<boolean>;
|
|
89
|
+
resumeJob(id: string): Promise<boolean>;
|
|
90
|
+
listJobs(): Promise<Record<string, unknown>[]>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type ExtendedRpcResult = { data: unknown } | { error: string };
|
|
94
|
+
|
|
95
|
+
const CRON_NOT_WIRED =
|
|
96
|
+
'持久化调度未接线:当前 Plugin Runtime ScheduleHost 仅支持插件注册的内存任务(list),' +
|
|
97
|
+
'不支持 add/remove/pause/resume(需要 @zhin.js/agent 持久化调度引擎)';
|
|
98
|
+
|
|
99
|
+
const CONSUMED_NOT_WIRED =
|
|
100
|
+
'收件箱已读标记未接线:unified_inbox_request/notice 表未注册或 DatabaseHost 未启动';
|
|
101
|
+
|
|
102
|
+
export interface ScheduleJobRow {
|
|
103
|
+
id: string;
|
|
104
|
+
cron: string;
|
|
105
|
+
description?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface InboxSelection {
|
|
109
|
+
where(query: Record<string, unknown>): InboxSelection;
|
|
110
|
+
/** 可选:DB 侧排序下推(wrapModel 链具备该能力,轻量 fake 可缺失)。 */
|
|
111
|
+
orderBy?(field: string, direction?: 'ASC' | 'DESC'): InboxSelection;
|
|
112
|
+
/** 可选:DB 侧 limit 下推。 */
|
|
113
|
+
limit?(count: number): InboxSelection;
|
|
114
|
+
/** 可选:DB 侧 offset 下推。 */
|
|
115
|
+
offset?(count: number): InboxSelection;
|
|
116
|
+
then<TResult1 = Record<string, unknown>[], TResult2 = never>(
|
|
117
|
+
onfulfilled?: ((value: Record<string, unknown>[]) => TResult1 | PromiseLike<TResult1>) | null,
|
|
118
|
+
onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
|
|
119
|
+
): Promise<TResult1 | TResult2>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface InboxModel {
|
|
123
|
+
select(): InboxSelection;
|
|
124
|
+
update?(patch: Record<string, unknown>): {
|
|
125
|
+
where(query: Record<string, unknown>): Promise<unknown> | unknown;
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface EndpointManagementPort {
|
|
130
|
+
listFriends?(): Promise<readonly { user_id: number | string; nickname: string; remark: string }[]>;
|
|
131
|
+
listGroups?(): Promise<readonly { group_id: number | string; name: string }[]>;
|
|
132
|
+
listChannels?(): Promise<readonly {
|
|
133
|
+
id: string;
|
|
134
|
+
name?: string;
|
|
135
|
+
parent?: { type: string; id: string; name?: string };
|
|
136
|
+
}[]>;
|
|
137
|
+
listGroupMembers?(groupId: string): Promise<readonly unknown[]>;
|
|
138
|
+
listRequests?(): Promise<readonly {
|
|
139
|
+
platform_request_id: string;
|
|
140
|
+
type: string;
|
|
141
|
+
scene_type?: string | null;
|
|
142
|
+
scene_id: string;
|
|
143
|
+
sub_type?: string | null;
|
|
144
|
+
actor_id: string;
|
|
145
|
+
actor_name?: string | null;
|
|
146
|
+
comment?: string | null;
|
|
147
|
+
created_at: number;
|
|
148
|
+
}[]>;
|
|
149
|
+
approveRequest?(requestId: string, remark?: string): Promise<void>;
|
|
150
|
+
rejectRequest?(requestId: string, reason?: string): Promise<void>;
|
|
151
|
+
kickGroupMember?(groupId: string, userId: string): Promise<void>;
|
|
152
|
+
muteGroupMember?(groupId: string, userId: string, durationSeconds: number): Promise<void>;
|
|
153
|
+
setGroupAdmin?(groupId: string, userId: string, enabled: boolean): Promise<void>;
|
|
154
|
+
deleteFriend?(userId: string): Promise<void>;
|
|
155
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { assertDemoConsoleRpcAllowed } from '@zhin.js/console-protocol';
|
|
2
|
+
import type { ConsoleRpcExtendedCtx, ExtendedRpcResult } from './contracts.js';
|
|
3
|
+
import { addCron, listSchedule, mutateCron } from './schedule-rpc.js';
|
|
4
|
+
import {
|
|
5
|
+
actOnRequest, listInbox, listInboxMessages, listPendingRequests, listRecentInboxMessages,
|
|
6
|
+
mapNoticeRow, mapRequestRow, markInboxConsumed, TABLE_NOTICE, TABLE_REQUEST,
|
|
7
|
+
} from './inbox-rpc.js';
|
|
8
|
+
import { cancelLogin, listLoginPending, submitLogin } from './login-rpc.js';
|
|
9
|
+
import {
|
|
10
|
+
deleteFriend, groupWriteOp, listChannels, listFriends, listGroupMembers, listGroups,
|
|
11
|
+
} from './endpoint-rpc.js';
|
|
12
|
+
import { mutateWorkroomKnowledge, mutateWorkroomProfile } from './workroom-rpc.js';
|
|
13
|
+
|
|
14
|
+
export async function dispatchExtendedConsoleRpc(
|
|
15
|
+
type: string,
|
|
16
|
+
data: Record<string, unknown> | undefined,
|
|
17
|
+
ctx: ConsoleRpcExtendedCtx,
|
|
18
|
+
): Promise<ExtendedRpcResult | undefined> {
|
|
19
|
+
const d = data ?? {};
|
|
20
|
+
|
|
21
|
+
if (!ctx.fullScope) {
|
|
22
|
+
const denied = assertDemoConsoleRpcAllowed(type);
|
|
23
|
+
if (denied) return { error: denied };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
switch (type) {
|
|
27
|
+
case 'schedule:list':
|
|
28
|
+
case 'cron:list':
|
|
29
|
+
return listSchedule(ctx);
|
|
30
|
+
|
|
31
|
+
case 'cron:add':
|
|
32
|
+
return addCron(d, ctx);
|
|
33
|
+
case 'cron:remove':
|
|
34
|
+
case 'cron:pause':
|
|
35
|
+
case 'cron:resume':
|
|
36
|
+
return mutateCron(type, d, ctx);
|
|
37
|
+
|
|
38
|
+
case 'request.list':
|
|
39
|
+
return listPendingRequests(d, ctx);
|
|
40
|
+
case 'inbox.requests':
|
|
41
|
+
return listInbox(d, ctx, TABLE_REQUEST, 'requests', mapRequestRow);
|
|
42
|
+
case 'inbox.notices':
|
|
43
|
+
return listInbox(d, ctx, TABLE_NOTICE, 'notices', mapNoticeRow);
|
|
44
|
+
case 'inbox.messages':
|
|
45
|
+
return listInboxMessages(d, ctx);
|
|
46
|
+
case 'inbox.recent':
|
|
47
|
+
return listRecentInboxMessages(d, ctx);
|
|
48
|
+
|
|
49
|
+
case 'request.approve':
|
|
50
|
+
case 'request.reject':
|
|
51
|
+
return actOnRequest(type, d, ctx);
|
|
52
|
+
|
|
53
|
+
case 'login.list':
|
|
54
|
+
return listLoginPending(ctx);
|
|
55
|
+
case 'login.submit':
|
|
56
|
+
return submitLogin(d, ctx);
|
|
57
|
+
case 'login.cancel':
|
|
58
|
+
return cancelLogin(d, ctx);
|
|
59
|
+
|
|
60
|
+
case 'request.consumed':
|
|
61
|
+
return markInboxConsumed(d, ctx, TABLE_REQUEST);
|
|
62
|
+
case 'notice.consumed':
|
|
63
|
+
return markInboxConsumed(d, ctx, TABLE_NOTICE);
|
|
64
|
+
|
|
65
|
+
case 'endpoint.friends':
|
|
66
|
+
return listFriends(d, ctx);
|
|
67
|
+
case 'endpoint.groups':
|
|
68
|
+
return listGroups(d, ctx);
|
|
69
|
+
case 'endpoint.channels':
|
|
70
|
+
return listChannels(d, ctx);
|
|
71
|
+
case 'endpoint.group_members':
|
|
72
|
+
return listGroupMembers(d, ctx);
|
|
73
|
+
|
|
74
|
+
case 'endpoint.group_kick':
|
|
75
|
+
return groupWriteOp(d, ctx, {
|
|
76
|
+
method: 'kickGroupMember',
|
|
77
|
+
buildArgs: (gid, uid) => [gid, uid],
|
|
78
|
+
requireUser: true,
|
|
79
|
+
unsupported: '踢出群成员',
|
|
80
|
+
});
|
|
81
|
+
case 'endpoint.group_mute':
|
|
82
|
+
return groupWriteOp(d, ctx, {
|
|
83
|
+
method: 'muteGroupMember',
|
|
84
|
+
buildArgs: (gid, uid, extra) => [gid, uid, extra.duration ?? 600],
|
|
85
|
+
requireUser: true,
|
|
86
|
+
unsupported: '禁言群成员',
|
|
87
|
+
});
|
|
88
|
+
case 'endpoint.group_admin':
|
|
89
|
+
return groupWriteOp(d, ctx, {
|
|
90
|
+
method: 'setGroupAdmin',
|
|
91
|
+
buildArgs: (gid, uid, extra) => [gid, uid, extra.enable !== false],
|
|
92
|
+
requireUser: true,
|
|
93
|
+
unsupported: '设置群管理员',
|
|
94
|
+
});
|
|
95
|
+
case 'endpoint.delete_friend':
|
|
96
|
+
return deleteFriend(d, ctx);
|
|
97
|
+
|
|
98
|
+
case 'workroom.profile.status':
|
|
99
|
+
case 'workroom.profile.bootstrap':
|
|
100
|
+
case 'workroom.profile.pack.publish':
|
|
101
|
+
case 'workroom.profile.publish':
|
|
102
|
+
case 'workroom.profile.rollback':
|
|
103
|
+
case 'workroom.profile.policy.publish':
|
|
104
|
+
return mutateWorkroomProfile(type, d, ctx);
|
|
105
|
+
|
|
106
|
+
case 'workroom.knowledge.get':
|
|
107
|
+
case 'workroom.knowledge.publish':
|
|
108
|
+
case 'workroom.knowledge.rollback':
|
|
109
|
+
return mutateWorkroomKnowledge(type, d, ctx);
|
|
110
|
+
|
|
111
|
+
default:
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { ConsoleRpcExtendedCtx, EndpointManagementPort, ExtendedRpcResult } from './contracts.js';
|
|
2
|
+
import { boolField, numField, strField, withLiveEndpoint } from './rpc-values.js';
|
|
3
|
+
|
|
4
|
+
export async function listFriends(
|
|
5
|
+
d: Record<string, unknown>,
|
|
6
|
+
ctx: ConsoleRpcExtendedCtx,
|
|
7
|
+
): Promise<ExtendedRpcResult> {
|
|
8
|
+
const endpoint = requireEndpointAddress(d);
|
|
9
|
+
if ('error' in endpoint) return endpoint;
|
|
10
|
+
return withLiveEndpoint(ctx, endpoint.adapter, endpoint.endpointKey, async (management) => {
|
|
11
|
+
if (management.listFriends) {
|
|
12
|
+
const friends = [...await management.listFriends()];
|
|
13
|
+
return { data: { friends, count: friends.length } };
|
|
14
|
+
}
|
|
15
|
+
return { error: `当前适配器(${endpoint.adapter})不支持好友列表` };
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function listGroups(
|
|
20
|
+
d: Record<string, unknown>,
|
|
21
|
+
ctx: ConsoleRpcExtendedCtx,
|
|
22
|
+
): Promise<ExtendedRpcResult> {
|
|
23
|
+
const endpoint = requireEndpointAddress(d);
|
|
24
|
+
if ('error' in endpoint) return endpoint;
|
|
25
|
+
return withLiveEndpoint(ctx, endpoint.adapter, endpoint.endpointKey, async (management) => {
|
|
26
|
+
if (management.listGroups) {
|
|
27
|
+
const groups = [...await management.listGroups()];
|
|
28
|
+
return { data: { groups, count: groups.length } };
|
|
29
|
+
}
|
|
30
|
+
return { error: `当前适配器(${endpoint.adapter})不支持群列表` };
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function listChannels(
|
|
35
|
+
d: Record<string, unknown>,
|
|
36
|
+
ctx: ConsoleRpcExtendedCtx,
|
|
37
|
+
): Promise<ExtendedRpcResult> {
|
|
38
|
+
const endpoint = requireEndpointAddress(d);
|
|
39
|
+
if ('error' in endpoint) return endpoint;
|
|
40
|
+
return withLiveEndpoint(ctx, endpoint.adapter, endpoint.endpointKey, async (management) => {
|
|
41
|
+
if (management.listChannels) {
|
|
42
|
+
const channels = [...await management.listChannels()];
|
|
43
|
+
return { data: { channels, count: channels.length } };
|
|
44
|
+
}
|
|
45
|
+
return { error: `当前适配器(${endpoint.adapter})不支持频道列表` };
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function listGroupMembers(
|
|
50
|
+
d: Record<string, unknown>,
|
|
51
|
+
ctx: ConsoleRpcExtendedCtx,
|
|
52
|
+
): Promise<ExtendedRpcResult> {
|
|
53
|
+
const adapter = strField(d, 'adapter');
|
|
54
|
+
const endpointKey = strField(d, 'endpointKey');
|
|
55
|
+
const groupId = strField(d, 'groupId');
|
|
56
|
+
if (!adapter || !endpointKey || !groupId) {
|
|
57
|
+
return { error: 'adapter, endpointKey, and groupId are required' };
|
|
58
|
+
}
|
|
59
|
+
return withLiveEndpoint(ctx, adapter, endpointKey, async (management) => {
|
|
60
|
+
const method = management.listGroupMembers;
|
|
61
|
+
if (!method) {
|
|
62
|
+
return { error: `当前适配器(${adapter})不支持群成员列表` };
|
|
63
|
+
}
|
|
64
|
+
const members = [...await method(groupId)];
|
|
65
|
+
return { data: { members, count: members.length } };
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------- 群管/好友写操作
|
|
70
|
+
|
|
71
|
+
interface GroupWriteSpec {
|
|
72
|
+
method: 'kickGroupMember' | 'muteGroupMember' | 'setGroupAdmin';
|
|
73
|
+
buildArgs(groupId: string, userId: string, extra: { duration?: number; enable?: boolean }): unknown[];
|
|
74
|
+
requireUser: boolean;
|
|
75
|
+
/** 中文操作名,用于"该平台不支持 xxx"。 */
|
|
76
|
+
unsupported: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function groupWriteOp(
|
|
80
|
+
d: Record<string, unknown>,
|
|
81
|
+
ctx: ConsoleRpcExtendedCtx,
|
|
82
|
+
spec: GroupWriteSpec,
|
|
83
|
+
): Promise<ExtendedRpcResult> {
|
|
84
|
+
const adapter = strField(d, 'adapter');
|
|
85
|
+
const endpointKey = strField(d, 'endpointKey');
|
|
86
|
+
const groupId = strField(d, 'groupId');
|
|
87
|
+
const userId = strField(d, 'userId');
|
|
88
|
+
if (!adapter || !endpointKey || !groupId) {
|
|
89
|
+
return { error: 'adapter, endpointKey, and groupId are required' };
|
|
90
|
+
}
|
|
91
|
+
if (spec.requireUser && !userId) {
|
|
92
|
+
return { error: 'userId is required' };
|
|
93
|
+
}
|
|
94
|
+
return withLiveEndpoint(ctx, adapter, endpointKey, async (management) => {
|
|
95
|
+
const method = management[spec.method];
|
|
96
|
+
if (!method) {
|
|
97
|
+
return { error: `当前适配器(${adapter})不支持${spec.unsupported}` };
|
|
98
|
+
}
|
|
99
|
+
const durationRaw = d.duration;
|
|
100
|
+
const enableRaw = d.enable;
|
|
101
|
+
const args = spec.buildArgs(groupId, userId, {
|
|
102
|
+
duration: typeof durationRaw === 'number' && Number.isFinite(durationRaw)
|
|
103
|
+
? durationRaw
|
|
104
|
+
: undefined,
|
|
105
|
+
enable: typeof enableRaw === 'boolean' ? enableRaw : undefined,
|
|
106
|
+
});
|
|
107
|
+
await (method as (...args: unknown[]) => Promise<void>).call(management, ...args);
|
|
108
|
+
return { data: { success: true } };
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function deleteFriend(
|
|
113
|
+
d: Record<string, unknown>,
|
|
114
|
+
ctx: ConsoleRpcExtendedCtx,
|
|
115
|
+
): Promise<ExtendedRpcResult> {
|
|
116
|
+
const adapter = strField(d, 'adapter');
|
|
117
|
+
const endpointKey = strField(d, 'endpointKey');
|
|
118
|
+
const userId = strField(d, 'userId');
|
|
119
|
+
if (!adapter || !endpointKey || !userId) {
|
|
120
|
+
return { error: 'adapter, endpointKey, and userId are required' };
|
|
121
|
+
}
|
|
122
|
+
return withLiveEndpoint(ctx, adapter, endpointKey, async (management) => {
|
|
123
|
+
const method = management.deleteFriend;
|
|
124
|
+
if (!method) {
|
|
125
|
+
return { error: '当前适配器暂不支持删除好友' };
|
|
126
|
+
}
|
|
127
|
+
await method(userId);
|
|
128
|
+
return { data: { success: true } };
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------- helpers
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
function requireEndpointAddress(
|
|
136
|
+
d: Record<string, unknown>,
|
|
137
|
+
): { adapter: string; endpointKey: string } | { error: string } {
|
|
138
|
+
const adapter = strField(d, 'adapter');
|
|
139
|
+
const endpointKey = strField(d, 'endpointKey');
|
|
140
|
+
if (!adapter || !endpointKey) return { error: 'adapter and endpointKey are required' };
|
|
141
|
+
return { adapter, endpointKey };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function normalizeParent(raw: unknown): { type: 'group' | 'guild'; id: string; name?: string } | undefined {
|
|
145
|
+
if (!raw || typeof raw !== 'object') return undefined;
|
|
146
|
+
const parent = raw as { type?: unknown; id?: unknown; name?: unknown };
|
|
147
|
+
const id = typeof parent.id === 'string' && parent.id.trim() ? parent.id.trim() : undefined;
|
|
148
|
+
if (!id) return undefined;
|
|
149
|
+
let type: 'group' | 'guild' | undefined;
|
|
150
|
+
if (parent.type === 'group' || parent.type === 'guild') type = parent.type;
|
|
151
|
+
else if (parent.type === 'channel') type = 'guild';
|
|
152
|
+
if (!type) return undefined;
|
|
153
|
+
const name = typeof parent.name === 'string' && parent.name.trim() ? parent.name.trim() : undefined;
|
|
154
|
+
return name ? { type, id, name } : { type, id };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function channelFromStoredRow(row: Record<string, unknown>): Record<string, unknown> {
|
|
158
|
+
const parent = parentFromStoredRow(row);
|
|
159
|
+
return {
|
|
160
|
+
id: String(row.channel_id ?? ''),
|
|
161
|
+
type: String(row.channel_type ?? ''),
|
|
162
|
+
...(row.channel_name != null ? { name: String(row.channel_name) } : {}),
|
|
163
|
+
...(parent ? { parent } : {}),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function parentFromStoredRow(
|
|
168
|
+
row: Record<string, unknown>,
|
|
169
|
+
): { type: 'group' | 'guild'; id: string; name?: string } | undefined {
|
|
170
|
+
return normalizeParent({
|
|
171
|
+
type: row.channel_parent_type,
|
|
172
|
+
id: row.channel_parent_id,
|
|
173
|
+
});
|
|
174
|
+
}
|