@zhin.js/host-http 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/lib/access-control.d.ts +50 -5
  2. package/lib/access-control.js +167 -13
  3. package/lib/console-rest-pages-helpers.js +24 -2
  4. package/lib/console-rest-pages.d.ts +4 -1
  5. package/lib/console-rest-pages.js +49 -23
  6. package/lib/console-rpc-extended/contracts.d.ts +92 -15
  7. package/lib/console-rpc-extended/dispatcher.js +10 -3
  8. package/lib/console-rpc-extended/endpoint-rpc.js +21 -4
  9. package/lib/console-rpc-extended/index.d.ts +2 -1
  10. package/lib/console-rpc-extended/index.js +1 -0
  11. package/lib/console-rpc-extended/login-rpc.d.ts +25 -2
  12. package/lib/console-rpc-extended/login-rpc.js +276 -16
  13. package/lib/console-rpc-extended/schedule-rpc.d.ts +1 -1
  14. package/lib/console-rpc-extended/schedule-rpc.js +78 -12
  15. package/lib/console-rpc-handlers.d.ts +11 -1
  16. package/lib/console-rpc-handlers.js +221 -12
  17. package/lib/console-rpc-types.d.ts +2 -0
  18. package/lib/console-rpc-types.js +43 -0
  19. package/lib/console-rpc.d.ts +29 -1
  20. package/lib/console-rpc.js +33 -7
  21. package/lib/http-host.js +5 -4
  22. package/lib/index.d.ts +5 -3
  23. package/lib/index.js +4 -2
  24. package/lib/project-files.d.ts +7 -0
  25. package/lib/project-files.js +88 -11
  26. package/lib/unknown-rpc.d.ts +12 -0
  27. package/lib/unknown-rpc.js +33 -0
  28. package/package.json +3 -3
  29. package/src/access-control.ts +209 -14
  30. package/src/console-rest-pages-helpers.ts +27 -3
  31. package/src/console-rest-pages.ts +52 -15
  32. package/src/console-rpc-extended/contracts.ts +83 -16
  33. package/src/console-rpc-extended/dispatcher.ts +10 -3
  34. package/src/console-rpc-extended/endpoint-rpc.ts +28 -4
  35. package/src/console-rpc-extended/index.ts +4 -0
  36. package/src/console-rpc-extended/login-rpc.ts +250 -17
  37. package/src/console-rpc-extended/schedule-rpc.ts +67 -3
  38. package/src/console-rpc-handlers.ts +198 -11
  39. package/src/console-rpc-types.ts +45 -0
  40. package/src/console-rpc.ts +63 -7
  41. package/src/http-host.ts +6 -2
  42. package/src/index.ts +15 -0
  43. package/src/project-files.ts +95 -11
  44. package/src/unknown-rpc.ts +44 -0
@@ -25,10 +25,17 @@ const INTROSPECTION_PAGE_SIZES = {
25
25
  endpoints: 30,
26
26
  bindings: 30,
27
27
  mcp: 30,
28
+ 'recent-turns': 20,
28
29
  } as const;
29
30
 
30
31
  type IntrospectionKind = keyof typeof INTROSPECTION_PAGE_SIZES;
31
32
 
33
+ /** Owning plugin of a capability row (`ConsoleIntrospectionOwner`, #1250): searchable by `q`. */
34
+ const OWNER_FIELDS: ReadonlyArray<(item: unknown) => string | undefined> = [
35
+ (item) => stringProp(item, 'pluginInstance'),
36
+ (item) => stringProp(item, 'pluginPackage'),
37
+ ];
38
+
32
39
  // ---------------------------------------------------------------------------
33
40
  // Runtime snapshot introspection
34
41
  // ---------------------------------------------------------------------------
