@nekro-nxt/contracts 0.5.0 → 0.7.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.
@@ -6,8 +6,19 @@ import { z } from 'zod'
6
6
  * ships; never reuse a level for a different meaning. Level 7 is Manifest V7 (host instance + agent attachments); level 8 adds the asset library, image info, tool result
7
7
  * images, the search index and host-layer model calls. Level 9 adds the structured logger and explicit chat commands. Level 10 adds version lineage, the data version with its
8
8
  * migration hook and administrator-only commands. Level 11 lists the pictures of an inbound message to inbound hooks.
9
+ * Level 12 adds credential placeholders (`secrets.placeholder`) that `http.fetch` fills for the credential's domains.
10
+ * Level 13 adds outbound processing (`harness.onOutbound`, `permissions.agent.outbound`).
11
+ * Level 14 adds work files (`files`), reading Asset bytes (`assets.read`), high-detail tool images (`toolImages`),
12
+ * long-running tools (`timeoutSeconds`, `execute(args, { signal, progress })`), media processing (`media`), command
13
+ * profiles (`commands`) and speech to text (`speech`).
9
14
  */
10
- export const EXTENSION_SDK_LEVEL = 11
15
+ export const EXTENSION_SDK_LEVEL = 14
16
+
17
+ /** Work files, Asset reads, high-detail tool images, tool time limits, media, commands and speech need this level. */
18
+ export const EXTENSION_FILES_SDK_LEVEL = 14
19
+
20
+ /** Outbound processing needs this level; packages that declare `permissions.agent.outbound` declare it. */
21
+ export const EXTENSION_OUTBOUND_SDK_LEVEL = 13
11
22
 
12
23
  /** Lineage, `dataVersion` and administrator-only commands need this level; packages that carry them declare it. */
13
24
  export const EXTENSION_GOVERNANCE_SDK_LEVEL = 10
@@ -23,6 +34,9 @@ export const EXTENSION_SDK_RELEASES: readonly { readonly level: number; readonly
23
34
  { level: 9, version: '0.6.0' },
24
35
  { level: 10, version: '0.6.0' },
25
36
  { level: 11, version: '0.6.0' },
37
+ { level: 12, version: '0.7.0' },
38
+ { level: 13, version: '0.7.0' },
39
+ { level: 14, version: '0.8.0' },
26
40
  ]
27
41
 
28
42
  /** The oldest NekroNXT version that supports `level`; undefined for a level no release supports yet. */
@@ -128,6 +142,32 @@ export const ExtensionInboundHookCapabilitySchema = z
128
142
  .strict()
129
143
  export type ExtensionInboundHookCapability = z.output<typeof ExtensionInboundHookCapabilitySchema>
130
144
 
145
+ export const EXTENSION_OUTBOUND_MAX_TIMEOUT_MS = 15_000
146
+
147
+ /**
148
+ * Outbound processing of the agent's own messages. `read` sees each message after it was delivered; `transform` may
149
+ * replace its content or append messages before it is sent; `cancel` may stop it. `onTimeout` decides what happens
150
+ * when the handler times out or throws before sending: `send` (default) sends the message as it is.
151
+ */
152
+ export const ExtensionOutboundCapabilitySchema = z
153
+ .object({
154
+ read: z.literal(true).optional(),
155
+ transform: z.literal(true).optional(),
156
+ cancel: z.literal(true).optional(),
157
+ timeoutMs: z.number().int().min(100).max(EXTENSION_OUTBOUND_MAX_TIMEOUT_MS),
158
+ onTimeout: z.enum(['send', 'cancel']).optional(),
159
+ })
160
+ .strict()
161
+ .refine(
162
+ (value) => value.read === true || value.transform === true || value.cancel === true,
163
+ 'outbound 至少声明 read、transform 或 cancel 中的一项。',
164
+ )
165
+ export type ExtensionOutboundCapability = z.output<typeof ExtensionOutboundCapabilitySchema>
166
+
167
+ /** Whether the extension takes part before sending (and so in the agent's processing order). */
168
+ export const outboundProcessesBeforeSend = (outbound: ExtensionOutboundCapability | undefined): boolean =>
169
+ outbound?.transform === true || outbound?.cancel === true
170
+
131
171
  export const EXTENSION_JOB_MAX_RUNTIME = 50
132
172
 
133
173
  export const ExtensionDeclaredJobSchema = z
