@zhin.js/host-http 1.0.10 → 1.0.11

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 内
@@ -51,6 +53,12 @@ export async function dispatchExtendedConsoleRpc(type, data, ctx) {
51
53
  case 'request.approve':
52
54
  case 'request.reject':
53
55
  return actOnRequest(type, d, ctx);
56
+ case 'login.list':
57
+ return listLoginPending(ctx);
58
+ case 'login.submit':
59
+ return submitLogin(d, ctx);
60
+ case 'login.cancel':
61
+ return cancelLogin(d, ctx);
54
62
  case 'request.consumed':
55
63
  return markInboxConsumed(d, ctx, TABLE_REQUEST);
56
64
  case 'notice.consumed':
@@ -86,10 +94,126 @@ export async function dispatchExtendedConsoleRpc(type, data, ctx) {
86
94
  });
87
95
  case 'endpoint.delete_friend':
88
96
  return deleteFriend(d, ctx);
97
+ case 'workroom.profile.pack.publish':
98
+ case 'workroom.profile.publish':
99
+ case 'workroom.profile.rollback':
100
+ case 'workroom.profile.policy.publish':
101
+ return mutateWorkroomProfile(type, d, ctx);
102
+ case 'workroom.knowledge.get':
103
+ case 'workroom.knowledge.publish':
104
+ case 'workroom.knowledge.rollback':
105
+ return mutateWorkroomKnowledge(type, d, ctx);
89
106
  default:
90
107
  return undefined;
91
108
  }
92
109
  }
