@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
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { boundedIntegerField, boolField, errorMessage, numArrayField, optionalNum, strField, withLiveEndpoint, } from './rpc-values.js';
|
|
2
|
+
import { channelFromStoredRow, normalizeParent } from './endpoint-rpc.js';
|
|
3
|
+
export const TABLE_MESSAGE = 'unified_inbox_message';
|
|
4
|
+
export const TABLE_REQUEST = 'unified_inbox_request';
|
|
5
|
+
export const TABLE_NOTICE = 'unified_inbox_notice';
|
|
6
|
+
const MAX_INBOX_OFFSET = 10_000;
|
|
7
|
+
const CONSUMED_NOT_WIRED = '收件箱已读标记未接线:unified_inbox_request/notice 表未注册或 DatabaseHost 未启动';
|
|
8
|
+
function getInboxModel(ctx, table) {
|
|
9
|
+
const model = ctx.databaseHost?.models?.get(table);
|
|
10
|
+
if (!model || typeof model.select !== 'function')
|
|
11
|
+
return undefined;
|
|
12
|
+
return model;
|
|
13
|
+
}
|
|
14
|
+
async function readInboxRows(ctx, table, where, options) {
|
|
15
|
+
const model = getInboxModel(ctx, table);
|
|
16
|
+
if (!model)
|
|
17
|
+
return { rows: [], enabled: false, offsetApplied: false };
|
|
18
|
+
try {
|
|
19
|
+
let selection = model.select().where(where);
|
|
20
|
+
let orderApplied = options?.orderBy == null;
|
|
21
|
+
if (options?.orderBy && typeof selection.orderBy === 'function') {
|
|
22
|
+
selection = selection.orderBy(options.orderBy.field, options.orderBy.direction ?? 'DESC');
|
|
23
|
+
orderApplied = true;
|
|
24
|
+
}
|
|
25
|
+
const offset = options?.offset ?? 0;
|
|
26
|
+
const offsetSelection = orderApplied ? selection.offset : undefined;
|
|
27
|
+
const canApplyOffset = typeof offsetSelection === 'function';
|
|
28
|
+
const offsetApplied = offset === 0 || canApplyOffset;
|
|
29
|
+
if (offset > 0 && canApplyOffset) {
|
|
30
|
+
selection = offsetSelection.call(selection, offset);
|
|
31
|
+
}
|
|
32
|
+
if (options?.limit != null && orderApplied && typeof selection.limit === 'function') {
|
|
33
|
+
selection = selection.limit(options.limit + (offsetApplied ? 0 : offset));
|
|
34
|
+
}
|
|
35
|
+
const rows = await selection;
|
|
36
|
+
return { rows: Array.isArray(rows) ? rows : [], enabled: true, offsetApplied };
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// 表未创建 / 方言未启动等场景降级为空,不向上抛。
|
|
40
|
+
return { rows: [], enabled: false, offsetApplied: false };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function listInbox(d, ctx, table, key, mapRow) {
|
|
44
|
+
const adapter = strField(d, 'adapter');
|
|
45
|
+
const endpointKey = strField(d, 'endpointKey');
|
|
46
|
+
if (!adapter || !endpointKey)
|
|
47
|
+
return { error: 'adapter and endpointKey are required' };
|
|
48
|
+
const limit = boundedIntegerField(d, 30, 1, 100, 'limit');
|
|
49
|
+
const offset = boundedIntegerField(d, 0, 0, MAX_INBOX_OFFSET, 'offset');
|
|
50
|
+
const unreadOnly = table === TABLE_NOTICE
|
|
51
|
+
&& boolField(d, false, 'unreadOnly');
|
|
52
|
+
const { rows, enabled, offsetApplied } = await readInboxRows(ctx, table, {
|
|
53
|
+
adapter,
|
|
54
|
+
endpoint_id: endpointKey,
|
|
55
|
+
...(unreadOnly ? { consumed: 0 } : {}),
|
|
56
|
+
}, {
|
|
57
|
+
// sort/limit 下推到 DB 侧(内存 sort/slice 保留,作为无下推能力模型的兜底)
|
|
58
|
+
orderBy: { field: 'created_at', direction: 'DESC' },
|
|
59
|
+
limit,
|
|
60
|
+
offset,
|
|
61
|
+
});
|
|
62
|
+
const sorted = rows
|
|
63
|
+
.slice()
|
|
64
|
+
.sort((a, b) => Number(b.created_at ?? 0) - Number(a.created_at ?? 0))
|
|
65
|
+
.slice(offsetApplied ? 0 : offset, (offsetApplied ? 0 : offset) + limit)
|
|
66
|
+
.map(mapRow);
|
|
67
|
+
return { data: { [key]: sorted, inboxEnabled: enabled } };
|
|
68
|
+
}
|
|
69
|
+
/** request.list —— 优先 live EndpointManagement.listRequests;否则回退 unified_inbox_request。 */
|
|
70
|
+
export async function listPendingRequests(d, ctx) {
|
|
71
|
+
const adapter = strField(d, 'adapter');
|
|
72
|
+
const endpointKey = strField(d, 'endpointKey');
|
|
73
|
+
if (!adapter || !endpointKey)
|
|
74
|
+
return { error: 'adapter and endpointKey are required' };
|
|
75
|
+
if (ctx.withEndpointManagement) {
|
|
76
|
+
try {
|
|
77
|
+
const live = await ctx.withEndpointManagement(adapter, endpointKey, async (management) => {
|
|
78
|
+
if (!management.listRequests)
|
|
79
|
+
return undefined;
|
|
80
|
+
const pending = await management.listRequests();
|
|
81
|
+
const requests = pending.map((row) => mapRequestRow({
|
|
82
|
+
platform_request_id: row.platform_request_id,
|
|
83
|
+
type: row.type,
|
|
84
|
+
scene_type: row.scene_type ?? null,
|
|
85
|
+
scene_id: row.scene_id,
|
|
86
|
+
sub_type: row.sub_type ?? null,
|
|
87
|
+
actor_id: row.actor_id,
|
|
88
|
+
actor_name: row.actor_name ?? null,
|
|
89
|
+
comment: row.comment ?? null,
|
|
90
|
+
created_at: row.created_at,
|
|
91
|
+
resolved: 0,
|
|
92
|
+
}));
|
|
93
|
+
return { data: { requests, inboxEnabled: false, source: 'endpoint' } };
|
|
94
|
+
});
|
|
95
|
+
if (live)
|
|
96
|
+
return live;
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
return { error: errorMessage(error) };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const { rows, enabled } = await readInboxRows(ctx, TABLE_REQUEST, {
|
|
103
|
+
adapter,
|
|
104
|
+
endpoint_id: endpointKey,
|
|
105
|
+
}, {
|
|
106
|
+
orderBy: { field: 'created_at', direction: 'ASC' },
|
|
107
|
+
});
|
|
108
|
+
const requests = rows
|
|
109
|
+
.filter((row) => Number(row.resolved ?? 0) === 0)
|
|
110
|
+
.sort((a, b) => Number(a.created_at ?? 0) - Number(b.created_at ?? 0))
|
|
111
|
+
.map(mapRequestRow);
|
|
112
|
+
return { data: { requests, inboxEnabled: enabled, source: 'inbox' } };
|
|
113
|
+
}
|
|
114
|
+
export async function listInboxMessages(d, ctx) {
|
|
115
|
+
const adapter = strField(d, 'adapter');
|
|
116
|
+
const endpointKey = strField(d, 'endpointKey');
|
|
117
|
+
const channelId = strField(d, 'channelId');
|
|
118
|
+
const channelType = strField(d, 'channelType');
|
|
119
|
+
if (!adapter || !endpointKey || !channelId || !channelType) {
|
|
120
|
+
return { error: 'adapter, endpointKey, channelId, and channelType are required' };
|
|
121
|
+
}
|
|
122
|
+
const limit = boundedIntegerField(d, 50, 1, 100, 'limit');
|
|
123
|
+
const beforeTs = optionalNum(d, 'beforeTs');
|
|
124
|
+
const beforeId = optionalNum(d, 'beforeId');
|
|
125
|
+
const parent = normalizeParent(d.parent);
|
|
126
|
+
const where = {
|
|
127
|
+
adapter,
|
|
128
|
+
endpoint_id: endpointKey,
|
|
129
|
+
channel_id: channelId,
|
|
130
|
+
channel_type: channelType,
|
|
131
|
+
};
|
|
132
|
+
if (parent) {
|
|
133
|
+
where.channel_parent_type = parent.type;
|
|
134
|
+
where.channel_parent_id = parent.id;
|
|
135
|
+
}
|
|
136
|
+
const { rows, enabled } = await readInboxRows(ctx, TABLE_MESSAGE, where, {
|
|
137
|
+
// before_ts/before_id 过滤保留在内存(涉及双字段比较),排序下推 DB 侧
|
|
138
|
+
orderBy: { field: 'created_at', direction: 'DESC' },
|
|
139
|
+
});
|
|
140
|
+
const messages = rows
|
|
141
|
+
.filter((row) => (beforeTs == null || Number(row.created_at ?? 0) < beforeTs)
|
|
142
|
+
&& (beforeId == null || Number(row.id ?? 0) < beforeId))
|
|
143
|
+
.sort((a, b) => Number(b.created_at ?? 0) - Number(a.created_at ?? 0))
|
|
144
|
+
.slice(0, limit)
|
|
145
|
+
.map(mapMessageRow);
|
|
146
|
+
return { data: { messages, inboxEnabled: enabled } };
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Cross-endpoint recent inbox used by paired Mobile Console clients.
|
|
150
|
+
* Unlike endpoint-detail `inbox.messages`, every row carries its complete
|
|
151
|
+
* routable address so selecting a conversation can safely call
|
|
152
|
+
* `endpoint.send_message` through the Device Protocol write allowlist.
|
|
153
|
+
*/
|
|
154
|
+
export async function listRecentInboxMessages(d, ctx) {
|
|
155
|
+
const limit = boundedIntegerField(d, 50, 1, 100, 'limit');
|
|
156
|
+
const [messagePage, requestPage] = await Promise.all([
|
|
157
|
+
readInboxRows(ctx, TABLE_MESSAGE, {}, {
|
|
158
|
+
orderBy: { field: 'created_at', direction: 'DESC' }, limit,
|
|
159
|
+
}),
|
|
160
|
+
readInboxRows(ctx, TABLE_REQUEST, { resolved: 0 }, {
|
|
161
|
+
orderBy: { field: 'created_at', direction: 'DESC' }, limit,
|
|
162
|
+
}),
|
|
163
|
+
]);
|
|
164
|
+
const messages = messagePage.rows
|
|
165
|
+
.slice()
|
|
166
|
+
.sort((left, right) => Number(right.created_at ?? 0) - Number(left.created_at ?? 0))
|
|
167
|
+
.slice(0, limit)
|
|
168
|
+
.map(row => ({
|
|
169
|
+
...mapMessageRow(row),
|
|
170
|
+
adapter: String(row.adapter ?? ''),
|
|
171
|
+
endpointKey: String(row.endpoint_id ?? ''),
|
|
172
|
+
}));
|
|
173
|
+
const requests = requestPage.rows
|
|
174
|
+
.filter(row => Number(row.resolved ?? 0) === 0)
|
|
175
|
+
.slice()
|
|
176
|
+
.sort((left, right) => Number(right.created_at ?? 0) - Number(left.created_at ?? 0))
|
|
177
|
+
.slice(0, limit)
|
|
178
|
+
.map(row => ({
|
|
179
|
+
...mapRequestRow(row),
|
|
180
|
+
adapter: String(row.adapter ?? ''),
|
|
181
|
+
endpointKey: String(row.endpoint_id ?? ''),
|
|
182
|
+
}));
|
|
183
|
+
return { data: { messages, requests, inboxEnabled: messagePage.enabled || requestPage.enabled } };
|
|
184
|
+
}
|
|
185
|
+
export function mapRequestRow(row) {
|
|
186
|
+
return {
|
|
187
|
+
id: row.id,
|
|
188
|
+
platformRequestId: row.platform_request_id,
|
|
189
|
+
type: row.type,
|
|
190
|
+
subType: row.sub_type ?? undefined,
|
|
191
|
+
actor: actorFromStoredRow(row, 'actor'),
|
|
192
|
+
comment: row.comment ?? undefined,
|
|
193
|
+
channel: {
|
|
194
|
+
id: String(row.scene_id ?? ''),
|
|
195
|
+
type: String(row.scene_type ?? ''),
|
|
196
|
+
},
|
|
197
|
+
timestamp: row.created_at,
|
|
198
|
+
resolved: Boolean(row.resolved),
|
|
199
|
+
resolvedAt: row.resolved_at ?? undefined,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
export function mapNoticeRow(row) {
|
|
203
|
+
const actorId = row.actor_id ?? undefined;
|
|
204
|
+
const actorName = row.actor_name ?? undefined;
|
|
205
|
+
const sceneId = String(row.scene_id ?? '');
|
|
206
|
+
const sceneType = row.scene_type == null ? undefined : String(row.scene_type);
|
|
207
|
+
return {
|
|
208
|
+
id: row.id,
|
|
209
|
+
platformNoticeId: row.platform_notice_id,
|
|
210
|
+
noticeType: row.type,
|
|
211
|
+
channel: { id: sceneId, type: sceneType ?? '' },
|
|
212
|
+
subType: row.sub_type ?? undefined,
|
|
213
|
+
...(actorId == null ? {} : { operator: { id: String(actorId), ...(actorName == null ? {} : { name: String(actorName) }) } }),
|
|
214
|
+
...(row.target_id == null ? {} : { target: {
|
|
215
|
+
id: String(row.target_id),
|
|
216
|
+
...(row.target_name == null ? {} : { name: String(row.target_name) }),
|
|
217
|
+
} }),
|
|
218
|
+
payload: row.payload,
|
|
219
|
+
timestamp: row.created_at,
|
|
220
|
+
consumed: Boolean(row.consumed),
|
|
221
|
+
consumedAt: row.consumed_at ?? undefined,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function mapMessageRow(row) {
|
|
225
|
+
return {
|
|
226
|
+
id: row.id,
|
|
227
|
+
platformMessageId: row.platform_message_id,
|
|
228
|
+
sender: actorFromStoredRow(row, 'sender'),
|
|
229
|
+
content: row.content,
|
|
230
|
+
raw: row.raw,
|
|
231
|
+
timestamp: row.created_at,
|
|
232
|
+
channel: channelFromStoredRow(row),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function actorFromStoredRow(row, field) {
|
|
236
|
+
const id = String(row[`${field}_id`] ?? '');
|
|
237
|
+
const name = row[`${field}_name`];
|
|
238
|
+
return name == null ? { id } : { id, name: String(name) };
|
|
239
|
+
}
|
|
240
|
+
// ---------------------------------------------------------------- consumed 标记
|
|
241
|
+
/**
|
|
242
|
+
* request.consumed / notice.consumed —— 按行 id 置 consumed=1。
|
|
243
|
+
* 表未注册(或 model 无 update)时保持"未接线"报错。
|
|
244
|
+
*/
|
|
245
|
+
export async function markInboxConsumed(d, ctx, table) {
|
|
246
|
+
const ids = numArrayField(d, 'rowIds');
|
|
247
|
+
if (ids.length === 0)
|
|
248
|
+
return { error: 'rowIds is required' };
|
|
249
|
+
const model = getInboxModel(ctx, table);
|
|
250
|
+
if (!model || typeof model.update !== 'function')
|
|
251
|
+
return { error: CONSUMED_NOT_WIRED };
|
|
252
|
+
const now = Date.now();
|
|
253
|
+
try {
|
|
254
|
+
for (const id of ids) {
|
|
255
|
+
await model.update({ consumed: 1, consumed_at: now }).where({ id });
|
|
256
|
+
}
|
|
257
|
+
return { data: { success: true, updated: ids.length } };
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
return { error: errorMessage(error) };
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// ---------------------------------------------------------------- 请求审批
|
|
264
|
+
export async function actOnRequest(type, d, ctx) {
|
|
265
|
+
const adapter = strField(d, 'adapter');
|
|
266
|
+
const endpointKey = strField(d, 'endpointKey');
|
|
267
|
+
const requestId = strField(d, 'platformRequestId');
|
|
268
|
+
if (!adapter || !endpointKey || !requestId) {
|
|
269
|
+
return { error: 'adapter, endpointKey, and platformRequestId are required' };
|
|
270
|
+
}
|
|
271
|
+
return withLiveEndpoint(ctx, adapter, endpointKey, async (management) => {
|
|
272
|
+
const approve = type === 'request.approve';
|
|
273
|
+
const method = approve ? management.approveRequest : management.rejectRequest;
|
|
274
|
+
if (!method) {
|
|
275
|
+
return {
|
|
276
|
+
error: `请求审批未接线:当前平台(${adapter})endpoint 不支持 ${approve ? 'approveRequest' : 'rejectRequest'},` +
|
|
277
|
+
'且新 Runtime 尚未挂载 pending request 注册表',
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const extra = approve
|
|
281
|
+
? strField(d, 'remark')
|
|
282
|
+
: strField(d, 'reason');
|
|
283
|
+
await method.call(management, requestId, extra || undefined);
|
|
284
|
+
const model = getInboxModel(ctx, TABLE_REQUEST);
|
|
285
|
+
if (model && typeof model.update === 'function') {
|
|
286
|
+
await model.update({ resolved: 1, resolved_at: Date.now() }).where({
|
|
287
|
+
adapter, endpoint_id: endpointKey, platform_request_id: requestId,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
return { data: { success: true } };
|
|
291
|
+
});
|
|
292
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { dispatchExtendedConsoleRpc } from './dispatcher.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ConsoleRpcExtendedCtx, ExtendedRpcResult } from './contracts.js';
|
|
2
|
+
export declare function listLoginPending(ctx: ConsoleRpcExtendedCtx): ExtendedRpcResult;
|
|
3
|
+
export declare function submitLogin(d: Record<string, unknown>, ctx: ConsoleRpcExtendedCtx): ExtendedRpcResult;
|
|
4
|
+
export declare function cancelLogin(d: Record<string, unknown>, ctx: ConsoleRpcExtendedCtx): ExtendedRpcResult;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { strField } from './rpc-values.js';
|
|
2
|
+
export function listLoginPending(ctx) {
|
|
3
|
+
const assist = ctx.loginAssist;
|
|
4
|
+
if (!assist)
|
|
5
|
+
return { error: '登录辅助未接线(LoginAssist 未挂载)' };
|
|
6
|
+
const tasks = assist.listPending().map((task) => ({
|
|
7
|
+
id: task.id,
|
|
8
|
+
adapter: task.adapter,
|
|
9
|
+
endpointKey: task.endpointKey,
|
|
10
|
+
type: task.type,
|
|
11
|
+
payload: task.payload,
|
|
12
|
+
createdAt: task.createdAt,
|
|
13
|
+
expiresAt: task.expiresAt,
|
|
14
|
+
}));
|
|
15
|
+
return { data: { tasks, count: tasks.length } };
|
|
16
|
+
}
|
|
17
|
+
export function submitLogin(d, ctx) {
|
|
18
|
+
const assist = ctx.loginAssist;
|
|
19
|
+
if (!assist)
|
|
20
|
+
return { error: '登录辅助未接线(LoginAssist 未挂载)' };
|
|
21
|
+
const id = strField(d, 'taskId');
|
|
22
|
+
if (!id)
|
|
23
|
+
return { error: 'taskId is required' };
|
|
24
|
+
const raw = d.value ?? '';
|
|
25
|
+
const value = typeof raw === 'string' || (raw && typeof raw === 'object')
|
|
26
|
+
? raw
|
|
27
|
+
: String(raw ?? '');
|
|
28
|
+
const ok = assist.submit(id, value);
|
|
29
|
+
if (!ok)
|
|
30
|
+
return { error: `login task not found: ${id}` };
|
|
31
|
+
return { data: { success: true } };
|
|
32
|
+
}
|
|
33
|
+
export function cancelLogin(d, ctx) {
|
|
34
|
+
const assist = ctx.loginAssist;
|
|
35
|
+
if (!assist)
|
|
36
|
+
return { error: '登录辅助未接线(LoginAssist 未挂载)' };
|
|
37
|
+
const id = strField(d, 'taskId');
|
|
38
|
+
if (!id)
|
|
39
|
+
return { error: 'taskId is required' };
|
|
40
|
+
const reason = strField(d, 'reason') || 'cancelled';
|
|
41
|
+
const ok = assist.cancel(id, reason);
|
|
42
|
+
if (!ok)
|
|
43
|
+
return { error: `login task not found: ${id}` };
|
|
44
|
+
return { data: { success: true } };
|
|
45
|
+
}
|
|
46
|
+
// ---------------------------------------------------------------- 社交读取
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ConsoleRpcExtendedCtx, EndpointManagementPort, ExtendedRpcResult } from './contracts.js';
|
|
2
|
+
export declare function requiredRpcValue(value: unknown, name: string): unknown;
|
|
3
|
+
export declare function requiredRpcText(value: unknown, name: string): string;
|
|
4
|
+
export declare function optionalRpcText(value: unknown, name: string): string | undefined;
|
|
5
|
+
export declare function optionalRpcTextArray(value: unknown, name: string): readonly string[] | undefined;
|
|
6
|
+
export declare function requiredRpcInteger(value: unknown, name: string, minimum?: number): number;
|
|
7
|
+
export declare function requiredRpcBoolean(value: unknown, name: string): boolean;
|
|
8
|
+
export declare function strField(d: Record<string, unknown>, key: string): string;
|
|
9
|
+
export declare function numField(d: Record<string, unknown>, fallback: number, key: string): number;
|
|
10
|
+
export declare function boundedIntegerField(d: Record<string, unknown>, fallback: number, minimum: number, maximum: number, key: string): number;
|
|
11
|
+
export declare function boolField(d: Record<string, unknown>, fallback: boolean, key: string): boolean;
|
|
12
|
+
export declare function optionalNum(d: Record<string, unknown>, key: string): number | undefined;
|
|
13
|
+
/** Parse one canonical numeric-array field into finite positive ids. */
|
|
14
|
+
export declare function numArrayField(d: Record<string, unknown>, key: string): number[];
|
|
15
|
+
export declare function withLiveEndpoint(ctx: ConsoleRpcExtendedCtx, adapter: string, endpointKey: string, run: (management: EndpointManagementPort) => ExtendedRpcResult | Promise<ExtendedRpcResult>): Promise<ExtendedRpcResult>;
|
|
16
|
+
export declare function errorMessage(error: unknown): string;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export function requiredRpcValue(value, name) {
|
|
2
|
+
if (value === undefined || value === null)
|
|
3
|
+
throw new Error(`${name} is required`);
|
|
4
|
+
return structuredClone(value);
|
|
5
|
+
}
|
|
6
|
+
export function requiredRpcText(value, name) {
|
|
7
|
+
if (typeof value !== 'string' || !value.trim() || value !== value.trim()) {
|
|
8
|
+
throw new Error(`${name} is invalid`);
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
export function optionalRpcText(value, name) {
|
|
13
|
+
return value === undefined ? undefined : requiredRpcText(value, name);
|
|
14
|
+
}
|
|
15
|
+
export function optionalRpcTextArray(value, name) {
|
|
16
|
+
if (value === undefined)
|
|
17
|
+
return undefined;
|
|
18
|
+
if (!Array.isArray(value))
|
|
19
|
+
throw new Error(`${name} is invalid`);
|
|
20
|
+
const result = value.map((item, index) => requiredRpcText(item, `${name}.${index}`));
|
|
21
|
+
if (new Set(result).size !== result.length)
|
|
22
|
+
throw new Error(`${name} contains duplicates`);
|
|
23
|
+
return Object.freeze(result);
|
|
24
|
+
}
|
|
25
|
+
export function requiredRpcInteger(value, name, minimum = 1) {
|
|
26
|
+
if (!Number.isSafeInteger(value) || Number(value) < minimum)
|
|
27
|
+
throw new Error(`${name} is invalid`);
|
|
28
|
+
return Number(value);
|
|
29
|
+
}
|
|
30
|
+
export function requiredRpcBoolean(value, name) {
|
|
31
|
+
if (typeof value !== 'boolean')
|
|
32
|
+
throw new Error(`${name} is invalid`);
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
// ---------------------------------------------------------------- cron
|
|
36
|
+
export function strField(d, key) {
|
|
37
|
+
const value = d[key];
|
|
38
|
+
if (typeof value === 'string' && value.trim())
|
|
39
|
+
return value.trim();
|
|
40
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
41
|
+
return String(value);
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
export function numField(d, fallback, key) {
|
|
45
|
+
const parsed = Number(d[key]);
|
|
46
|
+
return d[key] != null && Number.isFinite(parsed) ? parsed : fallback;
|
|
47
|
+
}
|
|
48
|
+
export function boundedIntegerField(d, fallback, minimum, maximum, key) {
|
|
49
|
+
const value = numField(d, fallback, key);
|
|
50
|
+
if (!Number.isFinite(value))
|
|
51
|
+
return fallback;
|
|
52
|
+
return Math.min(maximum, Math.max(minimum, Math.trunc(value)));
|
|
53
|
+
}
|
|
54
|
+
export function boolField(d, fallback, key) {
|
|
55
|
+
const value = d[key];
|
|
56
|
+
return typeof value === 'boolean' ? value : fallback;
|
|
57
|
+
}
|
|
58
|
+
export function optionalNum(d, key) {
|
|
59
|
+
if (d[key] == null)
|
|
60
|
+
return undefined;
|
|
61
|
+
const parsed = Number(d[key]);
|
|
62
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
63
|
+
}
|
|
64
|
+
/** Parse one canonical numeric-array field into finite positive ids. */
|
|
65
|
+
export function numArrayField(d, key) {
|
|
66
|
+
const value = d[key];
|
|
67
|
+
if (!Array.isArray(value))
|
|
68
|
+
return [];
|
|
69
|
+
return value
|
|
70
|
+
.map((item) => Number(item))
|
|
71
|
+
.filter((item) => Number.isFinite(item));
|
|
72
|
+
}
|
|
73
|
+
export async function withLiveEndpoint(ctx, adapter, endpointKey, run) {
|
|
74
|
+
if (!ctx.withEndpointManagement) {
|
|
75
|
+
return { error: 'Endpoint registry is not configured' };
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
return await ctx.withEndpointManagement(adapter, endpointKey, run)
|
|
79
|
+
?? { error: 'endpoint not found' };
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
return { error: errorMessage(error) };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function errorMessage(error) {
|
|
86
|
+
return error instanceof Error ? error.message : String(error);
|
|
87
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ConsoleRpcExtendedCtx, ExtendedRpcResult } from './contracts.js';
|
|
2
|
+
export declare function listSchedule(ctx: ConsoleScheduleListCtx): Promise<ExtendedRpcResult> | ExtendedRpcResult;
|
|
3
|
+
type ConsoleScheduleListCtx = ConsoleRpcExtendedCtx;
|
|
4
|
+
export declare function addCron(d: Record<string, unknown>, ctx: ConsoleRpcExtendedCtx): Promise<ExtendedRpcResult>;
|
|
5
|
+
export declare function mutateCron(type: 'cron:remove' | 'cron:pause' | 'cron:resume', d: Record<string, unknown>, ctx: ConsoleRpcExtendedCtx): Promise<ExtendedRpcResult>;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { errorMessage } from './rpc-values.js';
|
|
2
|
+
const CRON_NOT_WIRED = '持久化调度未接线:当前 Plugin Runtime ScheduleHost 仅支持插件注册的内存任务(list),' +
|
|
3
|
+
'不支持 add/remove/pause/resume(需要 @zhin.js/agent 持久化调度引擎)';
|
|
4
|
+
export function listSchedule(ctx) {
|
|
5
|
+
const host = ctx.scheduleHost;
|
|
6
|
+
if (!host || typeof host.list !== 'function') {
|
|
7
|
+
return { error: '调度服务未配置(scheduleHost 未挂载)' };
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
const raw = host.list();
|
|
11
|
+
const memory = (Array.isArray(raw) ? raw : [])
|
|
12
|
+
.filter((job) => !!job && typeof job === 'object'
|
|
13
|
+
&& typeof job.id === 'string'
|
|
14
|
+
&& typeof job.cron === 'string')
|
|
15
|
+
.map((job) => ({
|
|
16
|
+
id: job.id,
|
|
17
|
+
cron: job.cron,
|
|
18
|
+
...(typeof job.description === 'string' ? { description: job.description } : {}),
|
|
19
|
+
// console cron 页渲染字段(由 schedule host 提供时透传)
|
|
20
|
+
expression: job.expression ?? job.cron,
|
|
21
|
+
running: job.running ?? true,
|
|
22
|
+
...(job.plugin ? { plugin: job.plugin } : {}),
|
|
23
|
+
...(job.nextExecution
|
|
24
|
+
? { nextExecution: job.nextExecution } : {}),
|
|
25
|
+
}));
|
|
26
|
+
return listPersistentJobs(ctx).then((persistent) => ({ data: { memory, persistent } }));
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
return { error: errorMessage(error) };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** 持久化任务(Agent Host ScheduleJobEngine)→ 前端 PersistentCron 形状。 */
|
|
33
|
+
async function listPersistentJobs(ctx) {
|
|
34
|
+
const engine = ctx.resolveScheduleEngine?.();
|
|
35
|
+
if (!engine)
|
|
36
|
+
return [];
|
|
37
|
+
try {
|
|
38
|
+
const jobs = await engine.listJobs();
|
|
39
|
+
return jobs.map((job) => ({
|
|
40
|
+
id: job.id,
|
|
41
|
+
label: job.label ?? job.id,
|
|
42
|
+
cronExpression: job.schedule?.cron ?? '',
|
|
43
|
+
prompt: job.action?.prompt ?? '',
|
|
44
|
+
enabled: job.enabled !== false,
|
|
45
|
+
source: job.source ?? 'manual',
|
|
46
|
+
createdAt: job.createdAt,
|
|
47
|
+
nextExecution: job.state?.nextRunAtMs,
|
|
48
|
+
...(job.notify?.target
|
|
49
|
+
? { context: { target: job.notify.target } } : {}),
|
|
50
|
+
lastExecutedAt: job.state?.lastExecutedAt,
|
|
51
|
+
lastStatus: job.state?.lastStatus,
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export async function addCron(d, ctx) {
|
|
59
|
+
const engine = ctx.resolveScheduleEngine?.();
|
|
60
|
+
if (!engine)
|
|
61
|
+
return { error: CRON_NOT_WIRED };
|
|
62
|
+
const cronExpression = String(d.cronExpression ?? '').trim();
|
|
63
|
+
const prompt = String(d.prompt ?? '').trim();
|
|
64
|
+
if (!cronExpression)
|
|
65
|
+
return { error: 'cronExpression is required' };
|
|
66
|
+
if (!prompt)
|
|
67
|
+
return { error: 'prompt is required' };
|
|
68
|
+
const label = typeof d.label === 'string' && d.label.trim() ? d.label.trim() : undefined;
|
|
69
|
+
const context = (d.context ?? {});
|
|
70
|
+
const target = context.target ?? context.channel;
|
|
71
|
+
const job = {
|
|
72
|
+
id: `console-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`,
|
|
73
|
+
...(label ? { label } : {}),
|
|
74
|
+
enabled: true,
|
|
75
|
+
schedule: { kind: 'solar', cron: cronExpression },
|
|
76
|
+
action: { kind: 'agent', prompt },
|
|
77
|
+
notify: target
|
|
78
|
+
? { channel: 'im', target }
|
|
79
|
+
: { channel: 'silent' },
|
|
80
|
+
source: 'manual',
|
|
81
|
+
};
|
|
82
|
+
try {
|
|
83
|
+
const created = await engine.addJob(job);
|
|
84
|
+
return { data: { job: created, success: true } };
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
return { error: errorMessage(error) };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export async function mutateCron(type, d, ctx) {
|
|
91
|
+
const engine = ctx.resolveScheduleEngine?.();
|
|
92
|
+
if (!engine)
|
|
93
|
+
return { error: CRON_NOT_WIRED };
|
|
94
|
+
const id = String(d.id ?? '').trim();
|
|
95
|
+
if (!id)
|
|
96
|
+
return { error: 'id is required' };
|
|
97
|
+
try {
|
|
98
|
+
const ok = type === 'cron:remove'
|
|
99
|
+
? await engine.removeJob(id)
|
|
100
|
+
: type === 'cron:pause'
|
|
101
|
+
? await engine.pauseJob(id)
|
|
102
|
+
: await engine.resumeJob(id);
|
|
103
|
+
return ok ? { data: { success: true } } : { error: `任务不存在: ${id}` };
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
return { error: errorMessage(error) };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// ---------------------------------------------------------------- inbox
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ConsoleRpcExtendedCtx, ExtendedRpcResult } from './contracts.js';
|
|
2
|
+
export declare function mutateWorkroomProfile(type: 'workroom.profile.status' | 'workroom.profile.bootstrap' | 'workroom.profile.pack.publish' | 'workroom.profile.publish' | 'workroom.profile.rollback' | 'workroom.profile.policy.publish', data: Record<string, unknown>, ctx: ConsoleRpcExtendedCtx): Promise<ExtendedRpcResult>;
|
|
3
|
+
export declare function mutateWorkroomKnowledge(type: 'workroom.knowledge.get' | 'workroom.knowledge.publish' | 'workroom.knowledge.rollback', data: Record<string, unknown>, ctx: ConsoleRpcExtendedCtx): Promise<ExtendedRpcResult>;
|