@@ -192,17 +232,92 @@ export type ExtensionAssetLibraryCapability = z.output<typeof ExtensionAssetLibr
192
232
 
193
233
  /**
194
234
  * `write` creates Assets in the current channel; `library` keeps Assets in the extension's own library so they can be
195
- * used in any channel later.
235
+ * used in any channel later; `read` reads the bytes of Assets the current channel can see or the library holds.
196
236
  */
197
237
  export const ExtensionAssetsCapabilitySchema = z
198
238
  .object({
199
239
  write: z.literal(true).optional(),
200
240
  library: ExtensionAssetLibraryCapabilitySchema.optional(),
241
+ read: z.literal(true).optional(),
201
242
  })
202
243
  .strict()
203
- .refine((value) => value.write !== undefined || value.library !== undefined, 'assets 至少声明 write 或 library。')
244
+ .refine(
245
+ (value) => value.write !== undefined || value.library !== undefined || value.read !== undefined,
246
+ 'assets 至少声明 write、library 或 read。',
247
+ )
204
248
  export type ExtensionAssetsCapability = z.output<typeof ExtensionAssetsCapabilitySchema>
205
249
 
250
+ export const EXTENSION_FILES_DEFAULT_TEMP_QUOTA_BYTES = 1024 * 1024 * 1024
251
+ export const EXTENSION_FILES_DEFAULT_CACHE_QUOTA_BYTES = 1024 * 1024 * 1024
252
+ export const EXTENSION_FILES_MAX_QUOTA_BYTES = 16 * 1024 * 1024 * 1024
253
+
254
+ const FilesQuotaSchema = z
255
+ .number()
256
+ .int()
257
+ .min(1024 * 1024)
258
+ .max(EXTENSION_FILES_MAX_QUOTA_BYTES)
259
+
260
+ /**
261
+ * Work files passed between services by `fileId`. Temporary files expire a day after they are written; cache entries
262
+ * stay until the cache quota is full, then the least recently used go first.
263
+ */
264
+ export const ExtensionFilesCapabilitySchema = z
265
+ .object({ tempQuotaBytes: FilesQuotaSchema.optional(), cacheQuotaBytes: FilesQuotaSchema.optional() })
266
+ .strict()
267
+ export type ExtensionFilesCapability = z.output<typeof ExtensionFilesCapabilitySchema>
268
+
269
+ /** Tool result pictures with `detail: 'high'` reach the model at up to 2048 px on the long side. */
270
+ export const ExtensionToolImagesCapabilitySchema = z.object({ highDetail: z.literal(true) }).strict()
271
+ export type ExtensionToolImagesCapability = z.output<typeof ExtensionToolImagesCapabilitySchema>
272
+
273
+ /**
274
+ * Add-ons (附加功能) whose capability an extension can use. The ids match `AddonIdSchema` in host-api; they are
275
+ * repeated here so manifests validate without it.
276
+ */
277
+ export type ExtensionAddonId = 'media' | 'speech'
278
+
279
+ /**
280
+ * Command profiles extensions may run (`nxt.commands`, 附加功能 F7), and the add-on that provides each program. A new
281
+ * program needs a profile on the server and a row here.
282
+ */
283
+ export const ExtensionCommandProfileIdSchema = z.enum(['ffmpeg', 'ffprobe'], {
284
+ message: '命令档案只能是 ffmpeg、ffprobe。',
285
+ })
286
+ export type ExtensionCommandProfileId = z.output<typeof ExtensionCommandProfileIdSchema>
287
+
288
+ export const EXTENSION_COMMAND_PROFILES: Readonly<Record<ExtensionCommandProfileId, ExtensionAddonId>> = {
289
+ ffmpeg: 'media',
290
+ ffprobe: 'media',
291
+ }
292
+
293
+ /** Programs run through their command profiles: confined to the call's own files, without network. */
294
+ export const ExtensionCommandsCapabilitySchema = z
295
+ .object({
296
+ profiles: z
297
+ .array(ExtensionCommandProfileIdSchema)
298
+ .min(1)
299
+ .max(ExtensionCommandProfileIdSchema.options.length)
300
+ .refine((value) => new Set(value).size === value.length, '命令档案不能重复。'),
301
+ })
302
+ .strict()
303
+ export type ExtensionCommandsCapability = z.output<typeof ExtensionCommandsCapabilitySchema>
304
+
305
+ export const EXTENSION_SPEECH_MAX_CALLS_PER_MINUTE = 60
306
+ export const EXTENSION_SPEECH_DEFAULT_CALLS_PER_MINUTE = 10
307
+
308
+ /** Speech to text with the provider chosen in 设置 → 附加功能, which may be a cloud service. */
309
+ export const ExtensionSpeechCapabilitySchema = z
310
+ .object({
311
+ maxCallsPerMinute: z
312
+ .number()
313
+ .int()
314
+ .min(1)
315
+ .max(EXTENSION_SPEECH_MAX_CALLS_PER_MINUTE)
316
+ .default(EXTENSION_SPEECH_DEFAULT_CALLS_PER_MINUTE),
317
+ })
318
+ .strict()
319
+ export type ExtensionSpeechCapability = z.output<typeof ExtensionSpeechCapabilitySchema>
320
+
206
321
  const IndexNameSchema = z