110
+ async function mutateWorkroomProfile(type, data, ctx) {
111
+ const control = ctx.workroomProfileControl;
112
+ if (!control)
113
+ return { error: 'Workroom Profile authority control is unavailable' };
114
+ for (const forbidden of [
115
+ 'authenticatedPrincipalId', 'principalId', 'decision', 'governance', 'approval', 'authority',
116
+ ]) {
117
+ if (forbidden in data)
118
+ return { error: `Console Workroom Profile request cannot carry ${forbidden}` };
119
+ }
120
+ const principal = ctx.authenticatedPrincipal;
121
+ if (!principal)
122
+ return { error: 'Workroom Profile control requires an authenticated principal' };
123
+ try {
124
+ const operationId = requiredRpcText(data.operationId, 'operationId');
125
+ if (type === 'workroom.profile.pack.publish') {
126
+ return { data: await control.publishPack(Object.freeze({ operationId, pack: requiredRpcValue(data.pack, 'pack') }), principal) };
127
+ }
128
+ const projectId = requiredRpcText(data.projectId, 'projectId');
129
+ if (type === 'workroom.profile.policy.publish') {
130
+ const expectedPreviousDigest = optionalRpcText(data.expectedPreviousDigest, 'expectedPreviousDigest');
131
+ return { data: await control.publishPlanningPolicy(Object.freeze({
132
+ operationId,
133
+ projectId,
134
+ profileRevisionId: requiredRpcText(data.profileRevisionId, 'profileRevisionId'),
135
+ revision: requiredRpcInteger(data.revision, 'revision'),
136
+ ...(expectedPreviousDigest ? { expectedPreviousDigest } : {}),
137
+ policy: requiredRpcValue(data.policy, 'policy'),
138
+ }), principal) };
139
+ }
140
+ const common = {
141
+ operationId,
142
+ projectId,
143
+ expectedRegistryRevision: requiredRpcInteger(data.expectedRegistryRevision, 'expectedRegistryRevision', -1),
144
+ overlay: requiredRpcValue(data.overlay, 'overlay'),
145
+ activate: requiredRpcBoolean(data.activate, 'activate'),
146
+ };
147
+ return type === 'workroom.profile.rollback'
148
+ ? { data: await control.publishRollback(Object.freeze({
149
+ ...common,
150
+ restoredFromRevisionId: requiredRpcText(data.restoredFromRevisionId, 'restoredFromRevisionId'),
151
+ }), principal) }
152
+ : { data: await control.publishProfile(Object.freeze(common), principal) };
153
+ }
154
+ catch (error) {
155
+ return { error: errorMessage(error) };
156
+ }
157
+ }
158
+ async function mutateWorkroomKnowledge(type, data, ctx) {
159
+ const control = ctx.workroomKnowledgeControl;
160
+ if (!control)
161
+ return { error: 'Workroom Knowledge control is unavailable' };
162
+ for (const forbidden of [
163
+ 'authenticatedPrincipalId', 'principalId', 'ownerPrincipalId', 'source', 'decision',
164
+ 'governance', 'approval', 'authority', 'body', 'content',
165
+ ]) {
166
+ if (forbidden in data)
167
+ return { error: `Console Workroom Knowledge request cannot carry ${forbidden}` };
168
+ }
169
+ try {
170
+ const projectId = requiredRpcText(data.projectId, 'projectId');
171
+ if (type === 'workroom.knowledge.get')
172
+ return { data: await control.read(projectId) };
173
+ const principal = ctx.authenticatedPrincipal;
174
+ if (!principal)
175
+ return { error: 'Workroom Knowledge mutation requires an authenticated principal' };
176
+ const common = {
177
+ operationId: requiredRpcText(data.operationId, 'operationId'), projectId,
178
+ expectedRevision: requiredRpcInteger(data.expectedRevision, 'expectedRevision', -1),
179
+ };
180
+ if (type === 'workroom.knowledge.rollback') {
181
+ return { data: await control.rollback(Object.freeze({
182
+ ...common, restoreRevision: requiredRpcInteger(data.restoreRevision, 'restoreRevision', 0),
183
+ }), principal) };
184
+ }
185
+ if (!Array.isArray(data.entries))
186
+ throw new Error('entries is invalid');
187
+ return { data: await control.publish(Object.freeze({ ...common, entries: structuredClone(data.entries) }), principal) };
188
+ }
189
+ catch (error) {
190
+ return { error: errorMessage(error) };
191
+ }
192
+ }
193
+ function requiredRpcValue(value, name) {
194
+ if (value === undefined || value === null)
195
+ throw new Error(`${name} is required`);
196
+ return structuredClone(value);
197
+ }
198
+ function requiredRpcText(value, name) {
199
+ if (typeof value !== 'string' || !value.trim() || value !== value.trim()) {
200
+ throw new Error(`${name} is invalid`);
201
+ }
202
+ return value;
203
+ }
204
+ function optionalRpcText(value, name) {
205
+ return value === undefined ? undefined : requiredRpcText(value, name);
206
+ }
207
+ function requiredRpcInteger(value, name, minimum = 1) {
208
+ if (!Number.isSafeInteger(value) || Number(value) < minimum)
209
+ throw new Error(`${name} is invalid`);
210
+ return Number(value);
211
+ }
212
+ function requiredRpcBoolean(value, name) {
213
+ if (typeof value !== 'boolean')
214
+ throw new Error(`${name} is invalid`);
215
+ return value;
216
+ }
93
217
  // ---------------------------------------------------------------- cron
