@zhin.js/host-http 1.1.0 → 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.
Files changed (46) hide show
  1. package/README.md +5 -0
  2. package/lib/console-plugin-rpc.d.ts +3 -0
  3. package/lib/console-plugin-rpc.js +130 -0
  4. package/lib/console-rest-pages.d.ts +1 -1
  5. package/lib/console-rest-pages.js +11 -20
  6. package/lib/{console-rpc-extended.d.ts → console-rpc-extended/contracts.d.ts} +22 -21
  7. package/lib/console-rpc-extended/contracts.js +4 -0
  8. package/lib/console-rpc-extended/dispatcher.d.ts +2 -0
  9. package/lib/console-rpc-extended/dispatcher.js +92 -0
  10. package/lib/console-rpc-extended/endpoint-rpc.d.ts +24 -0
  11. package/lib/console-rpc-extended/endpoint-rpc.js +137 -0
  12. package/lib/console-rpc-extended/inbox-rpc.d.ts +23 -0
  13. package/lib/console-rpc-extended/inbox-rpc.js +292 -0
  14. package/lib/console-rpc-extended/index.d.ts +2 -0
  15. package/lib/console-rpc-extended/index.js +1 -0
  16. package/lib/console-rpc-extended/login-rpc.d.ts +4 -0
  17. package/lib/console-rpc-extended/login-rpc.js +46 -0
  18. package/lib/console-rpc-extended/rpc-values.d.ts +16 -0
  19. package/lib/console-rpc-extended/rpc-values.js +87 -0
  20. package/lib/console-rpc-extended/schedule-rpc.d.ts +6 -0
  21. package/lib/console-rpc-extended/schedule-rpc.js +109 -0
  22. package/lib/console-rpc-extended/workroom-rpc.d.ts +3 -0
  23. package/lib/console-rpc-extended/workroom-rpc.js +104 -0
  24. package/lib/console-rpc.d.ts +71 -15
  25. package/lib/console-rpc.js +116 -82
  26. package/lib/index.d.ts +3 -2
  27. package/lib/index.js +2 -1
  28. package/lib/marketplace-registry.d.ts +12 -0
  29. package/lib/marketplace-registry.js +45 -0
  30. package/package.json +4 -4
  31. package/src/console-plugin-rpc.ts +142 -0
  32. package/src/console-rest-pages.ts +14 -26
  33. package/src/console-rpc-extended/contracts.ts +155 -0
  34. package/src/console-rpc-extended/dispatcher.ts +114 -0
  35. package/src/console-rpc-extended/endpoint-rpc.ts +174 -0
  36. package/src/console-rpc-extended/inbox-rpc.ts +342 -0
  37. package/src/console-rpc-extended/index.ts +8 -0
  38. package/src/console-rpc-extended/login-rpc.ts +50 -0
  39. package/src/console-rpc-extended/rpc-values.ts +104 -0
  40. package/src/console-rpc-extended/schedule-rpc.ts +117 -0
  41. package/src/console-rpc-extended/workroom-rpc.ts +116 -0
  42. package/src/console-rpc.ts +189 -97
  43. package/src/index.ts +7 -2
  44. package/src/marketplace-registry.ts +58 -0
  45. package/lib/console-rpc-extended.js +0 -916
  46. 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
- * 响应形状对齐 legacy `packages/host/api/src/rest/`:
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
- /** 内省数据门面 — 由 basic/cli 用 agent 包 collectIntrospection* 装配。 */
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 fetchPluginRegistry = async (): Promise<unknown[]> => {
346
- if (pluginsCache && Date.now() - pluginsCache.ts < PLUGINS_CACHE_TTL) {
347
- return pluginsCache.data;
348
- }
349
- const resp = await fetchFn(pluginRegistryUrl, {
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 fetchPluginRegistry()) as Array<Record<string, unknown>>;
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 registry = (await fetchPluginRegistry()) as Array<{
420
+ const registryPlugins = (await registry.list()) as readonly {
433
421
  name?: string;
434
422
  downloads?: unknown;
435
- }>;
436
- const cached = registry.find((p) => p.name === pkgName);
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
- // introspection(legacy introspection-rest-api.ts;分页形状对齐 IntrospectionJsonResponse)
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
- // 内部分页 / 过滤(语义对齐 agent 包 introspection-pagination.ts)
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
+ }