207
322
  .string()
208
323
  .trim()
@@ -255,10 +370,14 @@ export const ExtensionModelsCapabilitySchema = z
255
370
  .strict()
256
371
  export type ExtensionModelsCapability = z.output<typeof ExtensionModelsCapabilitySchema>
257
372
 
258
- /** A header or environment value: literal text, or the current value of a `meta.role: 'secret'` config field. */
373
+ /**
374
+ * A header or environment value: literal text, the current value of a `meta.role: 'secret'` config field, or a named
375
+ * credential (by id or name) that the agent may use.
376
+ */
259
377
  export const ExtensionMcpValueSchema = z.union([
260
378
  z.string().max(4096),
261
379
  z.object({ secret: z.string().trim().min(1).max(64) }).strict(),
380
+ z.object({ credential: z.string().trim().min(1).max(64) }).strict(),
262
381
  ])
263
382
  export type ExtensionMcpValue = z.output<typeof ExtensionMcpValueSchema>
264
383
 
@@ -315,6 +434,8 @@ const McpFormValueSchema = z
315
434
  name: z.string().regex(/^[A-Za-z_][A-Za-z0-9_-]{0,127}$/u, '名称只能包含字母、数字、下划线和连字符。'),
316
435
  value: z.string().max(4096),
317
436
  secret: z.boolean().default(false),
437
+ /** Takes the value from this named credential instead; `value` and `secret` are then ignored. */
438
+ credential: z.string().trim().min(1).max(64).optional(),
318
439
  })
319
440
  .strict()
320
441
 
@@ -344,7 +465,7 @@ export const mcpSecretFields = (mcp: ExtensionMcpCapability | undefined): readon
344
465
  ...new Set(
345
466
  (mcp?.servers ?? []).flatMap((server) =>
346
467
  Object.values(server.transport === 'stdio' ? server.env : server.headers).flatMap((value) =>
347
- typeof value === 'string' ? [] : [value.secret],
468
+ typeof value === 'string' || !('secret' in value) ? [] : [value.secret],
348
469
  ),
349
470
  ),
350
471
  ),
@@ -365,6 +486,12 @@ export const ExtensionCapabilitiesSchema = z
365
486
  network: ExtensionNetworkCapabilitySchema.optional(),
366
487
  storage: ExtensionStorageCapabilitySchema.optional(),
367
488
  assets: ExtensionAssetsCapabilitySchema.optional(),
489
+ files: ExtensionFilesCapabilitySchema.optional(),
490
+ toolImages: ExtensionToolImagesCapabilitySchema.optional(),
491
+ /** `nxt.media`: probe, frames, audio and convert on the media add-on. */
492
+ media: z.literal(true).optional(),
493
+ commands: ExtensionCommandsCapabilitySchema.optional(),
494
+ speech: ExtensionSpeechCapabilitySchema.optional(),
368
495
  index: ExtensionIndexCapabilitySchema.optional(),
369
496
  /** Host layer only; the agent layer uses `llm`, which runs on the agent's own model. */
370
497
  models: ExtensionModelsCapabilitySchema.optional(),
@@ -373,6 +500,7 @@ export const ExtensionCapabilitiesSchema = z
373
500
  .strict()
374
501
  .optional(),
375
502
  inboundHook: ExtensionInboundHookCapabilitySchema.optional(),
503
+ outbound: ExtensionOutboundCapabilitySchema.optional(),
376
504
  jobs: ExtensionJobsCapabilitySchema.optional(),