@@ -41,7 +48,8 @@ export function registerIntrospectionRoutes(
41
48
  ): void {
42
49
  const collectors: Array<{
43
50
  kind: IntrospectionKind;
44
- collect: (introspection: ConsoleAgentIntrospection | undefined) => { rows: readonly unknown[]; note?: string };
51
+ collect: (introspection: ConsoleAgentIntrospection | undefined) =>
52
+ { rows: readonly unknown[]; note?: string } | Promise<{ rows: readonly unknown[]; note?: string }>;
45
53
  fields: Array<(item: unknown) => string | undefined>;
46
54
  }> = [
47
55
  {
@@ -51,6 +59,7 @@ export function registerIntrospectionRoutes(
51
59
  (c) => stringProp(c, 'pattern'),
52
60
  (c) => stringProp(c, 'desc'),
53
61
  (c) => stringProp(c, 'plugin'),
62
+ ...OWNER_FIELDS,
54
63
  ],
55
64
  },
56
65
  {
@@ -62,6 +71,7 @@ export function registerIntrospectionRoutes(
62
71
  (m) => stringProp(m, 'phase'),
63
72
  (m) => stringProp(m, 'target'),
64
73
  (m) => stringProp(m, 'source'),
74
+ ...OWNER_FIELDS,
65
75
  ],
66
76
  },
67
77
  {
@@ -71,6 +81,7 @@ export function registerIntrospectionRoutes(
71
81
  (c) => stringProp(c, 'name'),
72
82
  (c) => stringProp(c, 'owner'),
73
83
  (c) => stringProp(c, 'source'),
84
+ ...OWNER_FIELDS,
74
85
  ],
75
86
  },
76
87
  {
@@ -87,11 +98,22 @@ export function registerIntrospectionRoutes(
87
98
  },
88
99
  {
89
100
  kind: 'bindings',
90
- collect: (value) => ({ rows: value?.bindings?.() ?? [] }),
101
+ collect: async (value) => ({ rows: await value?.bindings?.() ?? [] }),
91
102
  fields: [
92
103
  (a) => stringProp(a, 'name'),
93
104
  (a) => stringProp(a, 'provider'),
94
105
  (a) => stringProp(a, 'model'),
106
+ (a) => stringProp(a, 'displayName'),
107
+ ],
108
+ },
109
+ {
110
+ // #1250: the Agent workbench's recent conversations come from the turn trace archive, newest first.
111
+ kind: 'recent-turns',
112
+ collect: async (value) => ({ rows: await value?.recentTurns?.() ?? [] }),
113
+ fields: [
114
+ (t) => stringProp(t, 'sessionDisplayName'),
115
+ (t) => stringProp(t, 'sessionKey'),
116
+ (t) => stringProp(t, 'status'),
95
117
  ],
96
118
  },
97
119
  {
@@ -101,6 +123,7 @@ export function registerIntrospectionRoutes(
101
123
  (t) => stringProp(t, 'name'),
102
124
  (t) => stringProp(t, 'source'),
103
125
  (t) => stringProp(t, 'description'),
126
+ ...OWNER_FIELDS,
104
127
  ],
105
128
  },
106
129
  {
@@ -114,6 +137,7 @@ export function registerIntrospectionRoutes(
114
137
  (section) => stringProp(section, 'layer'),
115
138
  (section) => stringProp(section, 'retention'),
116
139
  (section) => stringProp(section, 'source'),
140
+ ...OWNER_FIELDS,
117
141
  ],
118
142
  },
119
143
  {
@@ -136,7 +160,7 @@ export function registerIntrospectionRoutes(
136
160
  const lease = kind === 'endpoints' ? null : ctx.acquireAgentRuntime?.() ?? null;
137
161
  try {
138
162
  const introspection = lease?.value.introspection;
139
- const { rows, note } = collect(introspection);
163
+ const { rows, note } = await collect(introspection);
140
164
  const query = url.searchParams;
141
165
  const filter = query.get('filter')?.trim() || undefined;
142
166
  const filtered = filterByFields([...rows], filter, fields);
@@ -17,6 +17,15 @@ import {
17
17
  isValidNpmPackageName,
18
18
  } from './marketplace-registry.js';
19
19
  import type { ConsoleMessageEventData } from '@zhin.js/console-protocol';
20
+ import {
21
+ informativeKeywords,
22
+ isLocale,
23
+ normalizePluginCategory,
24
+ packageAuthorName,
25
+ pickLocalizedText,
26
+ readPluginMarketplaceMetadata,
27
+ type PluginLocalizedText,
28
+ } from '@zhin.js/plugin-runtime';
20
29
  import {
21
30
  clampPositiveInt,
22
31
  decodePathParam,
@@ -65,7 +74,10 @@ export interface ConsoleAgentIntrospection {
65
74
  commands?(): readonly unknown[];
66
75
  middlewares?(): readonly unknown[];
67
76
  components?(): readonly unknown[];
68
- bindings?(): readonly unknown[];
77
+ /** Agent bindings with the provider / model they actually run on (may read the provider graph). */
78
+ bindings?(): readonly unknown[] | Promise<readonly unknown[]>;
79
+ /** Recent Agent turns from the trace archive, newest first (#1250). */
80
+ recentTurns?(): readonly unknown[] | Promise<readonly unknown[]>;
69
81
  tools?(): readonly unknown[];
70
82
  promptSections?(): readonly unknown[];
71
83
  mcp?(): { rows: readonly unknown[]; note?: string } | readonly unknown[];
@@ -356,6 +368,11 @@ function registerLogsRoutes(
356
368
  }, { summary: 'Cleanup old system logs', tags: ['console', 'logs'] });
357
369
  }
358
370
 
371
+ /** Locale-keyed texts from an untrusted registry entry (reuses the published-manifest reader). */
372
+ function localizedTextOf(value: unknown): PluginLocalizedText | undefined {
373
+ return readPluginMarketplaceMetadata({ metadata: { displayNameI18n: value } }).displayNameI18n;
374
+ }
375
+
359
376
  // ---------------------------------------------------------------------------
360
377
  // marketplace(legacy marketplace-rest-api.ts;plugins.json + npmmirror)
361
378
  // ---------------------------------------------------------------------------
@@ -390,10 +407,17 @@ function registerMarketplaceRoutes(
390
407
  const versions = meta.versions as Record<string, Record<string, unknown>> | undefined;
391
408
  const latestInfo = latest && versions ? versions[latest] : undefined;
392
409
  const time = meta.time as Record<string, string> | undefined;
410
+ // Marketplace fields come from the published manifest (`zhin.metadata`); the author is the
411
+ // package's author / maintainers, never the publishing account (`_npmUser`, a release bot).
412
+ const market = readPluginMarketplaceMetadata(latestInfo?.zhin);
393
413
  return {
394
414
  name: meta.name,
395
415
  version: latest,
396
- description: meta.description || '',
416
+ ...(market.displayName ? { displayName: market.displayName } : {}),
417
+ ...(market.displayNameI18n ? { displayNameI18n: market.displayNameI18n } : {}),
418
+ ...(market.descriptionI18n ? { descriptionI18n: market.descriptionI18n } : {}),
419
+ ...(market.category ? { category: market.category } : {}),
420
+ description: (latestInfo?.description as string | undefined) || meta.description || '',
397
421
  readme: meta.readme || '',
398
422
  license: meta.license || latestInfo?.license || '',
399
423
  homepage: meta.homepage || latestInfo?.homepage || '',
@@ -401,11 +425,8 @@ function registerMarketplaceRoutes(
401
425
  (meta.repository as { url?: string } | undefined)?.url ||
402
426
  (latestInfo?.repository as { url?: string } | undefined)?.url ||
403
427
  '',
404
- author:
405
- typeof meta.author === 'string'
406
- ? meta.author
407
- : (meta.author as { name?: string } | undefined)?.name || '',
408
- keywords: (latestInfo?.keywords as string[]) || [],
428
+ author: packageAuthorName(latestInfo ?? {}) ?? packageAuthorName(meta) ?? '',
429
+ keywords: informativeKeywords(latestInfo?.keywords),
409
430
  engines: latestInfo?.engines || {},
410
431
  peerDependencies: latestInfo?.peerDependencies || {},
411
432
  versions: Object.keys(versions || {}),
@@ -421,32 +442,48 @@ function registerMarketplaceRoutes(
421
442
  100,
422
443
  Math.max(1, parseInt(url.searchParams.get('size') ?? url.searchParams.get('limit') ?? '20', 10) || 20),
423
444
  );
424
- const category = url.searchParams.get('category') ?? undefined;
445
+ const rawCategory = url.searchParams.get('category') || undefined;
446
+ const category = rawCategory === undefined ? undefined : normalizePluginCategory(rawCategory) ?? rawCategory;
425
447
  const official = url.searchParams.get('official') ?? undefined;
448
+ const rawLocale = url.searchParams.get('locale');
449
+ const locale = isLocale(rawLocale) ? rawLocale : undefined;
426
450
  const searchKeyword = q.trim().toLowerCase();
427
451
 
428
452
  try {
429
453
  const allPlugins = (await registry.list()) as readonly Record<string, unknown>[];
430
- let plugins = allPlugins.map((p) => ({
454
+ let plugins = allPlugins.map((p) => {
455
+ const displayNameI18n = localizedTextOf(p.displayNameI18n);
456
+ const descriptionI18n = localizedTextOf(p.descriptionI18n);
457
+ const displayName = typeof p.displayName === 'string' ? p.displayName : '';
458
+ const description = typeof p.description === 'string' ? p.description : '';
459
+ return {
431
460
  name: p.name,
432
- displayName: p.displayName || '',
461
+ // `locale` resolves the display texts for the caller; translations stay available either way.
462
+ displayName: locale ? pickLocalizedText(displayNameI18n, locale, displayName) ?? '' : displayName,
463
+ ...(displayNameI18n ? { displayNameI18n } : {}),
433
464
  version: p.version || '',
434
- description: p.description || '',
465
+ description: locale ? pickLocalizedText(descriptionI18n, locale, description) ?? '' : description,
466
+ ...(descriptionI18n ? { descriptionI18n } : {}),
435
467
  author: p.author || '',
436
468
  isOfficial: typeof p.name === 'string' && /^@zhin\.js\/[a-z0-9][a-z0-9._-]*$/.test(p.name),
437
469
  official: typeof p.name === 'string' && /^@zhin\.js\/[a-z0-9][a-z0-9._-]*$/.test(p.name),
438
- category: p.category || 'util',
439
- keywords: (p.tags as string[]) || [],
470
+ category: normalizePluginCategory(p.category) ?? 'utility',
471
+ keywords: informativeKeywords(p.tags),
440
472
  npm: p.npm || `https://www.npmjs.com/package/${p.name}`,
441
473
  date: p.lastUpdate || '',
442
474
  downloads: p.downloads || { weekly: 0, monthly: 0 },
443
475
  readme: p.readme || '',
444
476
  license: p.license || '',
445
- }));
477
+ };
478
+ });
446
479
 
447
480
  if (searchKeyword) {
448
481
  plugins = plugins.filter((p) => {
449
- const haystack = [p.name, p.displayName, p.description, ...(p.keywords || [])]
482
+ const haystack = [
483
+ p.name, p.displayName, p.description,
484
+ ...Object.values(p.displayNameI18n ?? {}), ...Object.values(p.descriptionI18n ?? {}),
485
+ ...p.keywords,
486
+ ]
450
487
  .join(' ')
451
488
  .toLowerCase();
452
489
  return haystack.includes(searchKeyword);
@@ -1,3 +1,4 @@
1
+ import type { ConsoleAdapterLoginDeclaration, ConsoleLoginFlow, ConsoleLoginOutcome } from '@zhin.js/console-protocol';
1
2
  interface LoginEndpointIdentity {
2
3
  readonly canonicalId: string;
3
4
  readonly instanceId: string;
@@ -21,25 +22,36 @@ export interface ConsoleRpcExtendedCtx {
21
22
  databaseHost?: { models: { get(name: string): unknown } };
22
23
  /** Narrow generation-bound persistent-job port; null when Assistant is disabled. */
23
24
  resolveScheduleEngine?: () => ConsoleScheduleEngine | null | undefined;
24
- /** LoginAssist list/submit/cancel(刷新后仍可消费扫码/滑块待办)。 */
25
- loginAssist?: {
26
- listPending(): readonly {
27
- id: string;
28
- adapter: string;
29
- endpointKey: string;
30
- endpointIdentity?: LoginEndpointIdentity;
31
- type: string;
32
- payload: Record<string, unknown>;
33
- createdAt: number;
34
- expiresAt?: number;
35
- }[];
36
- submit(id: string, value: string | Record<string, unknown>, endpointIdentity?: LoginEndpointIdentity): boolean;
37
- cancel(id: string, reason?: string, endpointIdentity?: LoginEndpointIdentity): boolean;
38
- };
25
+ /**
26
+ * Resolves a persistent job's delivery target (#1250): `{ resolved, display?, reason? }`, where
27
+ * `resolved: false` means the job's results cannot be delivered (no target / unknown account).
28
+ */
29
+ describeScheduleTarget?: (notify: unknown) => ConsoleScheduleTarget;
30
+ /** LoginAssist list/submit/cancel(刷新后仍可消费扫码/滑块待办);ADR 0058 flow methods are optional. */
31
+ loginAssist?: ConsoleLoginAssistPort;
32
+ /** ADR 0058: the Endpoint's login declaration and lease-scoped login control (`null` = unknown Endpoint). */
33
+ withEndpointLogin?: <T>(
34
+ adapter: string,
35
+ endpointKey: string,
36
+ run: (login: ConsoleEndpointLoginAccess) => T | Promise<T>,
37
+ ) => Promise<T | null>;
39
38
  /** Authenticated Root adapter; mutation DTOs deliberately contain no principal/decision fields. */
40
39
  workroomProfileControl?: ConsoleWorkroomProfileControlPort;
41
40
  /** Authenticated HTTP principal; never accepted from the RPC body. */
42
41
  authenticatedPrincipal?: Readonly<{ principalId: string }>;
42
+ /**
43
+ * The Endpoint the caller was authorized for when its grant covers this call only at an adapter /
44
+ * endpoint scope (never set for global grants, owners or in-process callers). Handlers that act on
45
+ * login tasks or flows restrict themselves to it; set by the dispatcher, never read from the body.
46
+ */
47
+ authorizedEndpoint?: Readonly<{ adapter: string; endpointKey: string }>;
48
+ /**
49
+ * Whether the caller holds `endpoint.operate` on one Endpoint (set by the dispatcher from the
50
+ * authenticated principal, never read from the body). Login read RPCs return live challenge
51
+ * material (QR image, sensitive blocks, verification links, legacy `payload.image` / `url`) only
52
+ * when it says yes; absent (in-process callers without a principal) = trusted.
53
+ */
54
+ canOperateEndpoint?: (adapter: string, endpointKey: string) => boolean;
43
55
  workroomKnowledgeControl?: ConsoleWorkroomKnowledgeControlPort;
44
56
  }
45
57
 
@@ -98,7 +110,49 @@ export interface ConsoleScheduleEngine {
98
110
  listJobs(): Promise<Record<string, unknown>[]>;
99
111
  }
100
112
 
101
- export type ExtendedRpcResult = { data: unknown } | { error: string };
113
+ /** `data` next to `error` carries a machine-readable code (e.g. `{ code: 'unsupported' }`). */
114
+ export type ExtendedRpcResult = { data: unknown } | { error: string; data?: unknown };
115
+
116
+ /** Structural LoginAssist port (host/http must not import `@zhin.js/core`). */
117
+ export interface ConsoleLoginAssistPort {
118
+ listPending(): readonly {
119
+ id: string;
120
+ adapter: string;
121
+ endpointKey: string;
122
+ endpointIdentity?: LoginEndpointIdentity;
123
+ type: string;
124
+ payload: Record<string, unknown>;
125
+ createdAt: number;
126
+ expiresAt?: number;
127
+ flowId?: string;
128
+ challengeId?: string;
129
+ }[];
130
+ submit(id: string, value: string | Record<string, unknown>, endpointIdentity?: LoginEndpointIdentity): boolean;
131
+ cancel(id: string, reason?: string, endpointIdentity?: LoginEndpointIdentity): boolean;
132
+ /** ADR 0058 */
133
+ listFlows?(): readonly ConsoleLoginFlow[];
134
+ getFlow?(adapter: string, endpointKey: string): ConsoleLoginFlow | undefined;
135
+ getFlowById?(id: string): ConsoleLoginFlow | undefined;
136
+ /**
137
+ * Event-safe copy of a flow (core `withoutLoginMedia`): no QR image, sensitive blocks redacted,
138
+ * verification links dropped. Returned to readers without `endpoint.operate` on the flow's
139
+ * Endpoint; without it the host strips every challenge's media and blocks.
140
+ */
141
+ eventSafeFlow?(flow: ConsoleLoginFlow): ConsoleLoginFlow;
142
+ submitAndWait?(
143
+ id: string,
144
+ value: string | Record<string, unknown>,
145
+ options?: { endpointIdentity?: LoginEndpointIdentity; timeoutMs?: number },
146
+ ): Promise<{ accepted: boolean; outcome?: ConsoleLoginOutcome; pending?: boolean; flow?: ConsoleLoginFlow }>;
147
+ }
148
+
149
+ export interface ConsoleEndpointLoginAccess {
150
+ readonly declaration?: ConsoleAdapterLoginDeclaration;
151
+ readonly control?: {
152
+ relogin?(request: { signal?: AbortSignal }): Promise<void>;
153
+ refreshChallenge?(request: { signal?: AbortSignal }): Promise<void>;
154
+ };
155
+ }
102
156
 
103
157
  const CRON_NOT_WIRED =
104
158
  '持久化调度未接线:当前 Plugin Runtime ScheduleHost 仅支持插件注册的内存任务(list),' +
@@ -111,6 +165,11 @@ export interface ScheduleJobRow {
111
165
  id: string;
112
166
  cron: string;
113
167
  description?: string;
168
+ /** Host display metadata (`ScheduleJobDisplay` in `@zhin.js/plugin-runtime`). */
169
+ title?: string;
170
+ titleI18n?: Readonly<Partial<Record<string, string>>>;
171
+ kind?: 'plugin-internal' | 'user-task';
172
+ lastRun?: { startedAt: number; finishedAt: number; status: 'succeeded' | 'failed'; error?: string };
114
173
  }
115
174
 
116
175
  export interface InboxSelection {
@@ -170,3 +229,11 @@ export interface EndpointManagementPort {
170
229
  setGroupAdmin?(request: EndpointAdminCommand): Promise<void>;
171
230
  deleteFriend?(request: EndpointUserCommand): Promise<void>;
172
231
  }
232
+
233
+ /** `target` of a `schedule:list` persistent row (#1250). */
234
+ export interface ConsoleScheduleTarget {
235
+ readonly resolved: boolean;
236
+ readonly display?: string;
237
+ /** `missing_target` | `incomplete_target` | `endpoint_not_found` */
238
+ readonly reason?: string;
239
+ }
@@ -4,7 +4,7 @@ import {
4
4
  actOnRequest, listInbox, listInboxMessages, listPendingRequests, listRecentInboxMessages,
5
5
  mapNoticeRow, mapRequestRow, markInboxConsumed, TABLE_NOTICE, TABLE_REQUEST,
6
6
  } from './inbox-rpc.js';
7
- import { cancelLogin, listLoginPending, submitLogin } from './login-rpc.js';
7
+ import { cancelLogin, controlLogin, getLoginFlow, listLoginFlows, listLoginPending, submitLogin } from './login-rpc.js';
8
8
  import {
9
9
  deleteFriend, groupWriteOp, listChannels, listFriends, listGroupMembers, listGroups,
10
10
  } from './endpoint-rpc.js';
@@ -21,7 +21,7 @@ export async function dispatchExtendedConsoleRpc(
21
21
  switch (type) {
22
22
  case 'schedule:list':
23
23
  case 'cron:list':
24
- return listSchedule(ctx);
24
+ return listSchedule(ctx, d);
25
25
 
26
26
  case 'cron:validate':
27
27
  return validateCron(d);
@@ -49,11 +49,18 @@ export async function dispatchExtendedConsoleRpc(
49
49
  return actOnRequest(type, d, ctx);
50
50
 
51
51
  case 'login.list':
52
- return listLoginPending(ctx);
52
+ return listLoginPending(d, ctx);
53
53
  case 'login.submit':
54
54
  return submitLogin(d, ctx);
55
55
  case 'login.cancel':
56
56
  return cancelLogin(d, ctx);
57
+ case 'login.flow.list':
58
+ return listLoginFlows(d, ctx);
59
+ case 'login.flow.get':
60
+ return getLoginFlow(d, ctx);
61
+ case 'login.relogin':
62
+ case 'login.refresh':
63
+ return controlLogin(type, d, ctx);
57
64
 
58
65
  case 'request.consumed':
59
66
  return markInboxConsumed(d, ctx, TABLE_REQUEST);
@@ -1,4 +1,5 @@
1
1
  import type { ConsoleRpcExtendedCtx, EndpointManagementPort, ExtendedRpcResult } from './contracts.js';
2
+ import type { EndpointManagementCapability } from '@zhin.js/console-protocol';
2
3
  import { boolField, numField, strField, withLiveEndpoint } from './rpc-values.js';
3
4
 
4
5
  export async function listFriends(
@@ -12,7 +13,7 @@ export async function listFriends(
12
13
  const friends = [...await management.listFriends()];
13
14
  return { data: { friends, count: friends.length } };
14
15
  }
15
- return { error: `当前适配器(${endpoint.adapter})不支持好友列表` };
16
+ return unsupportedList('friends', 'listFriends', endpoint.adapter, '好友列表');
16
17
  });
17
18
  }
18
19
 
@@ -27,7 +28,7 @@ export async function listGroups(
27
28
  const groups = [...await management.listGroups()];
28
29
  return { data: { groups, count: groups.length } };
29
30
  }
30
- return { error: `当前适配器(${endpoint.adapter})不支持群列表` };
31
+ return unsupportedList('groups', 'listGroups', endpoint.adapter, '群列表');
31
32
  });
32
33
  }
33
34
 
@@ -42,7 +43,7 @@ export async function listChannels(
42
43
  const channels = [...await management.listChannels()];
43
44
  return { data: { channels, count: channels.length } };
44
45
  }
45
- return { error: `当前适配器(${endpoint.adapter})不支持频道列表` };
46
+ return unsupportedList('channels', 'listChannels', endpoint.adapter, '频道列表');
46
47
  });
47
48
  }
48
49
 
@@ -59,13 +60,36 @@ export async function listGroupMembers(
59
60
  return withLiveEndpoint(ctx, adapter, endpointKey, async (management) => {
60
61
  const method = management.listGroupMembers;
61
62
  if (!method) {
62
- return { error: `当前适配器(${adapter})不支持群成员列表` };
63
+ return unsupportedList('members', 'listGroupMembers', adapter, '群成员列表');
63
64
  }
64
65
  const members = [...await method.call(management, { groupId })];
65
66
  return { data: { members, count: members.length } };
66
67
  });
67
68
  }
68
69
 
70
+ /**
71
+ * A read the Adapter does not declare is not a failure (#1250): reply with an empty list plus
72
+ * `status: 'unsupported'` (`ConsoleEndpointCapabilityUnsupported`) instead of `{ error }`, so the
73
+ * Console can render "该平台不提供" rather than a red read error. Older clients still see an empty
74
+ * list with `count: 0`.
75
+ */
76
+ function unsupportedList(
77
+ key: 'friends' | 'groups' | 'channels' | 'members',
78
+ capability: EndpointManagementCapability,
79
+ adapter: string,
80
+ label: string,
81
+ ): ExtendedRpcResult {
82
+ return {
83
+ data: {
84
+ [key]: [],
85
+ count: 0,
86
+ status: 'unsupported',
87
+ capability,
88
+ message: `当前适配器(${adapter})不支持${label}`,
89
+ },
90
+ };
91
+ }
92
+
69
93
  // ---------------------------------------------------------------- 群管/好友写操作
70
94
 
71
95
  interface GroupWriteSpec {
@@ -1,8 +1,12 @@
1
1
  export type {
2
2
  ConsoleRpcExtendedCtx,
3
3
  ConsoleScheduleEngine,
4
+ ConsoleScheduleTarget,
4
5
  ConsoleWorkroomKnowledgeControlPort,
5
6
  ConsoleWorkroomProfileControlPort,
6
7
  ExtendedRpcResult,
8
+ ConsoleLoginAssistPort,
9
+ ConsoleEndpointLoginAccess,
7
10
  } from './contracts.js';
11
+ export { loginAuditDetail, LOGIN_SUBMIT_WAIT_MS } from './login-rpc.js';
8
12
  export { dispatchExtendedConsoleRpc } from './dispatcher.js';