94
218
  function listSchedule(ctx) {
95
219
  const host = ctx.scheduleHost;
@@ -245,12 +369,39 @@ async function listInbox(d, ctx, table, key, mapRow) {
245
369
  .map(mapRow);
246
370
  return { data: { [key]: sorted, inboxEnabled: enabled } };
247
371
  }
248
- /** request.list —— 未处理的好友/群请求(unified_inbox_request 中 resolved=0)。 */
372
+ /** request.list —— 优先 live EndpointManagement.listRequests;否则回退 unified_inbox_request。 */
249
373
  async function listPendingRequests(d, ctx) {
250
374
  const adapter = strField(d, '$adapter', 'adapter');
251
375
  const endpointKey = strField(d, '$endpoint', 'endpointKey', 'endpoint');
252
376
  if (!adapter || !endpointKey)
253
377
  return { error: '$adapter and $endpoint required' };
378
+ if (ctx.withEndpointManagement) {
379
+ try {
380
+ const live = await ctx.withEndpointManagement(adapter, endpointKey, async (management) => {
381
+ if (!management.listRequests)
382
+ return undefined;
383
+ const pending = await management.listRequests();
384
+ const requests = pending.map((row) => mapRequestRow({
385
+ platform_request_id: row.platform_request_id,
386
+ type: row.type,
387
+ scene_type: row.scene_type ?? null,
388
+ scene_id: row.scene_id,
389
+ sub_type: row.sub_type ?? null,
390
+ actor_id: row.actor_id,
391
+ actor_name: row.actor_name ?? null,
392
+ comment: row.comment ?? null,
393
+ created_at: row.created_at,
394
+ resolved: 0,
395
+ }));
396
+ return { data: { requests, inboxEnabled: false, source: 'endpoint' } };
397
+ });
398
+ if (live)
399
+ return live;
400
+ }
401
+ catch (error) {
402
+ return { error: errorMessage(error) };
403
+ }
404
+ }
254
405
  const { rows, enabled } = await readInboxRows(ctx, TABLE_REQUEST, {
255
406
  adapter,
256
407
  endpoint_id: endpointKey,
@@ -261,7 +412,7 @@ async function listPendingRequests(d, ctx) {
261
412
  .filter((row) => Number(row.resolved ?? 0) === 0)
262
413
  .sort((a, b) => Number(a.created_at ?? 0) - Number(b.created_at ?? 0))
263
414
  .map(mapRequestRow);
264
- return { data: { requests, inboxEnabled: enabled } };
415
+ return { data: { requests, inboxEnabled: enabled, source: 'inbox' } };
265
416
  }
266
417
  async function listInboxMessages(d, ctx) {
267
418
  const adapter = strField(d, '$adapter', 'adapter');
@@ -414,6 +565,50 @@ async function actOnRequest(type, d, ctx) {
414
565
  return { data: { success: true } };
415
566
  });
416
567
  }
568
+ function listLoginPending(ctx) {
569
+ const assist = ctx.loginAssist;
570
+ if (!assist)
571
+ return { error: '登录辅助未接线(LoginAssist 未挂载)' };
572
+ const tasks = assist.listPending().map((task) => ({
573
+ id: task.id,
574
+ adapter: task.adapter,
575
+ endpointKey: task.endpointKey,
576
+ type: task.type,
577
+ payload: task.payload,
578
+ createdAt: task.createdAt,
579
+ expiresAt: task.expiresAt,
580
+ }));
581
+ return { data: { tasks, count: tasks.length } };
582
+ }
583
+ function submitLogin(d, ctx) {
584
+ const assist = ctx.loginAssist;
585
+ if (!assist)
586
+ return { error: '登录辅助未接线(LoginAssist 未挂载)' };
587
+ const id = strField(d, '$id', 'id', 'taskId');
588
+ if (!id)
589
+ return { error: '$id required' };
590
+ const raw = d.$value ?? d.value ?? d.ticket ?? d.code ?? '';
591
+ const value = typeof raw === 'string' || (raw && typeof raw === 'object')
592
+ ? raw
593
+ : String(raw ?? '');
594
+ const ok = assist.submit(id, value);
595
+ if (!ok)
596
+ return { error: `login task not found: ${id}` };
597
+ return { data: { success: true } };
598
+ }
599
+ function cancelLogin(d, ctx) {
600
+ const assist = ctx.loginAssist;
601
+ if (!assist)
602
+ return { error: '登录辅助未接线(LoginAssist 未挂载)' };
603
+ const id = strField(d, '$id', 'id', 'taskId');
604
+ if (!id)
605
+ return { error: '$id required' };
606
+ const reason = strField(d, '$reason', 'reason') || 'cancelled';
607
+ const ok = assist.cancel(id, reason);
608
+ if (!ok)
609
+ return { error: `login task not found: ${id}` };
610
+ return { data: { success: true } };
611
+ }
417
612
  // ---------------------------------------------------------------- 社交读取
418
613
  async function listFriends(d, ctx) {
419
614
  const endpoint = requireEndpointAddress(d);
@@ -50,6 +50,16 @@ export type RuntimeConsoleRpcContext = {
50
50
  setConfigKey?(pluginName: string, data: unknown): Promise<{
51
51
  restartRequired: boolean;
52
52
  }>;
53
+ /** Atomic, revision-checked runtime Workroom Catalog read/write. */
54
+ readWorkroomCatalog?(): Promise<Readonly<{
55
+ agents: Readonly<Record<string, unknown>>;
56
+ workrooms: Readonly<Record<string, unknown>>;
57
+ revision: string;
58
+ }>>;
59
+ setWorkroomCatalog?(workrooms: unknown, expectedRevision: string): Promise<Readonly<{
60
+ revision: string;
61
+ restartRequired: boolean;
62
+ }>>;
53
63
  /** Project file manager (allowlisted paths under project root). */
54
64
  listProjectFiles?(): Promise<readonly unknown[]>;
55
65
  readProjectFile?(filePath: string): Promise<{
@@ -116,6 +116,41 @@ export async function dispatchRuntimeConsoleRpc(message, ctx) {
116
116
  }
117
117
  return payloads;
118
118
  }
119
+ case 'workrooms:get': {
120
+ try {
121
+ if (!ctx.readWorkroomCatalog) {
122
+ emit({ requestId, error: 'Workroom Catalog read is not configured' });
123
+ return payloads;
124
+ }
125
+ emit({ requestId, data: await ctx.readWorkroomCatalog() });
126
+ }
127
+ catch (error) {
128
+ emit({ requestId, error: error instanceof Error ? error.message : String(error) });
129
+ }
130
+ return payloads;
131
+ }
132
+ case 'workrooms:set': {
133
+ try {
134
+ if (!ctx.setWorkroomCatalog) {
135
+ emit({ requestId, error: 'Workroom Catalog write is not configured' });
136
+ return payloads;
137
+ }
138
+ const expectedRevision = typeof message.expectedRevision === 'string'
139
+ ? message.expectedRevision
140
+ : '';
141
+ if (!/^[a-f0-9]{64}$/u.test(expectedRevision)) {
142
+ emit({ requestId, error: 'expectedRevision is required' });
143
+ return payloads;
144
+ }
145
+ const result = await ctx.setWorkroomCatalog(message.data, expectedRevision);
146
+ ctx.publishEvent?.('workrooms:updated', { revision: result.revision });
147
+ emit({ requestId, data: { success: true, ...result } });
148
+ }
149
+ catch (error) {
150
+ emit({ requestId, error: error instanceof Error ? error.message : String(error) });
151
+ }
152
+ return payloads;
153
+ }
119
154
  case 'config:save-yaml': {
120
155
  try {
121
156
  const yaml = message.yaml;
@@ -1,7 +1,7 @@
1
1
  import { type IncomingMessage, type ServerResponse } from 'node:http';
2
2
  import { WebSocket } from 'ws';
3
3
  import { type GenerationAdmissionBindable } from '@zhin.js/plugin-runtime';
4
- import { TokenRegistry, type AuthScope, type ScopedTokenConfig } from './token-registry.js';
4
+ import { TokenRegistry, type AuthenticatedTokenPrincipal, type AuthScope, type ScopedTokenConfig } from './token-registry.js';
5
5
  import { type ListedRoute, type RouteMeta } from './openapi.js';
6
6
  export declare const REMOTE_CONSOLE_ORIGIN = "https://console.zhin.dev";
7
7
  export interface WsConnection {
@@ -34,7 +34,7 @@ export interface HttpHostOptions {
34
34
  /** Extra HTTP path prefixes that skip auth even under `apiBase`. */
35
35
  readonly authExemptPaths?: readonly string[];
36
36
  }
37
- export type HttpHandler = (request: IncomingMessage, response: ServerResponse, url: URL, authScope: AuthScope) => void | Promise<void>;
37
+ export type HttpHandler = (request: IncomingMessage, response: ServerResponse, url: URL, authScope: AuthScope, authenticatedPrincipal?: AuthenticatedTokenPrincipal) => void | Promise<void>;
38
38
  export interface HttpRouteRegistration {
39
39
  (): void;
40
40
  }
package/lib/http-host.js CHANGED
@@ -218,7 +218,7 @@ export function createHttpHost(options = {}) {
218
218
  return;
219
219
  }
220
220
  if (!release) {
221
- await route.handler(request, response, url, auth.scope);
221
+ await route.handler(request, response, url, auth.scope, auth.principal);
222
222
  return;
223
223
  }
224
224
  let released = false;
@@ -232,7 +232,7 @@ export function createHttpHost(options = {}) {
232
232
  };
233
233
  response.once('finish', releaseResponse);
234
234
  response.once('close', releaseResponse);
235
- await route.handler(request, response, url, auth.scope);
235
+ await route.handler(request, response, url, auth.scope, auth.principal);
236
236
  if (response.writableFinished || response.destroyed)
237
237
  releaseResponse();
238
238
  }
@@ -272,7 +272,8 @@ export function createHttpHost(options = {}) {
272
272
  if (!isDemoHttpAllowed(method, pathname, apiBase))
273
273
  return { ok: false };
274
274
  }
275
- return { ok: true, scope };
275
+ const principal = tokenRegistry.resolvePrincipal(token);
276
+ return { ok: true, scope, ...(principal ? { principal } : {}) };
276
277
  }
277
278
  function listListedRoutes() {
278
279
  return Object.freeze(httpRoutes
package/lib/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { createHttpHost, httpHostToken, REMOTE_CONSOLE_ORIGIN, type HttpHandler, type HttpHost, type ProcessHttpHost, type HttpHostAddress, type HttpHostOptions, type HttpRouteRegistration, type WsConnection, type WsHandle, } from './http-host.js';
2
- export { TokenRegistry, extractBearerToken, isDemoWebSocketPath, type AuthScope, type ScopedTokenConfig, type TokenRegistryConfig, } from './token-registry.js';
2
+ export { TokenRegistry, extractBearerToken, isDemoWebSocketPath, type AuthenticatedTokenPrincipal, type AuthScope, type ScopedTokenConfig, type TokenRegistryConfig, } from './token-registry.js';
3
3
  export { timingSafeEqualString } from './timing-safe-equal.js';
4
4
  export { createConsoleEventHub, type ConsoleEventHub, } from './console-events.js';
5
5
  export { buildOpenApiDocument, patternToOpenApiPath, routeRequiresBearerAuth, type BuildOpenApiOptions, type ListedRoute, type OpenApiParameter, type RouteMeta, } from './openapi.js';
@@ -7,4 +7,4 @@ export { HttpBodyError, readJsonBody, } from './json-body.js';
7
7
  export { dispatchRuntimeConsoleRpc, isDemoHttpAllowed, pickRpcReply, type RuntimeConsolePage, type RuntimeConsoleRpcContext, type RuntimeConsoleRpcMessage, type RuntimeConsoleRpcReply, type RuntimeEndpointSendInput, type RuntimeEndpointSummary, } from './console-rpc.js';
8
8
  export { buildProjectFileTree, FILE_MANAGER_ALLOWED, FILE_MANAGER_BLOCKED, isProjectPathAllowed, listEnvFiles, readProjectFile, saveProjectFile, type FileTreeNode, } from './project-files.js';
9
9
  export { registerConsoleRestPages, type ConsoleAgentIntrospection, type ConsoleAgentRuntime, type ConsoleAgentSessionTree, type ConsoleRestCtx, type ConsoleRestPagesOptions, } from './console-rest-pages.js';
10
- export { dispatchExtendedConsoleRpc, type ConsoleRpcExtendedCtx, type ConsoleScheduleEngine, type ExtendedRpcResult, } from './console-rpc-extended.js';
10
+ export { dispatchExtendedConsoleRpc, type ConsoleRpcExtendedCtx, type ConsoleScheduleEngine, type ConsoleWorkroomProfileControlPort, type ConsoleWorkroomKnowledgeControlPort, type ExtendedRpcResult, } from './console-rpc-extended.js';
@@ -3,7 +3,13 @@ export type AuthScope = 'full' | 'demo';
3
3
  export type ScopedTokenConfig = {
4
4
  readonly token: string;
5
5
  readonly scope: AuthScope;
6
+ /** Root-configured subject binding; never accepted from an HTTP payload. */
7
+ readonly principalId?: string;
6
8
  };
9
+ export interface AuthenticatedTokenPrincipal {
10
+ readonly principalId: string;
11
+ readonly scope: AuthScope;
12
+ }
7
13
  export type TokenRegistryConfig = {
8
14
  readonly primaryToken?: string;
9
15
  readonly scopedTokens?: readonly ScopedTokenConfig[];
@@ -12,6 +18,7 @@ export declare class TokenRegistry {
12
18
  #private;
13
19
  constructor(config?: TokenRegistryConfig);
14
20
  resolve(token: string): AuthScope | null;
21
+ resolvePrincipal(token: string): AuthenticatedTokenPrincipal | null;
15
22
  hasAnyToken(): boolean;
16
23
  primaryTokenPrefixForLog(): string;
17
24
  }
@@ -3,18 +3,39 @@ export class TokenRegistry {
3
3
  #entries = new Map();
4
4
  constructor(config = {}) {
5
5
  if (config.primaryToken)
6
- this.#entries.set(config.primaryToken, 'full');
7
- for (const { token, scope } of config.scopedTokens ?? []) {
8
- if (token)
9
- this.#entries.set(token, scope);
6
+ this.#entries.set(config.primaryToken, Object.freeze({ scope: 'full' }));
7
+ for (const { token, scope, principalId } of config.scopedTokens ?? []) {
8
+ if (!token)
9
+ continue;
10
+ if (principalId !== undefined && (!principalId.trim() || principalId !== principalId.trim())) {
11
+ throw new Error('HTTP token principalId is invalid');
12
+ }
13
+ const candidate = Object.freeze({ scope, ...(principalId === undefined ? {} : { principalId }) });
14
+ const existing = this.#entries.get(token);
15
+ if (existing && (existing.scope !== candidate.scope || existing.principalId !== candidate.principalId)) {
16
+ throw new Error('HTTP token has conflicting authority bindings');
17
+ }
18
+ this.#entries.set(token, candidate);
10
19
  }
11
20
  }
12
21
  resolve(token) {
13
22
  if (!token)
14
23
  return null;
15
- for (const [known, scope] of this.#entries) {
24
+ for (const [known, binding] of this.#entries) {
16
25
  if (timingSafeEqualString(known, token))
17
- return scope;
26
+ return binding.scope;
27
+ }
28
+ return null;
29
+ }
30
+ resolvePrincipal(token) {
31
+ if (!token)
32
+ return null;
33
+ for (const [known, binding] of this.#entries) {
34
+ if (timingSafeEqualString(known, token)) {
35
+ return binding.principalId
36
+ ? Object.freeze({ principalId: binding.principalId, scope: binding.scope })
37
+ : null;
38
+ }
18
39
  }
19
40
  return null;
20
41
  }
@@ -22,8 +43,8 @@ export class TokenRegistry {
22
43
  return this.#entries.size > 0;
23
44
  }
24
45
  primaryTokenPrefixForLog() {
25
- for (const [tok, scope] of this.#entries) {
26
- if (scope === 'full')
46
+ for (const [tok, binding] of this.#entries) {
47
+ if (binding.scope === 'full')
27
48
  return tok.slice(0, 6);
28
49
  }
29
50
  const first = this.#entries.keys().next().value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/host-http",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "description": "Minimal HTTP and WebSocket host for Plugin Runtime (no Koa)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -18,10 +18,10 @@
18
18
  ],
19
19
  "dependencies": {
20
20
  "ws": "^8.21.1",
21
- "@zhin.js/console-protocol": "1.1.2",
21
+ "@zhin.js/console-protocol": "1.1.3",
22
22
  "@zhin.js/host-http-contract": "1.0.1",
23
23
  "@zhin.js/logger": "1.0.76",
24
- "@zhin.js/plugin-runtime": "1.1.6"
24
+ "@zhin.js/plugin-runtime": "1.1.7"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^26.1.2",