377
505
  platform: ExtensionPlatformCapabilitySchema.optional(),
378
506
  llm: ExtensionLlmCapabilitySchema.optional(),
@@ -397,8 +525,19 @@ export const HostLayerCapabilitiesSchema = z
397
525
  .object({ quotaBytes: z.number().int().min(1024).max(EXTENSION_STORAGE_MAX_QUOTA_BYTES).optional() })
398
526
  .strict()
399
527
  .optional(),
400
- /** The same library agent attachments use; the host instance lists and imports into it. */
401
- assets: z.object({ library: ExtensionAssetLibraryCapabilitySchema }).strict().optional(),
528
+ /**
529
+ * The same library agent attachments use; the host instance lists and imports into it. `read` reads the bytes of
530
+ * library Assets.
531
+ */
532
+ assets: z
533
+ .object({ library: ExtensionAssetLibraryCapabilitySchema.optional(), read: z.literal(true).optional() })
534
+ .strict()
535
+ .refine((value) => value.library !== undefined || value.read !== undefined, 'assets 至少声明 library 或 read。')
536
+ .optional(),
537
+ files: ExtensionFilesCapabilitySchema.optional(),
538
+ media: z.literal(true).optional(),
539
+ commands: ExtensionCommandsCapabilitySchema.optional(),
540
+ speech: ExtensionSpeechCapabilitySchema.optional(),
402
541
  /** The same collections agent attachments declare; pages search and maintain them. */
403
542
  index: ExtensionIndexCapabilitySchema.optional(),
404
543
  models: ExtensionModelsCapabilitySchema.optional(),
@@ -420,7 +559,18 @@ export const hostLayerAsCapabilities = (host: HostLayerCapabilities | undefined)
420
559
  ...(host.storage.quotaBytes === undefined ? {} : { quotaBytes: host.storage.quotaBytes }),
421
560
  },
422
561
  }),
423
- ...(host.assets === undefined ? {} : { assets: { library: host.assets.library } }),
562
+ ...(host.assets === undefined
563
+ ? {}
564
+ : {
565
+ assets: {
566
+ ...(host.assets.library === undefined ? {} : { library: host.assets.library }),
567
+ ...(host.assets.read === undefined ? {} : { read: host.assets.read }),
568
+ },
569
+ }),
570
+ ...(host.files === undefined ? {} : { files: host.files }),
571
+ ...(host.media === undefined ? {} : { media: host.media }),
572
+ ...(host.commands === undefined ? {} : { commands: host.commands }),
573
+ ...(host.speech === undefined ? {} : { speech: host.speech }),
424
574
  ...(host.index === undefined ? {} : { index: host.index }),
425
575
  ...(host.models === undefined ? {} : { models: host.models }),
426
576
  }
