@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.
- package/README.md +5 -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 +4 -4
- 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
package/README.md
CHANGED
|
@@ -54,3 +54,8 @@ http:
|
|
|
54
54
|
The legacy Koa `@zhin.js/host-router` stack and the `@zhin.js/host-api` management-plane plugin have
|
|
55
55
|
been removed; the management-plane REST/RPC/SSE is now assembled by `@zhin.js/cli` (Console Host) on
|
|
56
56
|
top of this package — no Host plugins need to be installed or enabled.
|
|
57
|
+
|
|
58
|
+
Extended Console RPC uses `src/console-rpc-extended/index.ts` as its module boundary. The dispatcher
|
|
59
|
+
only maps protocol method names to schedule, inbox, login, Endpoint-management, and Workroom control
|
|
60
|
+
handlers. Shared request parsing and generation-leased Endpoint execution live behind internal helper
|
|
61
|
+
ports, so adding one RPC domain does not enlarge the central dispatcher or couple unrelated domains.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { RuntimeConsoleRpcContext, RuntimeConsoleRpcMessage, RuntimeConsoleRpcReply } from './console-rpc.js';
|
|
2
|
+
/** Dispatches the plugin-management slice; undefined means the type belongs elsewhere. */
|
|
3
|
+
export declare function dispatchPluginConsoleRpc(type: string, message: RuntimeConsoleRpcMessage, ctx: RuntimeConsoleRpcContext): Promise<RuntimeConsoleRpcReply | undefined>;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { PLUGIN_RPC } from '@zhin.js/console-protocol';
|
|
2
|
+
/** Dispatches the plugin-management slice; undefined means the type belongs elsewhere. */
|
|
3
|
+
export async function dispatchPluginConsoleRpc(type, message, ctx) {
|
|
4
|
+
const requestId = message.requestId;
|
|
5
|
+
try {
|
|
6
|
+
switch (type) {
|
|
7
|
+
case PLUGIN_RPC.SET_ENABLED: {
|
|
8
|
+
const instanceKey = stringField(message, 'instanceKey');
|
|
9
|
+
const enabled = message.enabled;
|
|
10
|
+
if (!instanceKey || typeof enabled !== 'boolean') {
|
|
11
|
+
return { requestId, error: 'instanceKey and boolean enabled are required' };
|
|
12
|
+
}
|
|
13
|
+
if (!ctx.setPluginEnabled) {
|
|
14
|
+
return { requestId, error: 'Plugin lifecycle management is not configured' };
|
|
15
|
+
}
|
|
16
|
+
const lifecycle = await ctx.setPluginEnabled(instanceKey, enabled);
|
|
17
|
+
ctx.publishEvent?.('plugin:lifecycle-updated', { instanceKey, enabled });
|
|
18
|
+
if (ctx.requestRestart) {
|
|
19
|
+
setTimeout(() => void Promise.resolve(ctx.requestRestart?.()).catch(() => undefined), 500);
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
requestId,
|
|
23
|
+
data: {
|
|
24
|
+
success: true,
|
|
25
|
+
instanceKey,
|
|
26
|
+
enabled,
|
|
27
|
+
disabled: lifecycle.disabled,
|
|
28
|
+
restartRequired: true,
|
|
29
|
+
message: `${instanceKey} 已${enabled ? '启用' : '停用'},Host 正在重启`,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
case PLUGIN_RPC.PLAN_INSTALL: {
|
|
34
|
+
const packageName = stringField(message, 'packageName');
|
|
35
|
+
if (!packageName)
|
|
36
|
+
return { requestId, error: 'packageName is required' };
|
|
37
|
+
if (!ctx.pluginManagement)
|
|
38
|
+
return { requestId, error: 'Plugin management is not configured' };
|
|
39
|
+
return { requestId, data: await ctx.pluginManagement.planInstall(packageName) };
|
|
40
|
+
}
|
|
41
|
+
case PLUGIN_RPC.INSTALL: {
|
|
42
|
+
const packageName = stringField(message, 'packageName');
|
|
43
|
+
if (!packageName)
|
|
44
|
+
return { requestId, error: 'packageName is required' };
|
|
45
|
+
if (!ctx.pluginManagement?.install) {
|
|
46
|
+
return { requestId, error: 'Plugin installation is not configured' };
|
|
47
|
+
}
|
|
48
|
+
const result = await ctx.pluginManagement.install(packageName, optionalString(message.expectedRevision));
|
|
49
|
+
ctx.publishEvent?.('plugin:installed', { packageName, restartRequired: result.restartRequired });
|
|
50
|
+
return { requestId, data: { success: true, ...result } };
|
|
51
|
+
}
|
|
52
|
+
case PLUGIN_RPC.PLAN_UNINSTALL: {
|
|
53
|
+
const packageName = stringField(message, 'packageName');
|
|
54
|
+
if (!packageName)
|
|
55
|
+
return { requestId, error: 'packageName is required' };
|
|
56
|
+
if (!ctx.pluginManagement?.planUninstall) {
|
|
57
|
+
return { requestId, error: 'Plugin uninstall planning is not configured' };
|
|
58
|
+
}
|
|
59
|
+
return { requestId, data: await ctx.pluginManagement.planUninstall(packageName) };
|
|
60
|
+
}
|
|
61
|
+
case PLUGIN_RPC.UNINSTALL: {
|
|
62
|
+
const packageName = stringField(message, 'packageName');
|
|
63
|
+
if (!packageName || stringField(message, 'confirmation') !== packageName) {
|
|
64
|
+
return { requestId, error: 'confirmation must exactly match packageName' };
|
|
65
|
+
}
|
|
66
|
+
if (!ctx.pluginManagement?.uninstall) {
|
|
67
|
+
return { requestId, error: 'Plugin uninstall is not configured' };
|
|
68
|
+
}
|
|
69
|
+
const result = await ctx.pluginManagement.uninstall(packageName, optionalString(message.expectedRevision));
|
|
70
|
+
ctx.publishEvent?.('plugin:uninstalled', { packageName, restartRequired: result.restartRequired });
|
|
71
|
+
return { requestId, data: { success: true, ...result } };
|
|
72
|
+
}
|
|
73
|
+
case PLUGIN_RPC.PLAN_UPDATE: {
|
|
74
|
+
const packageName = stringField(message, 'packageName');
|
|
75
|
+
const targetVersion = stringField(message, 'targetVersion');
|
|
76
|
+
if (!packageName || !targetVersion) {
|
|
77
|
+
return { requestId, error: 'packageName and targetVersion are required' };
|
|
78
|
+
}
|
|
79
|
+
if (!ctx.pluginManagement?.planUpdate) {
|
|
80
|
+
return { requestId, error: 'Plugin update planning is not configured' };
|
|
81
|
+
}
|
|
82
|
+
return { requestId, data: await ctx.pluginManagement.planUpdate(packageName, targetVersion) };
|
|
83
|
+
}
|
|
84
|
+
case PLUGIN_RPC.UPDATE: {
|
|
85
|
+
const packageName = stringField(message, 'packageName');
|
|
86
|
+
const targetVersion = stringField(message, 'targetVersion');
|
|
87
|
+
if (!packageName || !targetVersion) {
|
|
88
|
+
return { requestId, error: 'packageName and targetVersion are required' };
|
|
89
|
+
}
|
|
90
|
+
if (!ctx.pluginManagement?.update)
|
|
91
|
+
return { requestId, error: 'Plugin update is not configured' };
|
|
92
|
+
const result = await ctx.pluginManagement.update(packageName, targetVersion, optionalString(message.expectedRevision));
|
|
93
|
+
ctx.publishEvent?.('plugin:updated', {
|
|
94
|
+
packageName,
|
|
95
|
+
version: result.installedVersion,
|
|
96
|
+
restartRequired: result.restartRequired,
|
|
97
|
+
});
|
|
98
|
+
return { requestId, data: { success: true, ...result } };
|
|
99
|
+
}
|
|
100
|
+
case PLUGIN_RPC.VALIDATE_CONFIG: {
|
|
101
|
+
const pluginName = stringField(message, 'pluginName');
|
|
102
|
+
if (!pluginName)
|
|
103
|
+
return { requestId, error: 'pluginName is required' };
|
|
104
|
+
if (!ctx.validatePluginConfig)
|
|
105
|
+
return { requestId, error: 'Config validation is not configured' };
|
|
106
|
+
return { requestId, data: await ctx.validatePluginConfig(pluginName, message.data) };
|
|
107
|
+
}
|
|
108
|
+
case PLUGIN_RPC.DIAGNOSE: {
|
|
109
|
+
const pluginName = stringField(message, 'pluginName');
|
|
110
|
+
if (!pluginName)
|
|
111
|
+
return { requestId, error: 'pluginName is required' };
|
|
112
|
+
if (!ctx.diagnosePlugin)
|
|
113
|
+
return { requestId, error: 'Plugin diagnostics are not configured' };
|
|
114
|
+
return { requestId, data: await ctx.diagnosePlugin(pluginName) };
|
|
115
|
+
}
|
|
116
|
+
default:
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
return { requestId, error: error instanceof Error ? error.message : String(error) };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function stringField(message, key) {
|
|
125
|
+
const value = message[key];
|
|
126
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
127
|
+
}
|
|
128
|
+
function optionalString(value) {
|
|
129
|
+
return typeof value === 'string' ? value : undefined;
|
|
130
|
+
}
|
|
@@ -29,7 +29,7 @@ export interface ConsoleRestCtx {
|
|
|
29
29
|
/** Distinguishes a real IM conversation with no Agent turn from a typo key. */
|
|
30
30
|
readonly isKnownConversationSession?: (sessionKey: string) => boolean | undefined | Promise<boolean | undefined>;
|
|
31
31
|
}
|
|
32
|
-
/** 内省数据门面 — 由
|
|
32
|
+
/** 内省数据门面 — 由 composition root 从当前 Runtime snapshot 装配。 */
|
|
33
33
|
export interface ConsoleAgentIntrospection {
|
|
34
34
|
commands?(): readonly unknown[];
|
|
35
35
|
middlewares?(): readonly unknown[];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { HttpBodyError, readJsonBody } from './json-body.js';
|
|
4
|
+
import { MarketplaceRegistry } from './marketplace-registry.js';
|
|
4
5
|
const INTROSPECTION_PAGE_SIZES = {
|
|
5
6
|
commands: 25,
|
|
6
7
|
middlewares: 30,
|
|
@@ -11,7 +12,6 @@ const INTROSPECTION_PAGE_SIZES = {
|
|
|
11
12
|
bindings: 30,
|
|
12
13
|
mcp: 30,
|
|
13
14
|
};
|
|
14
|
-
let pluginsCache = null;
|
|
15
15
|
const PLUGINS_CACHE_TTL = 5 * 60 * 1000;
|
|
16
16
|
/**
|
|
17
17
|
* 注册 Console 页面 REST 路由。返回注销函数(注销全部已注册路由)。
|
|
@@ -175,20 +175,11 @@ function registerLogsRoutes(route, base, ctx) {
|
|
|
175
175
|
// marketplace(legacy marketplace-rest-api.ts;plugins.json + npmmirror)
|
|
176
176
|
// ---------------------------------------------------------------------------
|
|
177
177
|
function registerMarketplaceRoutes(route, base, ctx, fetchFn, pluginRegistryUrl, npmRegistryUrl) {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
signal: AbortSignal.timeout(30_000),
|
|
184
|
-
});
|
|
185
|
-
if (!resp.ok)
|
|
186
|
-
throw new Error(`plugins.json fetch failed: ${resp.status}`);
|
|
187
|
-
const json = (await resp.json());
|
|
188
|
-
const list = json.plugins || [];
|
|
189
|
-
pluginsCache = { data: list, ts: Date.now() };
|
|
190
|
-
return list;
|
|
191
|
-
};
|
|
178
|
+
const registry = new MarketplaceRegistry({
|
|
179
|
+
fetchFn,
|
|
180
|
+
url: pluginRegistryUrl,
|
|
181
|
+
ttlMs: PLUGINS_CACHE_TTL,
|
|
182
|
+
});
|
|
192
183
|
route('GET', '/pub/marketplace/search', async (_request, response, url) => {
|
|
193
184
|
const q = url.searchParams.get('q') ?? url.searchParams.get('keyword') ?? '';
|
|
194
185
|
const pageNum = Math.max(1, parseInt(url.searchParams.get('page') ?? '1', 10) || 1);
|
|
@@ -197,7 +188,7 @@ function registerMarketplaceRoutes(route, base, ctx, fetchFn, pluginRegistryUrl,
|
|
|
197
188
|
const official = url.searchParams.get('official') ?? undefined;
|
|
198
189
|
const searchKeyword = q.trim().toLowerCase();
|
|
199
190
|
try {
|
|
200
|
-
const allPlugins = (await
|
|
191
|
+
const allPlugins = (await registry.list());
|
|
201
192
|
let plugins = allPlugins.map((p) => ({
|
|
202
193
|
name: p.name,
|
|
203
194
|
displayName: p.displayName || '',
|
|
@@ -259,8 +250,8 @@ function registerMarketplaceRoutes(route, base, ctx, fetchFn, pluginRegistryUrl,
|
|
|
259
250
|
try {
|
|
260
251
|
let cachedDownloads = { weekly: 0, monthly: 0 };
|
|
261
252
|
try {
|
|
262
|
-
const
|
|
263
|
-
const cached =
|
|
253
|
+
const registryPlugins = (await registry.list());
|
|
254
|
+
const cached = registryPlugins.find((p) => p.name === pkgName);
|
|
264
255
|
if (cached?.downloads && typeof cached.downloads === 'object') {
|
|
265
256
|
cachedDownloads = cached.downloads;
|
|
266
257
|
}
|
|
@@ -379,7 +370,7 @@ async function listInstalledPluginPackages(projectRoot) {
|
|
|
379
370
|
}));
|
|
380
371
|
}
|
|
381
372
|
// ---------------------------------------------------------------------------
|
|
382
|
-
//
|
|
373
|
+
// Runtime snapshot introspection
|
|
383
374
|
// ---------------------------------------------------------------------------
|
|
384
375
|
function registerIntrospectionRoutes(route, base, ctx, options) {
|
|
385
376
|
const collectors = [
|
|
@@ -822,7 +813,7 @@ function parseSessionAction(pathname, prefix, action) {
|
|
|
822
813
|
return { sessionKey: decodePathParam(rawKey) };
|
|
823
814
|
}
|
|
824
815
|
// ---------------------------------------------------------------------------
|
|
825
|
-
// 内部分页 /
|
|
816
|
+
// 内部分页 / 过滤
|
|
826
817
|
// ---------------------------------------------------------------------------
|
|
827
818
|
function paginateItems(all, page, pageSize) {
|
|
828
819
|
const total = all.length;
|
|
@@ -1,21 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Extended Console RPC: cron/schedule + endpoint social/inbox surface.
|
|
3
|
-
*
|
|
4
|
-
* 挂在 `dispatchRuntimeConsoleRpc`(console-rpc.ts)之后的第二级 dispatch:
|
|
5
|
-
* 返回 `undefined` 表示不识别该 type,由调用方继续走默认分支;
|
|
6
|
-
* 识别后返回 `{ data }` 或 `{ error }`(requestId 由调用方补齐)。
|
|
7
|
-
*
|
|
8
|
-
* 数据源约定(只读真实数据源,不做内存假数据):
|
|
9
|
-
* - cron:`ctx.scheduleHost`(Plugin Runtime ScheduleHost,仅 register/list,
|
|
10
|
-
* 无持久化引擎,故 cron:add/remove/pause/resume 报"未接线")。
|
|
11
|
-
* - request.list:优先 `management.listRequests()`(live endpoint);否则回退
|
|
12
|
-
* unified_inbox_request。审批始终走 approveRequest/rejectRequest。
|
|
13
|
-
* - inbox(历史):`ctx.databaseHost.models`(unified_inbox_message/request/notice,
|
|
14
|
-
* 表不存在或查询失败 → 空数组 + inboxEnabled:false;requestConsumed/noticeConsumed
|
|
15
|
-
* 按行 id update consumed=1,表/model 缺失时报"未接线")。
|
|
16
|
-
* - 社交/群管:`ctx.withEndpointManagement` 在固定 generation lease 内
|
|
17
|
-
* 消费 `management` 语义端口;平台 SDK 字段归一化由 Adapter 负责。
|
|
18
|
-
*/
|
|
19
1
|
export interface ConsoleRpcExtendedCtx {
|
|
20
2
|
/** full scope 才允许写操作;demo scope 只放行只读 RPC。 */
|
|
21
3
|
fullScope: boolean;
|
|
@@ -134,7 +116,28 @@ export type ExtendedRpcResult = {
|
|
|
134
116
|
} | {
|
|
135
117
|
error: string;
|
|
136
118
|
};
|
|
137
|
-
interface
|
|
119
|
+
export interface ScheduleJobRow {
|
|
120
|
+
id: string;
|
|
121
|
+
cron: string;
|
|
122
|
+
description?: string;
|
|
123
|
+
}
|
|
124
|
+
export interface InboxSelection {
|
|
125
|
+
where(query: Record<string, unknown>): InboxSelection;
|
|
126
|
+
/** 可选:DB 侧排序下推(wrapModel 链具备该能力,轻量 fake 可缺失)。 */
|
|
127
|
+
orderBy?(field: string, direction?: 'ASC' | 'DESC'): InboxSelection;
|
|
128
|
+
/** 可选:DB 侧 limit 下推。 */
|
|
129
|
+
limit?(count: number): InboxSelection;
|
|
130
|
+
/** 可选:DB 侧 offset 下推。 */
|
|
131
|
+
offset?(count: number): InboxSelection;
|
|
132
|
+
then<TResult1 = Record<string, unknown>[], TResult2 = never>(onfulfilled?: ((value: Record<string, unknown>[]) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
|
|
133
|
+
}
|
|
134
|
+
export interface InboxModel {
|
|
135
|
+
select(): InboxSelection;
|
|
136
|
+
update?(patch: Record<string, unknown>): {
|
|
137
|
+
where(query: Record<string, unknown>): Promise<unknown> | unknown;
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
export interface EndpointManagementPort {
|
|
138
141
|
listFriends?(): Promise<readonly {
|
|
139
142
|
user_id: number | string;
|
|
140
143
|
nickname: string;
|
|
@@ -172,5 +175,3 @@ interface EndpointManagementPort {
|
|
|
172
175
|
setGroupAdmin?(groupId: string, userId: string, enabled: boolean): Promise<void>;
|
|
173
176
|
deleteFriend?(userId: string): Promise<void>;
|
|
174
177
|
}
|
|
175
|
-
export declare function dispatchExtendedConsoleRpc(type: string, data: Record<string, unknown> | undefined, ctx: ConsoleRpcExtendedCtx): Promise<ExtendedRpcResult | undefined>;
|
|
176
|
-
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { assertDemoConsoleRpcAllowed } from '@zhin.js/console-protocol';
|
|
2
|
+
import { addCron, listSchedule, mutateCron } from './schedule-rpc.js';
|
|
3
|
+
import { actOnRequest, listInbox, listInboxMessages, listPendingRequests, listRecentInboxMessages, mapNoticeRow, mapRequestRow, markInboxConsumed, TABLE_NOTICE, TABLE_REQUEST, } from './inbox-rpc.js';
|
|
4
|
+
import { cancelLogin, listLoginPending, submitLogin } from './login-rpc.js';
|
|
5
|
+
import { deleteFriend, groupWriteOp, listChannels, listFriends, listGroupMembers, listGroups, } from './endpoint-rpc.js';
|
|
6
|
+
import { mutateWorkroomKnowledge, mutateWorkroomProfile } from './workroom-rpc.js';
|
|
7
|
+
export async function dispatchExtendedConsoleRpc(type, data, ctx) {
|
|
8
|
+
const d = data ?? {};
|
|
9
|
+
if (!ctx.fullScope) {
|
|
10
|
+
const denied = assertDemoConsoleRpcAllowed(type);
|
|
11
|
+
if (denied)
|
|
12
|
+
return { error: denied };
|
|
13
|
+
}
|
|
14
|
+
switch (type) {
|
|
15
|
+
case 'schedule:list':
|
|
16
|
+
case 'cron:list':
|
|
17
|
+
return listSchedule(ctx);
|
|
18
|
+
case 'cron:add':
|
|
19
|
+
return addCron(d, ctx);
|
|
20
|
+
case 'cron:remove':
|
|
21
|
+
case 'cron:pause':
|
|
22
|
+
case 'cron:resume':
|
|
23
|
+
return mutateCron(type, d, ctx);
|
|
24
|
+
case 'request.list':
|
|
25
|
+
return listPendingRequests(d, ctx);
|
|
26
|
+
case 'inbox.requests':
|
|
27
|
+
return listInbox(d, ctx, TABLE_REQUEST, 'requests', mapRequestRow);
|
|
28
|
+
case 'inbox.notices':
|
|
29
|
+
return listInbox(d, ctx, TABLE_NOTICE, 'notices', mapNoticeRow);
|
|
30
|
+
case 'inbox.messages':
|
|
31
|
+
return listInboxMessages(d, ctx);
|
|
32
|
+
case 'inbox.recent':
|
|
33
|
+
return listRecentInboxMessages(d, ctx);
|
|
34
|
+
case 'request.approve':
|
|
35
|
+
case 'request.reject':
|
|
36
|
+
return actOnRequest(type, d, ctx);
|
|
37
|
+
case 'login.list':
|
|
38
|
+
return listLoginPending(ctx);
|
|
39
|
+
case 'login.submit':
|
|
40
|
+
return submitLogin(d, ctx);
|
|
41
|
+
case 'login.cancel':
|
|
42
|
+
return cancelLogin(d, ctx);
|
|
43
|
+
case 'request.consumed':
|
|
44
|
+
return markInboxConsumed(d, ctx, TABLE_REQUEST);
|
|
45
|
+
case 'notice.consumed':
|
|
46
|
+
return markInboxConsumed(d, ctx, TABLE_NOTICE);
|
|
47
|
+
case 'endpoint.friends':
|
|
48
|
+
return listFriends(d, ctx);
|
|
49
|
+
case 'endpoint.groups':
|
|
50
|
+
return listGroups(d, ctx);
|
|
51
|
+
case 'endpoint.channels':
|
|
52
|
+
return listChannels(d, ctx);
|
|
53
|
+
case 'endpoint.group_members':
|
|
54
|
+
return listGroupMembers(d, ctx);
|
|
55
|
+
case 'endpoint.group_kick':
|
|
56
|
+
return groupWriteOp(d, ctx, {
|
|
57
|
+
method: 'kickGroupMember',
|
|
58
|
+
buildArgs: (gid, uid) => [gid, uid],
|
|
59
|
+
requireUser: true,
|
|
60
|
+
unsupported: '踢出群成员',
|
|
61
|
+
});
|
|
62
|
+
case 'endpoint.group_mute':
|
|
63
|
+
return groupWriteOp(d, ctx, {
|
|
64
|
+
method: 'muteGroupMember',
|
|
65
|
+
buildArgs: (gid, uid, extra) => [gid, uid, extra.duration ?? 600],
|
|
66
|
+
requireUser: true,
|
|
67
|
+
unsupported: '禁言群成员',
|
|
68
|
+
});
|
|
69
|
+
case 'endpoint.group_admin':
|
|
70
|
+
return groupWriteOp(d, ctx, {
|
|
71
|
+
method: 'setGroupAdmin',
|
|
72
|
+
buildArgs: (gid, uid, extra) => [gid, uid, extra.enable !== false],
|
|
73
|
+
requireUser: true,
|
|
74
|
+
unsupported: '设置群管理员',
|
|
75
|
+
});
|
|
76
|
+
case 'endpoint.delete_friend':
|
|
77
|
+
return deleteFriend(d, ctx);
|
|
78
|
+
case 'workroom.profile.status':
|
|
79
|
+
case 'workroom.profile.bootstrap':
|
|
80
|
+
case 'workroom.profile.pack.publish':
|
|
81
|
+
case 'workroom.profile.publish':
|
|
82
|
+
case 'workroom.profile.rollback':
|
|
83
|
+
case 'workroom.profile.policy.publish':
|
|
84
|
+
return mutateWorkroomProfile(type, d, ctx);
|
|
85
|
+
case 'workroom.knowledge.get':
|
|
86
|
+
case 'workroom.knowledge.publish':
|
|
87
|
+
case 'workroom.knowledge.rollback':
|
|
88
|
+
return mutateWorkroomKnowledge(type, d, ctx);
|
|
89
|
+
default:
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ConsoleRpcExtendedCtx, ExtendedRpcResult } from './contracts.js';
|
|
2
|
+
export declare function listFriends(d: Record<string, unknown>, ctx: ConsoleRpcExtendedCtx): Promise<ExtendedRpcResult>;
|
|
3
|
+
export declare function listGroups(d: Record<string, unknown>, ctx: ConsoleRpcExtendedCtx): Promise<ExtendedRpcResult>;
|
|
4
|
+
export declare function listChannels(d: Record<string, unknown>, ctx: ConsoleRpcExtendedCtx): Promise<ExtendedRpcResult>;
|
|
5
|
+
export declare function listGroupMembers(d: Record<string, unknown>, ctx: ConsoleRpcExtendedCtx): Promise<ExtendedRpcResult>;
|
|
6
|
+
interface GroupWriteSpec {
|
|
7
|
+
method: 'kickGroupMember' | 'muteGroupMember' | 'setGroupAdmin';
|
|
8
|
+
buildArgs(groupId: string, userId: string, extra: {
|
|
9
|
+
duration?: number;
|
|
10
|
+
enable?: boolean;
|
|
11
|
+
}): unknown[];
|
|
12
|
+
requireUser: boolean;
|
|
13
|
+
/** 中文操作名,用于"该平台不支持 xxx"。 */
|
|
14
|
+
unsupported: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function groupWriteOp(d: Record<string, unknown>, ctx: ConsoleRpcExtendedCtx, spec: GroupWriteSpec): Promise<ExtendedRpcResult>;
|
|
17
|
+
export declare function deleteFriend(d: Record<string, unknown>, ctx: ConsoleRpcExtendedCtx): Promise<ExtendedRpcResult>;
|
|
18
|
+
export declare function normalizeParent(raw: unknown): {
|
|
19
|
+
type: 'group' | 'guild';
|
|
20
|
+
id: string;
|
|
21
|
+
name?: string;
|
|
22
|
+
} | undefined;
|
|
23
|
+
export declare function channelFromStoredRow(row: Record<string, unknown>): Record<string, unknown>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { strField, withLiveEndpoint } from './rpc-values.js';
|
|
2
|
+
export async function listFriends(d, ctx) {
|
|
3
|
+
const endpoint = requireEndpointAddress(d);
|
|
4
|
+
if ('error' in endpoint)
|
|
5
|
+
return endpoint;
|
|
6
|
+
return withLiveEndpoint(ctx, endpoint.adapter, endpoint.endpointKey, async (management) => {
|
|
7
|
+
if (management.listFriends) {
|
|
8
|
+
const friends = [...await management.listFriends()];
|
|
9
|
+
return { data: { friends, count: friends.length } };
|
|
10
|
+
}
|
|
11
|
+
return { error: `当前适配器(${endpoint.adapter})不支持好友列表` };
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export async function listGroups(d, ctx) {
|
|
15
|
+
const endpoint = requireEndpointAddress(d);
|
|
16
|
+
if ('error' in endpoint)
|
|
17
|
+
return endpoint;
|
|
18
|
+
return withLiveEndpoint(ctx, endpoint.adapter, endpoint.endpointKey, async (management) => {
|
|
19
|
+
if (management.listGroups) {
|
|
20
|
+
const groups = [...await management.listGroups()];
|
|
21
|
+
return { data: { groups, count: groups.length } };
|
|
22
|
+
}
|
|
23
|
+
return { error: `当前适配器(${endpoint.adapter})不支持群列表` };
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export async function listChannels(d, ctx) {
|
|
27
|
+
const endpoint = requireEndpointAddress(d);
|
|
28
|
+
if ('error' in endpoint)
|
|
29
|
+
return endpoint;
|
|
30
|
+
return withLiveEndpoint(ctx, endpoint.adapter, endpoint.endpointKey, async (management) => {
|
|
31
|
+
if (management.listChannels) {
|
|
32
|
+
const channels = [...await management.listChannels()];
|
|
33
|
+
return { data: { channels, count: channels.length } };
|
|
34
|
+
}
|
|
35
|
+
return { error: `当前适配器(${endpoint.adapter})不支持频道列表` };
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export async function listGroupMembers(d, ctx) {
|
|
39
|
+
const adapter = strField(d, 'adapter');
|
|
40
|
+
const endpointKey = strField(d, 'endpointKey');
|
|
41
|
+
const groupId = strField(d, 'groupId');
|
|
42
|
+
if (!adapter || !endpointKey || !groupId) {
|
|
43
|
+
return { error: 'adapter, endpointKey, and groupId are required' };
|
|
44
|
+
}
|
|
45
|
+
return withLiveEndpoint(ctx, adapter, endpointKey, async (management) => {
|
|
46
|
+
const method = management.listGroupMembers;
|
|
47
|
+
if (!method) {
|
|
48
|
+
return { error: `当前适配器(${adapter})不支持群成员列表` };
|
|
49
|
+
}
|
|
50
|
+
const members = [...await method(groupId)];
|
|
51
|
+
return { data: { members, count: members.length } };
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
export async function groupWriteOp(d, ctx, spec) {
|
|
55
|
+
const adapter = strField(d, 'adapter');
|
|
56
|
+
const endpointKey = strField(d, 'endpointKey');
|
|
57
|
+
const groupId = strField(d, 'groupId');
|
|
58
|
+
const userId = strField(d, 'userId');
|
|
59
|
+
if (!adapter || !endpointKey || !groupId) {
|
|
60
|
+
return { error: 'adapter, endpointKey, and groupId are required' };
|
|
61
|
+
}
|
|
62
|
+
if (spec.requireUser && !userId) {
|
|
63
|
+
return { error: 'userId is required' };
|
|
64
|
+
}
|
|
65
|
+
return withLiveEndpoint(ctx, adapter, endpointKey, async (management) => {
|
|
66
|
+
const method = management[spec.method];
|
|
67
|
+
if (!method) {
|
|
68
|
+
return { error: `当前适配器(${adapter})不支持${spec.unsupported}` };
|
|
69
|
+
}
|
|
70
|
+
const durationRaw = d.duration;
|
|
71
|
+
const enableRaw = d.enable;
|
|
72
|
+
const args = spec.buildArgs(groupId, userId, {
|
|
73
|
+
duration: typeof durationRaw === 'number' && Number.isFinite(durationRaw)
|
|
74
|
+
? durationRaw
|
|
75
|
+
: undefined,
|
|
76
|
+
enable: typeof enableRaw === 'boolean' ? enableRaw : undefined,
|
|
77
|
+
});
|
|
78
|
+
await method.call(management, ...args);
|
|
79
|
+
return { data: { success: true } };
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
export async function deleteFriend(d, ctx) {
|
|
83
|
+
const adapter = strField(d, 'adapter');
|
|
84
|
+
const endpointKey = strField(d, 'endpointKey');
|
|
85
|
+
const userId = strField(d, 'userId');
|
|
86
|
+
if (!adapter || !endpointKey || !userId) {
|
|
87
|
+
return { error: 'adapter, endpointKey, and userId are required' };
|
|
88
|
+
}
|
|
89
|
+
return withLiveEndpoint(ctx, adapter, endpointKey, async (management) => {
|
|
90
|
+
const method = management.deleteFriend;
|
|
91
|
+
if (!method) {
|
|
92
|
+
return { error: '当前适配器暂不支持删除好友' };
|
|
93
|
+
}
|
|
94
|
+
await method(userId);
|
|
95
|
+
return { data: { success: true } };
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// ---------------------------------------------------------------- helpers
|
|
99
|
+
function requireEndpointAddress(d) {
|
|
100
|
+
const adapter = strField(d, 'adapter');
|
|
101
|
+
const endpointKey = strField(d, 'endpointKey');
|
|
102
|
+
if (!adapter || !endpointKey)
|
|
103
|
+
return { error: 'adapter and endpointKey are required' };
|
|
104
|
+
return { adapter, endpointKey };
|
|
105
|
+
}
|
|
106
|
+
export function normalizeParent(raw) {
|
|
107
|
+
if (!raw || typeof raw !== 'object')
|
|
108
|
+
return undefined;
|
|
109
|
+
const parent = raw;
|
|
110
|
+
const id = typeof parent.id === 'string' && parent.id.trim() ? parent.id.trim() : undefined;
|
|
111
|
+
if (!id)
|
|
112
|
+
return undefined;
|
|
113
|
+
let type;
|
|
114
|
+
if (parent.type === 'group' || parent.type === 'guild')
|
|
115
|
+
type = parent.type;
|
|
116
|
+
else if (parent.type === 'channel')
|
|
117
|
+
type = 'guild';
|
|
118
|
+
if (!type)
|
|
119
|
+
return undefined;
|
|
120
|
+
const name = typeof parent.name === 'string' && parent.name.trim() ? parent.name.trim() : undefined;
|
|
121
|
+
return name ? { type, id, name } : { type, id };
|
|
122
|
+
}
|
|
123
|
+
export function channelFromStoredRow(row) {
|
|
124
|
+
const parent = parentFromStoredRow(row);
|
|
125
|
+
return {
|
|
126
|
+
id: String(row.channel_id ?? ''),
|
|
127
|
+
type: String(row.channel_type ?? ''),
|
|
128
|
+
...(row.channel_name != null ? { name: String(row.channel_name) } : {}),
|
|
129
|
+
...(parent ? { parent } : {}),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function parentFromStoredRow(row) {
|
|
133
|
+
return normalizeParent({
|
|
134
|
+
type: row.channel_parent_type,
|
|
135
|
+
id: row.channel_parent_id,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ConsoleRpcExtendedCtx, ExtendedRpcResult } from './contracts.js';
|
|
2
|
+
export declare const TABLE_MESSAGE = "unified_inbox_message";
|
|
3
|
+
export declare const TABLE_REQUEST = "unified_inbox_request";
|
|
4
|
+
export declare const TABLE_NOTICE = "unified_inbox_notice";
|
|
5
|
+
export declare function listInbox(d: Record<string, unknown>, ctx: ConsoleRpcExtendedCtx, table: string, key: 'requests' | 'notices', mapRow: (row: Record<string, unknown>) => Record<string, unknown>): Promise<ExtendedRpcResult>;
|
|
6
|
+
/** request.list —— 优先 live EndpointManagement.listRequests;否则回退 unified_inbox_request。 */
|
|
7
|
+
export declare function listPendingRequests(d: Record<string, unknown>, ctx: ConsoleRpcExtendedCtx): Promise<ExtendedRpcResult>;
|
|
8
|
+
export declare function listInboxMessages(d: Record<string, unknown>, ctx: ConsoleRpcExtendedCtx): Promise<ExtendedRpcResult>;
|
|
9
|
+
/**
|
|
10
|
+
* Cross-endpoint recent inbox used by paired Mobile Console clients.
|
|
11
|
+
* Unlike endpoint-detail `inbox.messages`, every row carries its complete
|
|
12
|
+
* routable address so selecting a conversation can safely call
|
|
13
|
+
* `endpoint.send_message` through the Device Protocol write allowlist.
|
|
14
|
+
*/
|
|
15
|
+
export declare function listRecentInboxMessages(d: Record<string, unknown>, ctx: ConsoleRpcExtendedCtx): Promise<ExtendedRpcResult>;
|
|
16
|
+
export declare function mapRequestRow(row: Record<string, unknown>): Record<string, unknown>;
|
|
17
|
+
export declare function mapNoticeRow(row: Record<string, unknown>): Record<string, unknown>;
|
|
18
|
+
/**
|
|
19
|
+
* request.consumed / notice.consumed —— 按行 id 置 consumed=1。
|
|
20
|
+
* 表未注册(或 model 无 update)时保持"未接线"报错。
|
|
21
|
+
*/
|
|
22
|
+
export declare function markInboxConsumed(d: Record<string, unknown>, ctx: ConsoleRpcExtendedCtx, table: string): Promise<ExtendedRpcResult>;
|
|
23
|
+
export declare function actOnRequest(type: 'request.approve' | 'request.reject', d: Record<string, unknown>, ctx: ConsoleRpcExtendedCtx): Promise<ExtendedRpcResult>;
|