@zhin.js/host-http 1.1.11 → 1.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/console-rpc-extended/contracts.d.ts +44 -9
- package/lib/console-rpc-extended/dispatcher.js +0 -6
- package/lib/console-rpc-extended/endpoint-rpc.d.ts +0 -5
- package/lib/console-rpc-extended/endpoint-rpc.js +16 -10
- package/lib/console-rpc-extended/inbox-rpc.js +4 -1
- package/lib/console-rpc-extended/login-rpc.js +20 -2
- package/lib/console-rpc.js +6 -0
- package/lib/http-host.js +15 -11
- package/package.json +2 -2
- package/src/console-rpc-extended/contracts.ts +26 -9
- package/src/console-rpc-extended/dispatcher.ts +0 -6
- package/src/console-rpc-extended/endpoint-rpc.ts +16 -12
- package/src/console-rpc-extended/inbox-rpc.ts +2 -1
- package/src/console-rpc-extended/login-rpc.ts +16 -2
- package/src/console-rpc.ts +4 -0
- package/src/http-host.ts +12 -10
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
interface LoginEndpointIdentity {
|
|
2
|
+
readonly canonicalId: string;
|
|
3
|
+
readonly instanceId: string;
|
|
4
|
+
readonly epoch: number;
|
|
5
|
+
readonly incarnationId: string;
|
|
6
|
+
}
|
|
1
7
|
export interface ConsoleRpcExtendedCtx {
|
|
2
8
|
/** full scope 才允许写操作;demo scope 只放行只读 RPC。 */
|
|
3
9
|
fullScope: boolean;
|
|
@@ -20,13 +26,14 @@ export interface ConsoleRpcExtendedCtx {
|
|
|
20
26
|
id: string;
|
|
21
27
|
adapter: string;
|
|
22
28
|
endpointKey: string;
|
|
29
|
+
endpointIdentity?: LoginEndpointIdentity;
|
|
23
30
|
type: string;
|
|
24
31
|
payload: Record<string, unknown>;
|
|
25
32
|
createdAt: number;
|
|
26
33
|
expiresAt?: number;
|
|
27
34
|
}[];
|
|
28
|
-
submit(id: string, value: string | Record<string, unknown
|
|
29
|
-
cancel(id: string, reason?: string): boolean;
|
|
35
|
+
submit(id: string, value: string | Record<string, unknown>, endpointIdentity?: LoginEndpointIdentity): boolean;
|
|
36
|
+
cancel(id: string, reason?: string, endpointIdentity?: LoginEndpointIdentity): boolean;
|
|
30
37
|
};
|
|
31
38
|
/** Authenticated Root adapter; mutation DTOs deliberately contain no principal/decision fields. */
|
|
32
39
|
workroomProfileControl?: ConsoleWorkroomProfileControlPort;
|
|
@@ -137,6 +144,33 @@ export interface InboxModel {
|
|
|
137
144
|
where(query: Record<string, unknown>): Promise<unknown> | unknown;
|
|
138
145
|
};
|
|
139
146
|
}
|
|
147
|
+
/** HTTP's narrow structural seam; implementations keep platform objects and credentials private. */
|
|
148
|
+
export interface EndpointGroupCommand {
|
|
149
|
+
readonly groupId: string;
|
|
150
|
+
readonly signal?: AbortSignal;
|
|
151
|
+
}
|
|
152
|
+
export interface EndpointUserCommand {
|
|
153
|
+
readonly userId: string;
|
|
154
|
+
readonly signal?: AbortSignal;
|
|
155
|
+
}
|
|
156
|
+
export interface EndpointMemberCommand extends EndpointGroupCommand, EndpointUserCommand {
|
|
157
|
+
}
|
|
158
|
+
export interface EndpointMuteCommand extends EndpointMemberCommand {
|
|
159
|
+
readonly durationSeconds: number;
|
|
160
|
+
}
|
|
161
|
+
export interface EndpointAdminCommand extends EndpointMemberCommand {
|
|
162
|
+
readonly enabled: boolean;
|
|
163
|
+
}
|
|
164
|
+
export interface EndpointApproveCommand {
|
|
165
|
+
readonly requestId: string;
|
|
166
|
+
readonly remark?: string;
|
|
167
|
+
readonly signal?: AbortSignal;
|
|
168
|
+
}
|
|
169
|
+
export interface EndpointRejectCommand {
|
|
170
|
+
readonly requestId: string;
|
|
171
|
+
readonly reason?: string;
|
|
172
|
+
readonly signal?: AbortSignal;
|
|
173
|
+
}
|
|
140
174
|
export interface EndpointManagementPort {
|
|
141
175
|
listFriends?(): Promise<readonly {
|
|
142
176
|
user_id: number | string;
|
|
@@ -156,7 +190,7 @@ export interface EndpointManagementPort {
|
|
|
156
190
|
name?: string;
|
|
157
191
|
};
|
|
158
192
|
}[]>;
|
|
159
|
-
listGroupMembers?(
|
|
193
|
+
listGroupMembers?(request: EndpointGroupCommand): Promise<readonly unknown[]>;
|
|
160
194
|
listRequests?(): Promise<readonly {
|
|
161
195
|
platform_request_id: string;
|
|
162
196
|
type: string;
|
|
@@ -168,10 +202,11 @@ export interface EndpointManagementPort {
|
|
|
168
202
|
comment?: string | null;
|
|
169
203
|
created_at: number;
|
|
170
204
|
}[]>;
|
|
171
|
-
approveRequest?(
|
|
172
|
-
rejectRequest?(
|
|
173
|
-
kickGroupMember?(
|
|
174
|
-
muteGroupMember?(
|
|
175
|
-
setGroupAdmin?(
|
|
176
|
-
deleteFriend?(
|
|
205
|
+
approveRequest?(request: EndpointApproveCommand): Promise<void>;
|
|
206
|
+
rejectRequest?(request: EndpointRejectCommand): Promise<void>;
|
|
207
|
+
kickGroupMember?(request: EndpointMemberCommand): Promise<void>;
|
|
208
|
+
muteGroupMember?(request: EndpointMuteCommand): Promise<void>;
|
|
209
|
+
setGroupAdmin?(request: EndpointAdminCommand): Promise<void>;
|
|
210
|
+
deleteFriend?(request: EndpointUserCommand): Promise<void>;
|
|
177
211
|
}
|
|
212
|
+
export {};
|
|
@@ -57,22 +57,16 @@ export async function dispatchExtendedConsoleRpc(type, data, ctx) {
|
|
|
57
57
|
case 'endpoint.group_kick':
|
|
58
58
|
return groupWriteOp(d, ctx, {
|
|
59
59
|
method: 'kickGroupMember',
|
|
60
|
-
buildArgs: (gid, uid) => [gid, uid],
|
|
61
|
-
requireUser: true,
|
|
62
60
|
unsupported: '踢出群成员',
|
|
63
61
|
});
|
|
64
62
|
case 'endpoint.group_mute':
|
|
65
63
|
return groupWriteOp(d, ctx, {
|
|
66
64
|
method: 'muteGroupMember',
|
|
67
|
-
buildArgs: (gid, uid, extra) => [gid, uid, extra.duration ?? 600],
|
|
68
|
-
requireUser: true,
|
|
69
65
|
unsupported: '禁言群成员',
|
|
70
66
|
});
|
|
71
67
|
case 'endpoint.group_admin':
|
|
72
68
|
return groupWriteOp(d, ctx, {
|
|
73
69
|
method: 'setGroupAdmin',
|
|
74
|
-
buildArgs: (gid, uid, extra) => [gid, uid, extra.enable !== false],
|
|
75
|
-
requireUser: true,
|
|
76
70
|
unsupported: '设置群管理员',
|
|
77
71
|
});
|
|
78
72
|
case 'endpoint.delete_friend':
|
|
@@ -5,11 +5,6 @@ export declare function listChannels(d: Record<string, unknown>, ctx: ConsoleRpc
|
|
|
5
5
|
export declare function listGroupMembers(d: Record<string, unknown>, ctx: ConsoleRpcExtendedCtx): Promise<ExtendedRpcResult>;
|
|
6
6
|
interface GroupWriteSpec {
|
|
7
7
|
method: 'kickGroupMember' | 'muteGroupMember' | 'setGroupAdmin';
|
|
8
|
-
buildArgs(groupId: string, userId: string, extra: {
|
|
9
|
-
duration?: number;
|
|
10
|
-
enable?: boolean;
|
|
11
|
-
}): unknown[];
|
|
12
|
-
requireUser: boolean;
|
|
13
8
|
/** 中文操作名,用于"该平台不支持 xxx"。 */
|
|
14
9
|
unsupported: string;
|
|
15
10
|
}
|
|
@@ -47,7 +47,7 @@ export async function listGroupMembers(d, ctx) {
|
|
|
47
47
|
if (!method) {
|
|
48
48
|
return { error: `当前适配器(${adapter})不支持群成员列表` };
|
|
49
49
|
}
|
|
50
|
-
const members = [...await method(groupId)];
|
|
50
|
+
const members = [...await method.call(management, { groupId })];
|
|
51
51
|
return { data: { members, count: members.length } };
|
|
52
52
|
});
|
|
53
53
|
}
|
|
@@ -59,7 +59,7 @@ export async function groupWriteOp(d, ctx, spec) {
|
|
|
59
59
|
if (!adapter || !endpointKey || !groupId) {
|
|
60
60
|
return { error: 'adapter, endpointKey, and groupId are required' };
|
|
61
61
|
}
|
|
62
|
-
if (
|
|
62
|
+
if (!userId) {
|
|
63
63
|
return { error: 'userId is required' };
|
|
64
64
|
}
|
|
65
65
|
return withLiveEndpoint(ctx, adapter, endpointKey, async (management) => {
|
|
@@ -69,13 +69,19 @@ export async function groupWriteOp(d, ctx, spec) {
|
|
|
69
69
|
}
|
|
70
70
|
const durationRaw = d.duration;
|
|
71
71
|
const enableRaw = d.enable;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
// The discriminant selects one typed semantic request; no argument arrays or casts.
|
|
73
|
+
switch (spec.method) {
|
|
74
|
+
case 'kickGroupMember':
|
|
75
|
+
await management.kickGroupMember({ groupId, userId });
|
|
76
|
+
break;
|
|
77
|
+
case 'muteGroupMember':
|
|
78
|
+
await management.muteGroupMember({ groupId, userId,
|
|
79
|
+
durationSeconds: typeof durationRaw === 'number' && Number.isFinite(durationRaw) ? durationRaw : 600 });
|
|
80
|
+
break;
|
|
81
|
+
case 'setGroupAdmin':
|
|
82
|
+
await management.setGroupAdmin({ groupId, userId, enabled: enableRaw !== false });
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
79
85
|
return { data: { success: true } };
|
|
80
86
|
});
|
|
81
87
|
}
|
|
@@ -91,7 +97,7 @@ export async function deleteFriend(d, ctx) {
|
|
|
91
97
|
if (!method) {
|
|
92
98
|
return { error: '当前适配器暂不支持删除好友' };
|
|
93
99
|
}
|
|
94
|
-
await method(userId);
|
|
100
|
+
await method.call(management, { userId });
|
|
95
101
|
return { data: { success: true } };
|
|
96
102
|
});
|
|
97
103
|
}
|
|
@@ -322,7 +322,10 @@ export async function actOnRequest(type, d, ctx) {
|
|
|
322
322
|
const extra = approve
|
|
323
323
|
? strField(d, 'remark')
|
|
324
324
|
: strField(d, 'reason');
|
|
325
|
-
|
|
325
|
+
if (approve)
|
|
326
|
+
await management.approveRequest({ requestId, remark: extra || undefined });
|
|
327
|
+
else
|
|
328
|
+
await management.rejectRequest({ requestId, reason: extra || undefined });
|
|
326
329
|
const model = getInboxModel(ctx, TABLE_REQUEST);
|
|
327
330
|
if (model && typeof model.update === 'function') {
|
|
328
331
|
await model.update({ resolved: 1, resolved_at: Date.now() }).where({
|
|
@@ -7,6 +7,7 @@ export function listLoginPending(ctx) {
|
|
|
7
7
|
id: task.id,
|
|
8
8
|
adapter: task.adapter,
|
|
9
9
|
endpointKey: task.endpointKey,
|
|
10
|
+
...(task.endpointIdentity ? { endpointIdentity: task.endpointIdentity } : {}),
|
|
10
11
|
type: task.type,
|
|
11
12
|
payload: task.payload,
|
|
12
13
|
createdAt: task.createdAt,
|
|
@@ -25,7 +26,10 @@ export function submitLogin(d, ctx) {
|
|
|
25
26
|
const value = typeof raw === 'string' || (raw && typeof raw === 'object')
|
|
26
27
|
? raw
|
|
27
28
|
: String(raw ?? '');
|
|
28
|
-
const
|
|
29
|
+
const identity = readEndpointIdentity(d.endpointIdentity);
|
|
30
|
+
if (identity === false)
|
|
31
|
+
return { error: 'Invalid login Endpoint identity' };
|
|
32
|
+
const ok = identity ? assist.submit(id, value, identity) : assist.submit(id, value);
|
|
29
33
|
if (!ok)
|
|
30
34
|
return { error: `login task not found: ${id}` };
|
|
31
35
|
return { data: { success: true } };
|
|
@@ -38,9 +42,23 @@ export function cancelLogin(d, ctx) {
|
|
|
38
42
|
if (!id)
|
|
39
43
|
return { error: 'taskId is required' };
|
|
40
44
|
const reason = strField(d, 'reason') || 'cancelled';
|
|
41
|
-
const
|
|
45
|
+
const identity = readEndpointIdentity(d.endpointIdentity);
|
|
46
|
+
if (identity === false)
|
|
47
|
+
return { error: 'Invalid login Endpoint identity' };
|
|
48
|
+
const ok = identity ? assist.cancel(id, reason, identity) : assist.cancel(id, reason);
|
|
42
49
|
if (!ok)
|
|
43
50
|
return { error: `login task not found: ${id}` };
|
|
44
51
|
return { data: { success: true } };
|
|
45
52
|
}
|
|
46
53
|
// ---------------------------------------------------------------- 社交读取
|
|
54
|
+
function readEndpointIdentity(value) {
|
|
55
|
+
if (value === undefined)
|
|
56
|
+
return undefined;
|
|
57
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
58
|
+
return false;
|
|
59
|
+
const identity = value;
|
|
60
|
+
if (Object.keys(identity).length !== 4 || typeof identity.canonicalId !== 'string' || !identity.canonicalId
|
|
61
|
+
|| typeof identity.instanceId !== 'string' || !identity.instanceId || typeof identity.incarnationId !== 'string' || !identity.incarnationId || !Number.isSafeInteger(identity.epoch) || Number(identity.epoch) < 1)
|
|
62
|
+
return false;
|
|
63
|
+
return { canonicalId: identity.canonicalId, instanceId: identity.instanceId, epoch: Number(identity.epoch), incarnationId: identity.incarnationId };
|
|
64
|
+
}
|
package/lib/console-rpc.js
CHANGED
|
@@ -838,6 +838,12 @@ function errorReply(payloads, requestId, error) {
|
|
|
838
838
|
export function isDemoHttpAllowed(method, pathname, apiBase) {
|
|
839
839
|
const normalizedBase = apiBase.replace(/\/$/u, '') || '/api';
|
|
840
840
|
const m = method.toUpperCase();
|
|
841
|
+
// These handlers require an authenticated principal and verify its bound personal owner.
|
|
842
|
+
if ((m === 'GET' || m === 'PUT') && (pathname === `${normalizedBase}/agent/personal-models`
|
|
843
|
+
|| pathname === `${normalizedBase}/agent/personal-session`))
|
|
844
|
+
return true;
|
|
845
|
+
if (m === 'GET' && pathname === `${normalizedBase}/agent/personal-sessions`)
|
|
846
|
+
return true;
|
|
841
847
|
if (m === 'GET' && pathname === `${normalizedBase}/events`)
|
|
842
848
|
return true;
|
|
843
849
|
if (m === 'GET' && pathname === `${normalizedBase}/events/history`)
|
package/lib/http-host.js
CHANGED
|
@@ -112,26 +112,30 @@ export function createHttpHost(options = {}) {
|
|
|
112
112
|
try {
|
|
113
113
|
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
114
114
|
let settled = false;
|
|
115
|
-
const
|
|
115
|
+
const stopSubscriptions = [];
|
|
116
116
|
const releaseAdmissions = () => {
|
|
117
117
|
if (settled)
|
|
118
118
|
return;
|
|
119
119
|
settled = true;
|
|
120
|
-
for (const
|
|
121
|
-
|
|
120
|
+
for (const unsubscribe of stopSubscriptions)
|
|
121
|
+
unsubscribe();
|
|
122
122
|
for (const item of admitted)
|
|
123
123
|
item.release?.();
|
|
124
124
|
};
|
|
125
125
|
ws.once('close', releaseAdmissions);
|
|
126
126
|
ws.once('error', releaseAdmissions);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
127
|
+
// Replacement closes new upgrades, not accepted plugin work. Explicit Root stop
|
|
128
|
+
// still closes its sockets before waiting for their admission leases to drain.
|
|
129
|
+
const stops = new Set(admitted.map(item => item.entry.admission?.stopSignal).filter(signal => signal !== undefined));
|
|
130
|
+
for (const signal of stops) {
|
|
131
|
+
const stop = () => {
|
|
132
|
+
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)
|
|
133
|
+
ws.close(1012, 'Root stopped');
|
|
134
|
+
};
|
|
135
|
+
signal.addEventListener('abort', stop, { once: true });
|
|
136
|
+
stopSubscriptions.push(() => signal.removeEventListener('abort', stop));
|
|
137
|
+
if (signal.aborted)
|
|
138
|
+
stop();
|
|
135
139
|
}
|
|
136
140
|
const connection = Object.freeze({
|
|
137
141
|
socket: ws,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/host-http",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.12",
|
|
4
4
|
"description": "Minimal HTTP and WebSocket host for Plugin Runtime (no Koa)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"@zhin.js/host-http-contract": "1.1.1",
|
|
22
22
|
"@zhin.js/logger": "1.1.2",
|
|
23
23
|
"@zhin.js/schedule": "1.1.3",
|
|
24
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
24
|
+
"@zhin.js/plugin-runtime": "1.1.18"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^26.1.2",
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
interface LoginEndpointIdentity {
|
|
2
|
+
readonly canonicalId: string;
|
|
3
|
+
readonly instanceId: string;
|
|
4
|
+
readonly epoch: number;
|
|
5
|
+
readonly incarnationId: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
1
8
|
export interface ConsoleRpcExtendedCtx {
|
|
2
9
|
/** full scope 才允许写操作;demo scope 只放行只读 RPC。 */
|
|
3
10
|
fullScope: boolean;
|
|
@@ -20,13 +27,14 @@ export interface ConsoleRpcExtendedCtx {
|
|
|
20
27
|
id: string;
|
|
21
28
|
adapter: string;
|
|
22
29
|
endpointKey: string;
|
|
30
|
+
endpointIdentity?: LoginEndpointIdentity;
|
|
23
31
|
type: string;
|
|
24
32
|
payload: Record<string, unknown>;
|
|
25
33
|
createdAt: number;
|
|
26
34
|
expiresAt?: number;
|
|
27
35
|
}[];
|
|
28
|
-
submit(id: string, value: string | Record<string, unknown
|
|
29
|
-
cancel(id: string, reason?: string): boolean;
|
|
36
|
+
submit(id: string, value: string | Record<string, unknown>, endpointIdentity?: LoginEndpointIdentity): boolean;
|
|
37
|
+
cancel(id: string, reason?: string, endpointIdentity?: LoginEndpointIdentity): boolean;
|
|
30
38
|
};
|
|
31
39
|
/** Authenticated Root adapter; mutation DTOs deliberately contain no principal/decision fields. */
|
|
32
40
|
workroomProfileControl?: ConsoleWorkroomProfileControlPort;
|
|
@@ -126,6 +134,15 @@ export interface InboxModel {
|
|
|
126
134
|
};
|
|
127
135
|
}
|
|
128
136
|
|
|
137
|
+
/** HTTP's narrow structural seam; implementations keep platform objects and credentials private. */
|
|
138
|
+
export interface EndpointGroupCommand { readonly groupId: string; readonly signal?: AbortSignal }
|
|
139
|
+
export interface EndpointUserCommand { readonly userId: string; readonly signal?: AbortSignal }
|
|
140
|
+
export interface EndpointMemberCommand extends EndpointGroupCommand, EndpointUserCommand {}
|
|
141
|
+
export interface EndpointMuteCommand extends EndpointMemberCommand { readonly durationSeconds: number }
|
|
142
|
+
export interface EndpointAdminCommand extends EndpointMemberCommand { readonly enabled: boolean }
|
|
143
|
+
export interface EndpointApproveCommand { readonly requestId: string; readonly remark?: string; readonly signal?: AbortSignal }
|
|
144
|
+
export interface EndpointRejectCommand { readonly requestId: string; readonly reason?: string; readonly signal?: AbortSignal }
|
|
145
|
+
|
|
129
146
|
export interface EndpointManagementPort {
|
|
130
147
|
listFriends?(): Promise<readonly { user_id: number | string; nickname: string; remark: string }[]>;
|
|
131
148
|
listGroups?(): Promise<readonly { group_id: number | string; name: string }[]>;
|
|
@@ -134,7 +151,7 @@ export interface EndpointManagementPort {
|
|
|
134
151
|
name?: string;
|
|
135
152
|
parent?: { type: string; id: string; name?: string };
|
|
136
153
|
}[]>;
|
|
137
|
-
listGroupMembers?(
|
|
154
|
+
listGroupMembers?(request: EndpointGroupCommand): Promise<readonly unknown[]>;
|
|
138
155
|
listRequests?(): Promise<readonly {
|
|
139
156
|
platform_request_id: string;
|
|
140
157
|
type: string;
|
|
@@ -146,10 +163,10 @@ export interface EndpointManagementPort {
|
|
|
146
163
|
comment?: string | null;
|
|
147
164
|
created_at: number;
|
|
148
165
|
}[]>;
|
|
149
|
-
approveRequest?(
|
|
150
|
-
rejectRequest?(
|
|
151
|
-
kickGroupMember?(
|
|
152
|
-
muteGroupMember?(
|
|
153
|
-
setGroupAdmin?(
|
|
154
|
-
deleteFriend?(
|
|
166
|
+
approveRequest?(request: EndpointApproveCommand): Promise<void>;
|
|
167
|
+
rejectRequest?(request: EndpointRejectCommand): Promise<void>;
|
|
168
|
+
kickGroupMember?(request: EndpointMemberCommand): Promise<void>;
|
|
169
|
+
muteGroupMember?(request: EndpointMuteCommand): Promise<void>;
|
|
170
|
+
setGroupAdmin?(request: EndpointAdminCommand): Promise<void>;
|
|
171
|
+
deleteFriend?(request: EndpointUserCommand): Promise<void>;
|
|
155
172
|
}
|
|
@@ -77,22 +77,16 @@ export async function dispatchExtendedConsoleRpc(
|
|
|
77
77
|
case 'endpoint.group_kick':
|
|
78
78
|
return groupWriteOp(d, ctx, {
|
|
79
79
|
method: 'kickGroupMember',
|
|
80
|
-
buildArgs: (gid, uid) => [gid, uid],
|
|
81
|
-
requireUser: true,
|
|
82
80
|
unsupported: '踢出群成员',
|
|
83
81
|
});
|
|
84
82
|
case 'endpoint.group_mute':
|
|
85
83
|
return groupWriteOp(d, ctx, {
|
|
86
84
|
method: 'muteGroupMember',
|
|
87
|
-
buildArgs: (gid, uid, extra) => [gid, uid, extra.duration ?? 600],
|
|
88
|
-
requireUser: true,
|
|
89
85
|
unsupported: '禁言群成员',
|
|
90
86
|
});
|
|
91
87
|
case 'endpoint.group_admin':
|
|
92
88
|
return groupWriteOp(d, ctx, {
|
|
93
89
|
method: 'setGroupAdmin',
|
|
94
|
-
buildArgs: (gid, uid, extra) => [gid, uid, extra.enable !== false],
|
|
95
|
-
requireUser: true,
|
|
96
90
|
unsupported: '设置群管理员',
|
|
97
91
|
});
|
|
98
92
|
case 'endpoint.delete_friend':
|
|
@@ -61,7 +61,7 @@ export async function listGroupMembers(
|
|
|
61
61
|
if (!method) {
|
|
62
62
|
return { error: `当前适配器(${adapter})不支持群成员列表` };
|
|
63
63
|
}
|
|
64
|
-
const members = [...await method(groupId)];
|
|
64
|
+
const members = [...await method.call(management, { groupId })];
|
|
65
65
|
return { data: { members, count: members.length } };
|
|
66
66
|
});
|
|
67
67
|
}
|
|
@@ -70,8 +70,6 @@ export async function listGroupMembers(
|
|
|
70
70
|
|
|
71
71
|
interface GroupWriteSpec {
|
|
72
72
|
method: 'kickGroupMember' | 'muteGroupMember' | 'setGroupAdmin';
|
|
73
|
-
buildArgs(groupId: string, userId: string, extra: { duration?: number; enable?: boolean }): unknown[];
|
|
74
|
-
requireUser: boolean;
|
|
75
73
|
/** 中文操作名,用于"该平台不支持 xxx"。 */
|
|
76
74
|
unsupported: string;
|
|
77
75
|
}
|
|
@@ -88,7 +86,7 @@ export async function groupWriteOp(
|
|
|
88
86
|
if (!adapter || !endpointKey || !groupId) {
|
|
89
87
|
return { error: 'adapter, endpointKey, and groupId are required' };
|
|
90
88
|
}
|
|
91
|
-
if (
|
|
89
|
+
if (!userId) {
|
|
92
90
|
return { error: 'userId is required' };
|
|
93
91
|
}
|
|
94
92
|
return withLiveEndpoint(ctx, adapter, endpointKey, async (management) => {
|
|
@@ -98,13 +96,19 @@ export async function groupWriteOp(
|
|
|
98
96
|
}
|
|
99
97
|
const durationRaw = d.duration;
|
|
100
98
|
const enableRaw = d.enable;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
99
|
+
// The discriminant selects one typed semantic request; no argument arrays or casts.
|
|
100
|
+
switch (spec.method) {
|
|
101
|
+
case 'kickGroupMember':
|
|
102
|
+
await management.kickGroupMember!({ groupId, userId });
|
|
103
|
+
break;
|
|
104
|
+
case 'muteGroupMember':
|
|
105
|
+
await management.muteGroupMember!({ groupId, userId,
|
|
106
|
+
durationSeconds: typeof durationRaw === 'number' && Number.isFinite(durationRaw) ? durationRaw : 600 });
|
|
107
|
+
break;
|
|
108
|
+
case 'setGroupAdmin':
|
|
109
|
+
await management.setGroupAdmin!({ groupId, userId, enabled: enableRaw !== false });
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
108
112
|
return { data: { success: true } };
|
|
109
113
|
});
|
|
110
114
|
}
|
|
@@ -124,7 +128,7 @@ export async function deleteFriend(
|
|
|
124
128
|
if (!method) {
|
|
125
129
|
return { error: '当前适配器暂不支持删除好友' };
|
|
126
130
|
}
|
|
127
|
-
await method(userId);
|
|
131
|
+
await method.call(management, { userId });
|
|
128
132
|
return { data: { success: true } };
|
|
129
133
|
});
|
|
130
134
|
}
|
|
@@ -364,7 +364,8 @@ export async function actOnRequest(
|
|
|
364
364
|
const extra = approve
|
|
365
365
|
? strField(d, 'remark')
|
|
366
366
|
: strField(d, 'reason');
|
|
367
|
-
await
|
|
367
|
+
if (approve) await management.approveRequest!({ requestId, remark: extra || undefined });
|
|
368
|
+
else await management.rejectRequest!({ requestId, reason: extra || undefined });
|
|
368
369
|
const model = getInboxModel(ctx, TABLE_REQUEST);
|
|
369
370
|
if (model && typeof model.update === 'function') {
|
|
370
371
|
await model.update({resolved: 1, resolved_at: Date.now()}).where({
|
|
@@ -8,6 +8,7 @@ export function listLoginPending(ctx: ConsoleRpcExtendedCtx): ExtendedRpcResult
|
|
|
8
8
|
id: task.id,
|
|
9
9
|
adapter: task.adapter,
|
|
10
10
|
endpointKey: task.endpointKey,
|
|
11
|
+
...(task.endpointIdentity ? { endpointIdentity: task.endpointIdentity } : {}),
|
|
11
12
|
type: task.type,
|
|
12
13
|
payload: task.payload,
|
|
13
14
|
createdAt: task.createdAt,
|
|
@@ -28,7 +29,9 @@ export function submitLogin(
|
|
|
28
29
|
const value = typeof raw === 'string' || (raw && typeof raw === 'object')
|
|
29
30
|
? raw as string | Record<string, unknown>
|
|
30
31
|
: String(raw ?? '');
|
|
31
|
-
const
|
|
32
|
+
const identity = readEndpointIdentity(d.endpointIdentity);
|
|
33
|
+
if (identity === false) return { error: 'Invalid login Endpoint identity' };
|
|
34
|
+
const ok = identity ? assist.submit(id, value, identity) : assist.submit(id, value);
|
|
32
35
|
if (!ok) return { error: `login task not found: ${id}` };
|
|
33
36
|
return { data: { success: true } };
|
|
34
37
|
}
|
|
@@ -42,9 +45,20 @@ export function cancelLogin(
|
|
|
42
45
|
const id = strField(d, 'taskId');
|
|
43
46
|
if (!id) return { error: 'taskId is required' };
|
|
44
47
|
const reason = strField(d, 'reason') || 'cancelled';
|
|
45
|
-
const
|
|
48
|
+
const identity = readEndpointIdentity(d.endpointIdentity);
|
|
49
|
+
if (identity === false) return { error: 'Invalid login Endpoint identity' };
|
|
50
|
+
const ok = identity ? assist.cancel(id, reason, identity) : assist.cancel(id, reason);
|
|
46
51
|
if (!ok) return { error: `login task not found: ${id}` };
|
|
47
52
|
return { data: { success: true } };
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
// ---------------------------------------------------------------- 社交读取
|
|
56
|
+
|
|
57
|
+
function readEndpointIdentity(value: unknown): { readonly canonicalId: string; readonly instanceId: string; readonly epoch: number; readonly incarnationId: string } | false | undefined {
|
|
58
|
+
if (value === undefined) return undefined;
|
|
59
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
60
|
+
const identity = value as Record<string, unknown>;
|
|
61
|
+
if (Object.keys(identity).length !== 4 || typeof identity.canonicalId !== 'string' || !identity.canonicalId
|
|
62
|
+
|| typeof identity.instanceId !== 'string' || !identity.instanceId || typeof identity.incarnationId !== 'string' || !identity.incarnationId || !Number.isSafeInteger(identity.epoch) || Number(identity.epoch) < 1) return false;
|
|
63
|
+
return { canonicalId: identity.canonicalId, instanceId: identity.instanceId, epoch: Number(identity.epoch), incarnationId: identity.incarnationId };
|
|
64
|
+
}
|
package/src/console-rpc.ts
CHANGED
|
@@ -1056,6 +1056,10 @@ function errorReply(
|
|
|
1056
1056
|
export function isDemoHttpAllowed(method: string, pathname: string, apiBase: string): boolean {
|
|
1057
1057
|
const normalizedBase = apiBase.replace(/\/$/u, '') || '/api';
|
|
1058
1058
|
const m = method.toUpperCase();
|
|
1059
|
+
// These handlers require an authenticated principal and verify its bound personal owner.
|
|
1060
|
+
if ((m === 'GET' || m === 'PUT') && (pathname === `${normalizedBase}/agent/personal-models`
|
|
1061
|
+
|| pathname === `${normalizedBase}/agent/personal-session`)) return true;
|
|
1062
|
+
if (m === 'GET' && pathname === `${normalizedBase}/agent/personal-sessions`) return true;
|
|
1059
1063
|
if (m === 'GET' && pathname === `${normalizedBase}/events`) return true;
|
|
1060
1064
|
if (m === 'GET' && pathname === `${normalizedBase}/events/history`) return true;
|
|
1061
1065
|
if (m === 'GET' && DEMO_INTROSPECTION_PATHS.has(pathname.slice(normalizedBase.length))) return true;
|
package/src/http-host.ts
CHANGED
|
@@ -256,23 +256,25 @@ export function createHttpHost(options: HttpHostOptions = {}): ProcessHttpHost {
|
|
|
256
256
|
try {
|
|
257
257
|
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
258
258
|
let settled = false;
|
|
259
|
-
const
|
|
259
|
+
const stopSubscriptions: Array<() => void> = [];
|
|
260
260
|
const releaseAdmissions = () => {
|
|
261
261
|
if (settled) return;
|
|
262
262
|
settled = true;
|
|
263
|
-
for (const
|
|
263
|
+
for (const unsubscribe of stopSubscriptions) unsubscribe();
|
|
264
264
|
for (const item of admitted) item.release?.();
|
|
265
265
|
};
|
|
266
266
|
ws.once('close', releaseAdmissions);
|
|
267
267
|
ws.once('error', releaseAdmissions);
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
268
|
+
// Replacement closes new upgrades, not accepted plugin work. Explicit Root stop
|
|
269
|
+
// still closes its sockets before waiting for their admission leases to drain.
|
|
270
|
+
const stops = new Set(admitted.map(item => item.entry.admission?.stopSignal).filter(signal => signal !== undefined));
|
|
271
|
+
for (const signal of stops) {
|
|
272
|
+
const stop = () => {
|
|
273
|
+
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) ws.close(1012, 'Root stopped');
|
|
274
|
+
};
|
|
275
|
+
signal.addEventListener('abort', stop, { once: true });
|
|
276
|
+
stopSubscriptions.push(() => signal.removeEventListener('abort', stop));
|
|
277
|
+
if (signal.aborted) stop();
|
|
276
278
|
}
|
|
277
279
|
const connection = Object.freeze({
|
|
278
280
|
socket: ws,
|