@@ -459,6 +609,19 @@ export const extensionCapabilitiesExpand = (
459
609
  if (next.storage?.scopes.some((scope) => !(previous?.storage?.scopes ?? []).includes(scope))) return true
460
610
  if (next.assets?.write !== undefined && previous?.assets?.write === undefined) return true
461
611
  if (next.assets?.library !== undefined && previous?.assets?.library === undefined) return true
612
+ if (next.assets?.read !== undefined && previous?.assets?.read === undefined) return true
613
+ if (next.files !== undefined && previous?.files === undefined) return true
614
+ if (next.toolImages !== undefined && previous?.toolImages === undefined) return true
615
+ if (next.media !== undefined && previous?.media === undefined) return true
616
+ if ((next.commands?.profiles ?? []).some((profile) => !(previous?.commands?.profiles ?? []).includes(profile))) {
617
+ return true
618
+ }
619
+ if (
620
+ next.speech !== undefined &&
621
+ (previous?.speech === undefined || next.speech.maxCallsPerMinute > previous.speech.maxCallsPerMinute)
622
+ ) {
623
+ return true
624
+ }
462
625
  if (
463
626
  next.models !== undefined &&
464
627
  (previous?.models === undefined ||
@@ -484,6 +647,18 @@ export const extensionCapabilitiesExpand = (
484
647
  ) {
485
648
  return true
486
649
  }
650
+ const outbound = next.outbound
651
+ const previousOutbound = previous?.outbound
652
+ if (
653
+ outbound !== undefined &&
654
+ (previousOutbound === undefined ||
655
+ (outbound.read === true && previousOutbound.read !== true) ||
656
+ (outbound.transform === true && previousOutbound.transform !== true) ||
657
+ (outbound.cancel === true && previousOutbound.cancel !== true) ||
658
+ (outbound.onTimeout === 'cancel' && previousOutbound.onTimeout !== 'cancel'))
659
+ ) {
660
+ return true
661
+ }
487
662
  if (
488
663
  next.jobs?.runtime !== undefined &&
489
664
  (previous?.jobs?.runtime === undefined || next.jobs.runtime.maxActive > previous.jobs.runtime.maxActive)
@@ -516,6 +691,25 @@ export interface ExtensionCapabilitySummaryItem {
516
691
  readonly risk: ExtensionCapabilityRisk
517
692
  readonly label: string
518
693
  readonly detail?: string
694
+ /** For a high-risk capability with limits: what it stays within, shown where the user accepts the risk. */
695
+ readonly scope?: string
696
+ }
697
+
698
+ /**
699
+ * Add-ons the declared capabilities need (附加功能 F13), in a stable order: `media` needs 媒体处理, `speech` needs
700
+ * 语音识别, and each command profile needs the add-on that provides its program. Pass both layers, host-layer ones
701
+ * through {@link hostLayerAsCapabilities}.
702
+ */
703
+ export const requiredExtensionAddons = (
704
+ ...layers: readonly (ExtensionCapabilities | undefined)[]
705
+ ): readonly ExtensionAddonId[] => {
706
+ const needed = new Set<ExtensionAddonId>()
707
+ for (const capabilities of layers) {
708
+ if (capabilities?.media !== undefined) needed.add('media')
709
+ if (capabilities?.speech !== undefined) needed.add('speech')
710
+ for (const profile of capabilities?.commands?.profiles ?? []) needed.add(EXTENSION_COMMAND_PROFILES[profile])
711
+ }
712
+ return (['media', 'speech'] as const).filter((id) => needed.has(id))
519
713
  }
520
714
 
521
715
  /**
@@ -565,6 +759,49 @@ export const summarizeExtensionCapabilities = (
565
759
  detail: `最多 ${formatLibraryQuota(capabilities.assets.library.quotaBytes ?? EXTENSION_ASSET_LIBRARY_DEFAULT_QUOTA_BYTES)}`,
566
760
  })
567
761
  }
762
+ if (capabilities.assets?.read !== undefined) {
763
+ items.push(
764
+ options.layer === 'host'
765
+ ? { key: 'assets.read', risk: 'normal', label: '读取扩展资源库里的文件' }
766
+ : { key: 'assets.read', risk: 'sensitive', label: '读取当前频道里的图片和文件' },
767
+ )
768
+ }
769
+ if (capabilities.files !== undefined) {
770
+ items.push({
771
+ key: 'files',
772
+ risk: 'normal',
773
+ label: '在本机存放临时文件和缓存',
774
+ detail: `临时文件最多 ${formatLibraryQuota(capabilities.files.tempQuotaBytes ?? EXTENSION_FILES_DEFAULT_TEMP_QUOTA_BYTES)},缓存最多 ${formatLibraryQuota(capabilities.files.cacheQuotaBytes ?? EXTENSION_FILES_DEFAULT_CACHE_QUOTA_BYTES)}`,
775
+ })
776
+ }
777
+ if (capabilities.toolImages !== undefined) {
778
+ items.push({
779
+ key: 'toolImages',
780
+ risk: 'normal',
781
+ label: '以高细节向智能体展示图片',
782
+ detail: '计入智能体用量',
783
+ })
784
+ }
785
+ if (capabilities.media !== undefined) {
786
+ items.push({ key: 'media', risk: 'normal', label: '处理音频、视频和图片' })
787
+ }
788
+ if (capabilities.commands !== undefined) {
789
+ items.push({
790
+ key: 'commands',
791
+ risk: 'high',
792
+ label: '在本机运行程序',
793
+ detail: capabilities.commands.profiles.join('、'),
794
+ scope: '只能读写交给它的文件,不能联网',
795
+ })
796
+ }
797
+ if (capabilities.speech !== undefined) {
798
+ items.push({
799
+ key: 'speech',
800
+ risk: 'sensitive',
801
+ label: '把音频转成文字',
802
+ detail: `音频可能发给「设置 → 附加功能」里选择的语音识别服务,每分钟最多 ${capabilities.speech.maxCallsPerMinute} 次`,
803
+ })
804
+ }
568
805
  if (capabilities.index !== undefined) {
569
806
  items.push({
570
807
  key: 'index',
@@ -598,6 +835,21 @@ export const summarizeExtensionCapabilities = (
598
835
  ...(effects === '' ? {} : { detail: `可以${effects}` }),
599
836
  })
600
837
  }
838
+ const outbound = capabilities.outbound
839
+ if (outbound !== undefined) {
840
+ if (outbound.transform === true) {
841
+ items.push({ key: 'outbound.transform', risk: 'high', label: '在智能体的消息发出前修改内容或追加消息' })
842
+ }
843
+ if (outbound.cancel === true) {
844
+ items.push({ key: 'outbound.cancel', risk: 'high', label: '取消智能体要发出的消息' })
845
+ }
846
+ if (outbound.read === true) {
847
+ items.push({ key: 'outbound.read', risk: 'sensitive', label: '查看智能体发出的每条消息' })
848
+ }
849
+ if (outbound.onTimeout === 'cancel' && (outbound.transform === true || outbound.cancel === true)) {
850
+ items.push({ key: 'outbound.onTimeout', risk: 'high', label: '这个扩展出错时,智能体的消息会发不出去' })
851
+ }
852
+ }
601
853
  const jobs = capabilities.jobs
602
854
  if (jobs?.declared !== undefined && jobs.declared.length > 0) {
603
855
  items.push({
@@ -4,6 +4,8 @@ import {
4
4
  HostLayerCapabilitiesSchema,
5
5
  hostLayerAsCapabilities,
6
6
  EXTENSION_SDK_BASE_LEVEL,
7
+ EXTENSION_FILES_SDK_LEVEL,
8
+ EXTENSION_OUTBOUND_SDK_LEVEL,
7
9
  } from './extension-capabilities.js'
8
10
  import { ExtensionIdSchema, ExtensionRevisionIdSchema, HostUiPageInstanceIdSchema } from './domain.js'
9
11
  import { ConfigSchemaDocumentSchema } from './config-schema.js'
@@ -548,12 +550,25 @@ export type ExtensionDataHookName = keyof typeof EXTENSION_DATA_HOOK_PERMISSIONS
548
550
  export const requiredExtensionSdkLevel = (
549
551
  permissions: ExtensionPermissions | undefined,
550
552
  ): { readonly level: number; readonly capabilities: readonly string[] } => {
553
+ const level14 = [
554
+ permissions?.host?.files === undefined && permissions?.agent?.files === undefined ? undefined : '工作文件',
555
+ permissions?.host?.assets?.read === undefined && permissions?.agent?.assets?.read === undefined
556
+ ? undefined
557
+ : '读取频道文件',
558
+ permissions?.agent?.toolImages === undefined ? undefined : '高细节图片',
559
+ permissions?.host?.media === undefined && permissions?.agent?.media === undefined ? undefined : '媒体处理',
560
+ permissions?.host?.commands === undefined && permissions?.agent?.commands === undefined ? undefined : '运行程序',
561
+ permissions?.host?.speech === undefined && permissions?.agent?.speech === undefined ? undefined : '语音识别',
562
+ ].filter((name): name is string => name !== undefined)
563
+ if (level14.length > 0) return { level: EXTENSION_FILES_SDK_LEVEL, capabilities: level14 }
551
564
  const level8 = [
552
- permissions?.host?.assets === undefined ? undefined : '扩展资源库',
565
+ permissions?.host?.assets?.library === undefined ? undefined : '扩展资源库',
553
566
  permissions?.agent?.assets?.library === undefined ? undefined : '扩展资源库',
554
567
  permissions?.host?.index === undefined && permissions?.agent?.index === undefined ? undefined : '检索索引',
555
568
  permissions?.host?.models === undefined ? undefined : '本机层模型调用',
556
569
  ].filter((name): name is string => name !== undefined)
570
+ if (permissions?.agent?.outbound !== undefined)
571
+ return { level: EXTENSION_OUTBOUND_SDK_LEVEL, capabilities: ['出站处理'] }
557
572
  return level8.length > 0
558
573
  ? { level: 8, capabilities: [...new Set(level8)] }
559
574
  : { level: EXTENSION_SDK_BASE_LEVEL, capabilities: [] }