@zhin.js/host-http 1.0.10 → 1.0.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.
@@ -8,7 +8,9 @@
8
8
  * 数据源约定(只读真实数据源,不做内存假数据):
9
9
  * - cron:`ctx.scheduleHost`(Plugin Runtime ScheduleHost,仅 register/list,
10
10
  * 无持久化引擎,故 cron:add/remove/pause/resume 报"未接线")。
11
- * - inbox:`ctx.databaseHost.models`(unified_inbox_message/request/notice 三张表,
11
+ * - request.list:优先 `management.listRequests()`(live endpoint);否则回退
12
+ * unified_inbox_request。审批始终走 approveRequest/rejectRequest。
13
+ * - inbox(历史):`ctx.databaseHost.models`(unified_inbox_message/request/notice,
12
14
  * 表不存在或查询失败 → 空数组 + inboxEnabled:false;requestConsumed/noticeConsumed
13
15
  * 按行 id update consumed=1,表/model 缺失时报"未接线")。
14
16
  * - 社交/群管:`ctx.withEndpointManagement` 在固定 generation lease 内
@@ -24,6 +26,7 @@ import {
24
26
  const TABLE_MESSAGE = 'unified_inbox_message';
25
27
  const TABLE_REQUEST = 'unified_inbox_request';
26
28
  const TABLE_NOTICE = 'unified_inbox_notice';
29
+ const MAX_INBOX_OFFSET = 10_000;
27
30
 
28
31
  export interface ConsoleRpcExtendedCtx {
29
32
  /** full scope 才允许写操作;demo scope 只放行只读 RPC。 */
@@ -39,8 +42,65 @@ export interface ConsoleRpcExtendedCtx {
39
42
  ) => Promise<T | null>;
40
43
  /** Plugin Runtime DatabaseHost 的 models 视图。 */
41
44
  databaseHost?: { models: { get(name: string): unknown } };
42
- /** Agent Host 持久化调度引擎(@zhin.js/agent getAssistantRuntime().engine),未 init 时返回 null。 */
45
+ /** Narrow generation-bound persistent-job port; null when Assistant is disabled. */
43
46
  resolveScheduleEngine?: () => ConsoleScheduleEngine | null | undefined;
47
+ /** LoginAssist list/submit/cancel(刷新后仍可消费扫码/滑块待办)。 */
48
+ loginAssist?: {
49
+ listPending(): readonly {
50
+ id: string;
51
+ adapter: string;
52
+ endpointKey: string;
53
+ type: string;
54
+ payload: Record<string, unknown>;
55
+ createdAt: number;
56
+ expiresAt?: number;
57
+ }[];
58
+ submit(id: string, value: string | Record<string, unknown>): boolean;
59
+ cancel(id: string, reason?: string): boolean;
60
+ };
61
+ /** Authenticated Root adapter; mutation DTOs deliberately contain no principal/decision fields. */
62
+ workroomProfileControl?: ConsoleWorkroomProfileControlPort;
63
+ /** Authenticated HTTP principal; never accepted from the RPC body. */
64
+ authenticatedPrincipal?: Readonly<{ principalId: string }>;
65
+ workroomKnowledgeControl?: ConsoleWorkroomKnowledgeControlPort;
66
+ }
67
+
68
+ export interface ConsoleWorkroomProfileControlPort {
69
+ publishPack(command: Readonly<{ operationId: string; pack: unknown }>,
70
+ authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
71
+ publishProfile(command: Readonly<{
72
+ operationId: string;
73
+ projectId: string;
74
+ expectedRegistryRevision: number;
75
+ overlay: unknown;
76
+ activate: boolean;
77
+ }>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
78
+ publishRollback(command: Readonly<{
79
+ operationId: string;
80
+ projectId: string;
81
+ expectedRegistryRevision: number;
82
+ restoredFromRevisionId: string;
83
+ overlay: unknown;
84
+ activate: boolean;
85
+ }>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
86
+ publishPlanningPolicy(command: Readonly<{
87
+ operationId: string;
88
+ projectId: string;
89
+ profileRevisionId: string;
90
+ revision: number;
91
+ expectedPreviousDigest?: string;
92
+ policy: unknown;
93
+ }>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
94
+ }
95
+
96
+ export interface ConsoleWorkroomKnowledgeControlPort {
97
+ read(projectId: string): Promise<unknown>;
98
+ publish(command: Readonly<{
99
+ operationId: string; projectId: string; expectedRevision: number; entries: readonly unknown[];
100
+ }>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
101
+ rollback(command: Readonly<{
102
+ operationId: string; projectId: string; expectedRevision: number; restoreRevision: number;
103
+ }>, authenticatedPrincipal: Readonly<{ principalId: string }>): Promise<unknown>;
44
104
  }
45
105
 
46
106
  export interface ConsoleScheduleEngine {
@@ -72,6 +132,8 @@ interface InboxSelection {
72
132
  orderBy?(field: string, direction?: 'ASC' | 'DESC'): InboxSelection;
73
133
  /** 可选:DB 侧 limit 下推。 */
74
134
  limit?(count: number): InboxSelection;
135
+ /** 可选:DB 侧 offset 下推。 */
136
+ offset?(count: number): InboxSelection;
75
137
  then<TResult1 = Record<string, unknown>[], TResult2 = never>(
76
138
  onfulfilled?: ((value: Record<string, unknown>[]) => TResult1 | PromiseLike<TResult1>) | null,
77
139
  onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
@@ -94,6 +156,17 @@ interface EndpointManagementPort {
94
156
  parent?: { type: string; id: string; name?: string };
95
157
  }[]>;
96
158
  listGroupMembers?(groupId: string): Promise<readonly unknown[]>;
159
+ listRequests?(): Promise<readonly {
160
+ platform_request_id: string;
161
+ type: string;
162
+ scene_type?: string | null;
163
+ scene_id: string;
164
+ sub_type?: string | null;
165
+ actor_id: string;
166
+ actor_name?: string | null;
167
+ comment?: string | null;
168
+ created_at: number;
169
+ }[]>;
97
170
  approveRequest?(requestId: string, remark?: string): Promise<void>;
98
171
  rejectRequest?(requestId: string, reason?: string): Promise<void>;
99
172
  kickGroupMember?(groupId: string, userId: string): Promise<void>;
@@ -140,6 +213,13 @@ export async function dispatchExtendedConsoleRpc(
140
213
  case 'request.reject':
141
214
  return actOnRequest(type, d, ctx);
142
215
 
216
+ case 'login.list':
217
+ return listLoginPending(ctx);
218
+ case 'login.submit':
219
+ return submitLogin(d, ctx);
220
+ case 'login.cancel':
221
+ return cancelLogin(d, ctx);
222
+
143
223
  case 'request.consumed':
144
224
  return markInboxConsumed(d, ctx, TABLE_REQUEST);
145
225
  case 'notice.consumed':
@@ -178,11 +258,132 @@ export async function dispatchExtendedConsoleRpc(
178
258
  case 'endpoint.delete_friend':
179
259
  return deleteFriend(d, ctx);
180
260
 
261
+ case 'workroom.profile.pack.publish':
262
+ case 'workroom.profile.publish':
263
+ case 'workroom.profile.rollback':
264
+ case 'workroom.profile.policy.publish':
265
+ return mutateWorkroomProfile(type, d, ctx);
266
+
267
+ case 'workroom.knowledge.get':
268
+ case 'workroom.knowledge.publish':
269
+ case 'workroom.knowledge.rollback':
270
+ return mutateWorkroomKnowledge(type, d, ctx);
271
+
181
272
  default:
182
273
  return undefined;
183
274
  }
184
275
  }
185
276
 
277
+ async function mutateWorkroomProfile(
278
+ type: 'workroom.profile.pack.publish' | 'workroom.profile.publish'
279
+ | 'workroom.profile.rollback' | 'workroom.profile.policy.publish',
280
+ data: Record<string, unknown>,
281
+ ctx: ConsoleRpcExtendedCtx,
282
+ ): Promise<ExtendedRpcResult> {
283
+ const control = ctx.workroomProfileControl;
284
+ if (!control) return { error: 'Workroom Profile authority control is unavailable' };
285
+ for (const forbidden of [
286
+ 'authenticatedPrincipalId', 'principalId', 'decision', 'governance', 'approval', 'authority',
287
+ ]) {
288
+ if (forbidden in data) return { error: `Console Workroom Profile request cannot carry ${forbidden}` };
289
+ }
290
+ const principal = ctx.authenticatedPrincipal;
291
+ if (!principal) return { error: 'Workroom Profile control requires an authenticated principal' };
292
+ try {
293
+ const operationId = requiredRpcText(data.operationId, 'operationId');
294
+ if (type === 'workroom.profile.pack.publish') {
295
+ return { data: await control.publishPack(Object.freeze({ operationId, pack: requiredRpcValue(data.pack, 'pack') }), principal) };
296
+ }
297
+ const projectId = requiredRpcText(data.projectId, 'projectId');
298
+ if (type === 'workroom.profile.policy.publish') {
299
+ const expectedPreviousDigest = optionalRpcText(data.expectedPreviousDigest, 'expectedPreviousDigest');
300
+ return { data: await control.publishPlanningPolicy(Object.freeze({
301
+ operationId,
302
+ projectId,
303
+ profileRevisionId: requiredRpcText(data.profileRevisionId, 'profileRevisionId'),
304
+ revision: requiredRpcInteger(data.revision, 'revision'),
305
+ ...(expectedPreviousDigest ? { expectedPreviousDigest } : {}),
306
+ policy: requiredRpcValue(data.policy, 'policy'),
307
+ }), principal) };
308
+ }
309
+ const common = {
310
+ operationId,
311
+ projectId,
312
+ expectedRegistryRevision: requiredRpcInteger(data.expectedRegistryRevision, 'expectedRegistryRevision', -1),
313
+ overlay: requiredRpcValue(data.overlay, 'overlay'),
314
+ activate: requiredRpcBoolean(data.activate, 'activate'),
315
+ };
316
+ return type === 'workroom.profile.rollback'
317
+ ? { data: await control.publishRollback(Object.freeze({
318
+ ...common,
319
+ restoredFromRevisionId: requiredRpcText(data.restoredFromRevisionId, 'restoredFromRevisionId'),
320
+ }), principal) }
321
+ : { data: await control.publishProfile(Object.freeze(common), principal) };
322
+ } catch (error) {
323
+ return { error: errorMessage(error) };
324
+ }
325
+ }
326
+
327
+ async function mutateWorkroomKnowledge(
328
+ type: 'workroom.knowledge.get' | 'workroom.knowledge.publish' | 'workroom.knowledge.rollback',
329
+ data: Record<string, unknown>,
330
+ ctx: ConsoleRpcExtendedCtx,
331
+ ): Promise<ExtendedRpcResult> {
332
+ const control = ctx.workroomKnowledgeControl;
333
+ if (!control) return { error: 'Workroom Knowledge control is unavailable' };
334
+ for (const forbidden of [
335
+ 'authenticatedPrincipalId', 'principalId', 'ownerPrincipalId', 'source', 'decision',
336
+ 'governance', 'approval', 'authority', 'body', 'content',
337
+ ]) {
338
+ if (forbidden in data) return { error: `Console Workroom Knowledge request cannot carry ${forbidden}` };
339
+ }
340
+ try {
341
+ const projectId = requiredRpcText(data.projectId, 'projectId');
342
+ if (type === 'workroom.knowledge.get') return { data: await control.read(projectId) };
343
+ const principal = ctx.authenticatedPrincipal;
344
+ if (!principal) return { error: 'Workroom Knowledge mutation requires an authenticated principal' };
345
+ const common = {
346
+ operationId: requiredRpcText(data.operationId, 'operationId'), projectId,
347
+ expectedRevision: requiredRpcInteger(data.expectedRevision, 'expectedRevision', -1),
348
+ };
349
+ if (type === 'workroom.knowledge.rollback') {
350
+ return { data: await control.rollback(Object.freeze({
351
+ ...common, restoreRevision: requiredRpcInteger(data.restoreRevision, 'restoreRevision', 0),
352
+ }), principal) };
353
+ }
354
+ if (!Array.isArray(data.entries)) throw new Error('entries is invalid');
355
+ return { data: await control.publish(Object.freeze({ ...common, entries: structuredClone(data.entries) }), principal) };
356
+ } catch (error) {
357
+ return { error: errorMessage(error) };
358
+ }
359
+ }
360
+
361
+ function requiredRpcValue(value: unknown, name: string): unknown {
362
+ if (value === undefined || value === null) throw new Error(`${name} is required`);
363
+ return structuredClone(value);
364
+ }
365
+
366
+ function requiredRpcText(value: unknown, name: string): string {
367
+ if (typeof value !== 'string' || !value.trim() || value !== value.trim()) {
368
+ throw new Error(`${name} is invalid`);
369
+ }
370
+ return value;
371
+ }
372
+
373
+ function optionalRpcText(value: unknown, name: string): string | undefined {
374
+ return value === undefined ? undefined : requiredRpcText(value, name);
375
+ }
376
+
377
+ function requiredRpcInteger(value: unknown, name: string, minimum = 1): number {
378
+ if (!Number.isSafeInteger(value) || Number(value) < minimum) throw new Error(`${name} is invalid`);
379
+ return Number(value);
380
+ }
381
+
382
+ function requiredRpcBoolean(value: unknown, name: string): boolean {
383
+ if (typeof value !== 'boolean') throw new Error(`${name} is invalid`);
384
+ return value;
385
+ }
386
+
186
387
  // ---------------------------------------------------------------- cron
187
388
 
188
389
  function listSchedule(ctx: ConsoleScheduleListCtx): Promise<ExtendedRpcResult> | ExtendedRpcResult {
@@ -311,23 +512,34 @@ async function readInboxRows(
311
512
  orderBy?: { field: string; direction?: 'ASC' | 'DESC' };
312
513
  /** DB 侧 limit 下推(模型不支持时由调用方内存分页兜底)。 */
313
514
  limit?: number;
515
+ /** DB 侧 offset 下推(模型不支持时由调用方内存分页兜底)。 */
516
+ offset?: number;
314
517
  },
315
- ): Promise<{ rows: Record<string, unknown>[]; enabled: boolean }> {
518
+ ): Promise<{ rows: Record<string, unknown>[]; enabled: boolean; offsetApplied: boolean }> {
316
519
  const model = getInboxModel(ctx, table);
317
- if (!model) return { rows: [], enabled: false };
520
+ if (!model) return { rows: [], enabled: false, offsetApplied: false };
318
521
  try {
319
522
  let selection: InboxSelection = model.select().where(where);
523
+ let orderApplied = options?.orderBy == null;
320
524
  if (options?.orderBy && typeof selection.orderBy === 'function') {
321
525
  selection = selection.orderBy(options.orderBy.field, options.orderBy.direction ?? 'DESC');
526
+ orderApplied = true;
322
527
  }
323
- if (options?.limit != null && typeof selection.limit === 'function') {
324
- selection = selection.limit(options.limit);
528
+ const offset = options?.offset ?? 0;
529
+ const offsetSelection = orderApplied ? selection.offset : undefined;
530
+ const canApplyOffset = typeof offsetSelection === 'function';
531
+ const offsetApplied = offset === 0 || canApplyOffset;
532
+ if (offset > 0 && canApplyOffset) {
533
+ selection = offsetSelection.call(selection, offset);
534
+ }
535
+ if (options?.limit != null && orderApplied && typeof selection.limit === 'function') {
536
+ selection = selection.limit(options.limit + (offsetApplied ? 0 : offset));
325
537
  }
326
538
  const rows = await selection;
327
- return { rows: Array.isArray(rows) ? rows : [], enabled: true };
539
+ return { rows: Array.isArray(rows) ? rows : [], enabled: true, offsetApplied };
328
540
  } catch {
329
541
  // 表未创建 / 方言未启动等场景降级为空,不向上抛。
330
- return { rows: [], enabled: false };
542
+ return { rows: [], enabled: false, offsetApplied: false };
331
543
  }
332
544
  }
333
545
 
@@ -341,25 +553,31 @@ async function listInbox(
341
553
  const adapter = strField(d, '$adapter', 'adapter');
342
554
  const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
343
555
  if (!adapter || !endpointKey) return { error: '$adapter and $endpoint required' };
344
- const limit = Math.min(numField(d, 30, '$limit', 'limit'), 100);
345
- const offset = Math.max(0, numField(d, 0, '$offset', 'offset'));
346
- const { rows, enabled } = await readInboxRows(ctx, table, {
556
+ const limit = boundedIntegerField(d, 30, 1, 100, '$limit', 'limit');
557
+ const offset = boundedIntegerField(
558
+ d, 0, 0, MAX_INBOX_OFFSET, '$offset', 'offset',
559
+ );
560
+ const unreadOnly = table === TABLE_NOTICE
561
+ && boolField(d, false, '$unread_only', 'unreadOnly', 'unread_only');
562
+ const { rows, enabled, offsetApplied } = await readInboxRows(ctx, table, {
347
563
  adapter,
348
564
  endpoint_id: endpointKey,
565
+ ...(unreadOnly ? { consumed: 0 } : {}),
349
566
  }, {
350
567
  // sort/limit 下推到 DB 侧(内存 sort/slice 保留,作为无下推能力模型的兜底)
351
568
  orderBy: { field: 'created_at', direction: 'DESC' },
352
- limit: offset + limit,
569
+ limit,
570
+ offset,
353
571
  });
354
572
  const sorted = rows
355
573
  .slice()
356
574
  .sort((a, b) => Number(b.created_at ?? 0) - Number(a.created_at ?? 0))
357
- .slice(offset, offset + limit)
575
+ .slice(offsetApplied ? 0 : offset, (offsetApplied ? 0 : offset) + limit)
358
576
  .map(mapRow);
359
577
  return { data: { [key]: sorted, inboxEnabled: enabled } };
360
578
  }
361
579
 
362
- /** request.list —— 未处理的好友/群请求(unified_inbox_request 中 resolved=0)。 */
580
+ /** request.list —— 优先 live EndpointManagement.listRequests;否则回退 unified_inbox_request。 */
363
581
  async function listPendingRequests(
364
582
  d: Record<string, unknown>,
365
583
  ctx: ConsoleRpcExtendedCtx,
@@ -367,6 +585,32 @@ async function listPendingRequests(
367
585
  const adapter = strField(d, '$adapter', 'adapter');
368
586
  const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
369
587
  if (!adapter || !endpointKey) return { error: '$adapter and $endpoint required' };
588
+
589
+ if (ctx.withEndpointManagement) {
590
+ try {
591
+ const live = await ctx.withEndpointManagement(adapter, endpointKey, async (management) => {
592
+ if (!management.listRequests) return undefined;
593
+ const pending = await management.listRequests();
594
+ const requests = pending.map((row) => mapRequestRow({
595
+ platform_request_id: row.platform_request_id,
596
+ type: row.type,
597
+ scene_type: row.scene_type ?? null,
598
+ scene_id: row.scene_id,
599
+ sub_type: row.sub_type ?? null,
600
+ actor_id: row.actor_id,
601
+ actor_name: row.actor_name ?? null,
602
+ comment: row.comment ?? null,
603
+ created_at: row.created_at,
604
+ resolved: 0,
605
+ }));
606
+ return { data: { requests, inboxEnabled: false, source: 'endpoint' } };
607
+ });
608
+ if (live) return live;
609
+ } catch (error) {
610
+ return { error: errorMessage(error) };
611
+ }
612
+ }
613
+
370
614
  const { rows, enabled } = await readInboxRows(ctx, TABLE_REQUEST, {
371
615
  adapter,
372
616
  endpoint_id: endpointKey,
@@ -377,7 +621,7 @@ async function listPendingRequests(
377
621
  .filter((row) => Number(row.resolved ?? 0) === 0)
378
622
  .sort((a, b) => Number(a.created_at ?? 0) - Number(b.created_at ?? 0))
379
623
  .map(mapRequestRow);
380
- return { data: { requests, inboxEnabled: enabled } };
624
+ return { data: { requests, inboxEnabled: enabled, source: 'inbox' } };
381
625
  }
382
626
 
383
627
  async function listInboxMessages(
@@ -391,7 +635,7 @@ async function listInboxMessages(
391
635
  if (!adapter || !endpointKey || !channelId || !channelType) {
392
636
  return { error: '$adapter, $endpoint, $channel_id, $channel_type required' };
393
637
  }
394
- const limit = Math.min(numField(d, 50, '$limit', 'limit'), 100);
638
+ const limit = boundedIntegerField(d, 50, 1, 100, '$limit', 'limit');
395
639
  const beforeTs = optionalNum(d, '$before_ts', 'beforeTs', 'before_ts');
396
640
  const beforeId = optionalNum(d, '$before_id', 'beforeId', 'before_id');
397
641
  const parent = normalizeParent(d.$parent ?? d.parent);
@@ -461,16 +705,18 @@ function mapRequestRow(row: Record<string, unknown>): Record<string, unknown> {
461
705
  function mapNoticeRow(row: Record<string, unknown>): Record<string, unknown> {
462
706
  const actorId = row.actor_id ?? undefined;
463
707
  const actorName = row.actor_name ?? undefined;
464
- const sceneId = row.scene_id;
465
- const sceneType = row.scene_type ?? undefined;
708
+ const sceneId = String(row.scene_id ?? '');
709
+ const sceneType = row.scene_type == null ? undefined : String(row.scene_type);
466
710
  return {
467
711
  id: row.id,
468
712
  platform_notice_id: row.platform_notice_id,
469
713
  type: row.type,
714
+ noticeType: row.type,
470
715
  scene_type: sceneType,
471
716
  scene_id: sceneId,
472
717
  channel_id: sceneId,
473
718
  channel_type: sceneType,
719
+ channel: { id: sceneId, type: sceneType ?? '' },
474
720
  sub_type: row.sub_type ?? undefined,
475
721
  actor_id: actorId,
476
722
  actor_name: actorName,
@@ -482,6 +728,8 @@ function mapNoticeRow(row: Record<string, unknown>): Record<string, unknown> {
482
728
  payload: row.payload,
483
729
  created_at: row.created_at,
484
730
  timestamp: row.created_at,
731
+ consumed: row.consumed,
732
+ consumed_at: row.consumed_at ?? undefined,
485
733
  };
486
734
  }
487
735
 
@@ -546,6 +794,52 @@ async function actOnRequest(
546
794
  });
547
795
  }
548
796
 
797
+ function listLoginPending(ctx: ConsoleRpcExtendedCtx): ExtendedRpcResult {
798
+ const assist = ctx.loginAssist;
799
+ if (!assist) return { error: '登录辅助未接线(LoginAssist 未挂载)' };
800
+ const tasks = assist.listPending().map((task) => ({
801
+ id: task.id,
802
+ adapter: task.adapter,
803
+ endpointKey: task.endpointKey,
804
+ type: task.type,
805
+ payload: task.payload,
806
+ createdAt: task.createdAt,
807
+ expiresAt: task.expiresAt,
808
+ }));
809
+ return { data: { tasks, count: tasks.length } };
810
+ }
811
+
812
+ function submitLogin(
813
+ d: Record<string, unknown>,
814
+ ctx: ConsoleRpcExtendedCtx,
815
+ ): ExtendedRpcResult {
816
+ const assist = ctx.loginAssist;
817
+ if (!assist) return { error: '登录辅助未接线(LoginAssist 未挂载)' };
818
+ const id = strField(d, '$id', 'id', 'taskId');
819
+ if (!id) return { error: '$id required' };
820
+ const raw = d.$value ?? d.value ?? d.ticket ?? d.code ?? '';
821
+ const value = typeof raw === 'string' || (raw && typeof raw === 'object')
822
+ ? raw as string | Record<string, unknown>
823
+ : String(raw ?? '');
824
+ const ok = assist.submit(id, value);
825
+ if (!ok) return { error: `login task not found: ${id}` };
826
+ return { data: { success: true } };
827
+ }
828
+
829
+ function cancelLogin(
830
+ d: Record<string, unknown>,
831
+ ctx: ConsoleRpcExtendedCtx,
832
+ ): ExtendedRpcResult {
833
+ const assist = ctx.loginAssist;
834
+ if (!assist) return { error: '登录辅助未接线(LoginAssist 未挂载)' };
835
+ const id = strField(d, '$id', 'id', 'taskId');
836
+ if (!id) return { error: '$id required' };
837
+ const reason = strField(d, '$reason', 'reason') || 'cancelled';
838
+ const ok = assist.cancel(id, reason);
839
+ if (!ok) return { error: `login task not found: ${id}` };
840
+ return { data: { success: true } };
841
+ }
842
+
549
843
  // ---------------------------------------------------------------- 社交读取
550
844
 
551
845
  async function listFriends(
@@ -695,6 +989,26 @@ function numField(d: Record<string, unknown>, fallback: number, ...keys: string[
695
989
  return fallback;
696
990
  }
697
991
 
992
+ function boundedIntegerField(
993
+ d: Record<string, unknown>,
994
+ fallback: number,
995
+ minimum: number,
996
+ maximum: number,
997
+ ...keys: string[]
998
+ ): number {
999
+ const value = numField(d, fallback, ...keys);
1000
+ if (!Number.isFinite(value)) return fallback;
1001
+ return Math.min(maximum, Math.max(minimum, Math.trunc(value)));
1002
+ }
1003
+
1004
+ function boolField(d: Record<string, unknown>, fallback: boolean, ...keys: string[]): boolean {
1005
+ for (const key of keys) {
1006
+ const value = d[key];
1007
+ if (typeof value === 'boolean') return value;
1008
+ }
1009
+ return fallback;
1010
+ }
1011
+
698
1012
  function optionalNum(d: Record<string, unknown>, ...keys: string[]): number | undefined {
699
1013
  for (const key of keys) {
700
1014
  if (d[key] == null) continue;
@@ -81,6 +81,16 @@ export type RuntimeConsoleRpcContext = {
81
81
  * Returns whether a process restart is required (always true without ConfigFeature).
82
82
  */
83
83
  setConfigKey?(pluginName: string, data: unknown): Promise<{ restartRequired: boolean }>;
84
+ /** Atomic, revision-checked runtime Workroom Catalog read/write. */
85
+ readWorkroomCatalog?(): Promise<Readonly<{
86
+ agents: Readonly<Record<string, unknown>>;
87
+ workrooms: Readonly<Record<string, unknown>>;
88
+ revision: string;
89
+ }>>;
90
+ setWorkroomCatalog?(
91
+ workrooms: unknown,
92
+ expectedRevision: string,
93
+ ): Promise<Readonly<{ revision: string; restartRequired: boolean }>>;
84
94
  /** Project file manager (allowlisted paths under project root). */
85
95
  listProjectFiles?(): Promise<readonly unknown[]>;
86
96
  readProjectFile?(filePath: string): Promise<{ content: string; size: number }>;
@@ -240,6 +250,39 @@ export async function dispatchRuntimeConsoleRpc(
240
250
  }
241
251
  return payloads;
242
252
  }
253
+ case 'workrooms:get': {
254
+ try {
255
+ if (!ctx.readWorkroomCatalog) {
256
+ emit({ requestId, error: 'Workroom Catalog read is not configured' });
257
+ return payloads;
258
+ }
259
+ emit({ requestId, data: await ctx.readWorkroomCatalog() });
260
+ } catch (error) {
261
+ emit({ requestId, error: error instanceof Error ? error.message : String(error) });
262
+ }
263
+ return payloads;
264
+ }
265
+ case 'workrooms:set': {
266
+ try {
267
+ if (!ctx.setWorkroomCatalog) {
268
+ emit({ requestId, error: 'Workroom Catalog write is not configured' });
269
+ return payloads;
270
+ }
271
+ const expectedRevision = typeof message.expectedRevision === 'string'
272
+ ? message.expectedRevision
273
+ : '';
274
+ if (!/^[a-f0-9]{64}$/u.test(expectedRevision)) {
275
+ emit({ requestId, error: 'expectedRevision is required' });
276
+ return payloads;
277
+ }
278
+ const result = await ctx.setWorkroomCatalog(message.data, expectedRevision);
279
+ ctx.publishEvent?.('workrooms:updated', { revision: result.revision });
280
+ emit({ requestId, data: { success: true, ...result } });
281
+ } catch (error) {
282
+ emit({ requestId, error: error instanceof Error ? error.message : String(error) });
283
+ }
284
+ return payloads;
285
+ }
243
286
  case 'config:save-yaml': {
244
287
  try {
245
288
  const yaml = message.yaml;
@@ -758,6 +801,8 @@ export function isDemoHttpAllowed(method: string, pathname: string, apiBase: str
758
801
  const normalizedBase = apiBase.replace(/\/$/u, '') || '/api';
759
802
  const m = method.toUpperCase();
760
803
  if (m === 'GET' && pathname === `${normalizedBase}/events`) return true;
804
+ if (m === 'GET' && pathname === `${normalizedBase}/events/history`) return true;
805
+ if (m === 'GET' && DEMO_INTROSPECTION_PATHS.has(pathname.slice(normalizedBase.length))) return true;
761
806
  if (m === 'POST' && pathname === `${normalizedBase}/console/request`) return true;
762
807
  // Read-only dashboard / plugin inspection routes (legacy host-api parity).
763
808
  if (m === 'GET' && pathname === `${normalizedBase}/system/status`) return true;
@@ -766,3 +811,13 @@ export function isDemoHttpAllowed(method: string, pathname: string, apiBase: str
766
811
  || pathname.startsWith(`${normalizedBase}/plugins/`))) return true;
767
812
  return false;
768
813
  }
814
+
815
+ const DEMO_INTROSPECTION_PATHS: ReadonlySet<string> = new Set([
816
+ '/introspection/commands',
817
+ '/introspection/middlewares',
818
+ '/introspection/components',
819
+ '/introspection/endpoints',
820
+ '/introspection/bindings',
821
+ '/introspection/tools',
822
+ '/introspection/mcp',
823
+ ]);
package/src/http-host.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  TokenRegistry,
18
18
  extractBearerToken,
19
19
  isDemoWebSocketPath,
20
+ type AuthenticatedTokenPrincipal,
20
21
  type AuthScope,
21
22
  type ScopedTokenConfig,
22
23
  type TokenRegistryConfig,
@@ -73,6 +74,7 @@ export type HttpHandler = (
73
74
  response: ServerResponse,
74
75
  url: URL,
75
76
  authScope: AuthScope,
77
+ authenticatedPrincipal?: AuthenticatedTokenPrincipal,
76
78
  ) => void | Promise<void>;
77
79
 
78
80
  export interface HttpRouteRegistration {
@@ -323,7 +325,7 @@ export function createHttpHost(options: HttpHostOptions = {}): ProcessHttpHost {
323
325
  return;
324
326
  }
325
327
  if (!release) {
326
- await route.handler(request, response, url, auth.scope);
328
+ await route.handler(request, response, url, auth.scope, auth.principal);
327
329
  return;
328
330
  }
329
331
  let released = false;
@@ -336,7 +338,7 @@ export function createHttpHost(options: HttpHostOptions = {}): ProcessHttpHost {
336
338
  };
337
339
  response.once('finish', releaseResponse);
338
340
  response.once('close', releaseResponse);
339
- await route.handler(request, response, url, auth.scope);
341
+ await route.handler(request, response, url, auth.scope, auth.principal);
340
342
  if (response.writableFinished || response.destroyed) releaseResponse();
341
343
  } catch (err) {
342
344
  // 统一把请求体错误(400/413)映射回对应状态码,
@@ -363,7 +365,7 @@ export function createHttpHost(options: HttpHostOptions = {}): ProcessHttpHost {
363
365
  request: IncomingMessage,
364
366
  url: URL,
365
367
  pathname: string,
366
- ): { ok: true; scope: AuthScope } | { ok: false } {
368
+ ): { ok: true; scope: AuthScope; principal?: AuthenticatedTokenPrincipal } | { ok: false } {
367
369
  if (!requiresHttpAuth(pathname, apiBase, authExempt)) {
368
370
  return { ok: true, scope: 'full' };
369
371
  }
@@ -380,7 +382,8 @@ export function createHttpHost(options: HttpHostOptions = {}): ProcessHttpHost {
380
382
  const method = (request.method ?? 'GET').toUpperCase();
381
383
  if (!isDemoHttpAllowed(method, pathname, apiBase)) return { ok: false };
382
384
  }
383
- return { ok: true, scope };
385
+ const principal = tokenRegistry.resolvePrincipal(token);
386
+ return { ok: true, scope, ...(principal ? { principal } : {}) };
384
387
  }
385
388
 
386
389
  function listListedRoutes(): readonly ListedRoute[] {
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export {
15
15
  TokenRegistry,
16
16
  extractBearerToken,
17
17
  isDemoWebSocketPath,
18
+ type AuthenticatedTokenPrincipal,
18
19
  type AuthScope,
19
20
  type ScopedTokenConfig,
20
21
  type TokenRegistryConfig,
@@ -70,5 +71,7 @@ export {
70
71
  dispatchExtendedConsoleRpc,
71
72
  type ConsoleRpcExtendedCtx,
72
73
  type ConsoleScheduleEngine,
74
+ type ConsoleWorkroomProfileControlPort,
75
+ type ConsoleWorkroomKnowledgeControlPort,
73
76
  type ExtendedRpcResult,
74
77
  } from './console-rpc-extended.js';