@nekro-nxt/contracts 0.0.0-stage → 0.2.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.
- package/LICENSE +21 -0
- package/README.md +6 -2
- package/dist/index.mjs +4694 -0
- package/dist/index.mjs.map +1 -0
- package/lib/types/community.d.ts +583 -0
- package/lib/types/community.d.ts.map +1 -0
- package/lib/types/config-schema.d.ts +133 -0
- package/lib/types/config-schema.d.ts.map +1 -0
- package/lib/types/domain.d.ts +291 -0
- package/lib/types/domain.d.ts.map +1 -0
- package/lib/types/extension-capabilities.d.ts +277 -0
- package/lib/types/extension-capabilities.d.ts.map +1 -0
- package/lib/types/extension-ui.d.ts +789 -0
- package/lib/types/extension-ui.d.ts.map +1 -0
- package/lib/types/host-api.d.ts +9495 -0
- package/lib/types/host-api.d.ts.map +1 -0
- package/lib/types/index.d.ts +9 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/management-api.d.ts +108 -0
- package/lib/types/management-api.d.ts.map +1 -0
- package/lib/types/runtime-compatibility.d.ts +72 -0
- package/lib/types/runtime-compatibility.d.ts.map +1 -0
- package/package.json +40 -3
- package/src/community.ts +268 -0
- package/src/config-schema.ts +351 -0
- package/src/domain.ts +365 -0
- package/src/extension-capabilities.ts +473 -0
- package/src/extension-ui.ts +433 -0
- package/src/host-api.ts +3468 -0
- package/src/index.ts +8 -0
- package/src/management-api.ts +168 -0
- package/src/runtime-compatibility.ts +28 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Host capability level this build implements. A Revision that declares `requires.sdk` above it is rejected at
|
|
5
|
+
* import with an "upgrade NekroNXT" message instead of an opaque schema error. Bump only when a new optional
|
|
6
|
+
* Manifest capability ships; never reuse a level for a different meaning.
|
|
7
|
+
*/
|
|
8
|
+
export const EXTENSION_SDK_LEVEL = 6
|
|
9
|
+
|
|
10
|
+
export const ExtensionRequiresSchema = z.object({ sdk: z.number().int().min(1).max(1000) }).strict()
|
|
11
|
+
export type ExtensionRequires = z.output<typeof ExtensionRequiresSchema>
|
|
12
|
+
|
|
13
|
+
/** `api.example.com` or `*.example.com` (subdomains only); lower-case DNS labels, no scheme, port or path. */
|
|
14
|
+
export const ExtensionNetworkDomainSchema = z
|
|
15
|
+
.string()
|
|
16
|
+
.trim()
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.max(253)
|
|
19
|
+
.regex(/^(\*\.)?(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/u, {
|
|
20
|
+
message: '网络域名必须是 api.example.com 或 *.example.com 形式,不含协议、端口和路径。',
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export const ExtensionNetworkCapabilitySchema = z.discriminatedUnion('mode', [
|
|
24
|
+
z
|
|
25
|
+
.object({
|
|
26
|
+
mode: z.literal('domains'),
|
|
27
|
+
domains: z.array(ExtensionNetworkDomainSchema).min(1).max(32),
|
|
28
|
+
})
|
|
29
|
+
.strict(),
|
|
30
|
+
z
|
|
31
|
+
.object({
|
|
32
|
+
mode: z.literal('config'),
|
|
33
|
+
/** Top-level config fields whose current value is a URL or host the user typed. */
|
|
34
|
+
fields: z.array(z.string().trim().min(1).max(64)).min(1).max(8),
|
|
35
|
+
})
|
|
36
|
+
.strict(),
|
|
37
|
+
z
|
|
38
|
+
.object({
|
|
39
|
+
mode: z.literal('unrestricted'),
|
|
40
|
+
/** Shown to the user next to the risk acknowledgement. */
|
|
41
|
+
purpose: z.string().trim().min(4).max(200),
|
|
42
|
+
})
|
|
43
|
+
.strict(),
|
|
44
|
+
])
|
|
45
|
+
export type ExtensionNetworkCapability = z.output<typeof ExtensionNetworkCapabilitySchema>
|
|
46
|
+
|
|
47
|
+
export const EXTENSION_STORAGE_SCOPES = ['agent', 'channel', 'member', 'shared'] as const
|
|
48
|
+
export const ExtensionStorageScopeSchema = z.enum(EXTENSION_STORAGE_SCOPES)
|
|
49
|
+
export type ExtensionStorageScope = z.output<typeof ExtensionStorageScopeSchema>
|
|
50
|
+
|
|
51
|
+
export const EXTENSION_STORAGE_DEFAULT_QUOTA_BYTES = 8 * 1024 * 1024
|
|
52
|
+
export const EXTENSION_STORAGE_MAX_QUOTA_BYTES = 64 * 1024 * 1024
|
|
53
|
+
|
|
54
|
+
export const ExtensionStorageCapabilitySchema = z
|
|
55
|
+
.object({
|
|
56
|
+
scopes: z.array(ExtensionStorageScopeSchema).min(1).max(EXTENSION_STORAGE_SCOPES.length),
|
|
57
|
+
quotaBytes: z.number().int().min(1024).max(EXTENSION_STORAGE_MAX_QUOTA_BYTES).optional(),
|
|
58
|
+
})
|
|
59
|
+
.strict()
|
|
60
|
+
.refine((value) => new Set(value.scopes).size === value.scopes.length, '存储作用域不能重复。')
|
|
61
|
+
export type ExtensionStorageCapability = z.output<typeof ExtensionStorageCapabilitySchema>
|
|
62
|
+
|
|
63
|
+
export const EXTENSION_CONTEXT_STATIC_MAX_CHARS = 4000
|
|
64
|
+
export const EXTENSION_CONTEXT_DYNAMIC_MAX_CHARS = 2000
|
|
65
|
+
|
|
66
|
+
export const ExtensionContextContributionSchema = z
|
|
67
|
+
.object({
|
|
68
|
+
name: z
|
|
69
|
+
.string()
|
|
70
|
+
.trim()
|
|
71
|
+
.regex(/^[a-z0-9][a-z0-9-]{0,39}$/u, '上下文名称只能使用小写字母、数字和连字符。'),
|
|
72
|
+
kind: z.enum(['static', 'dynamic']),
|
|
73
|
+
maxChars: z.number().int().min(1).max(EXTENSION_CONTEXT_STATIC_MAX_CHARS),
|
|
74
|
+
})
|
|
75
|
+
.strict()
|
|
76
|
+
.refine(
|
|
77
|
+
(value) => value.kind === 'static' || value.maxChars <= EXTENSION_CONTEXT_DYNAMIC_MAX_CHARS,
|
|
78
|
+
`动态上下文最多 ${EXTENSION_CONTEXT_DYNAMIC_MAX_CHARS} 字。`,
|
|
79
|
+
)
|
|
80
|
+
export type ExtensionContextContribution = z.output<typeof ExtensionContextContributionSchema>
|
|
81
|
+
|
|
82
|
+
export const EXTENSION_LLM_MAX_CALLS_PER_TURN = 20
|
|
83
|
+
export const EXTENSION_LLM_MAX_OUTPUT_TOKENS = 8192
|
|
84
|
+
|
|
85
|
+
export const ExtensionLlmCapabilitySchema = z
|
|
86
|
+
.object({
|
|
87
|
+
maxCallsPerTurn: z.number().int().min(1).max(EXTENSION_LLM_MAX_CALLS_PER_TURN),
|
|
88
|
+
maxOutputTokens: z.number().int().min(64).max(EXTENSION_LLM_MAX_OUTPUT_TOKENS),
|
|
89
|
+
})
|
|
90
|
+
.strict()
|
|
91
|
+
export type ExtensionLlmCapability = z.output<typeof ExtensionLlmCapabilitySchema>
|
|
92
|
+
|
|
93
|
+
export const EXTENSION_INBOUND_HOOK_MAX_TIMEOUT_MS = 15_000
|
|
94
|
+
|
|
95
|
+
export const ExtensionInboundHookCapabilitySchema = z
|
|
96
|
+
.object({
|
|
97
|
+
/** `triggered`: only messages that would wake the agent anyway; `all`: every message in bound channels. */
|
|
98
|
+
reads: z.enum(['triggered', 'all']),
|
|
99
|
+
mayHide: z.boolean(),
|
|
100
|
+
mayForceTrigger: z.boolean(),
|
|
101
|
+
timeoutMs: z.number().int().min(100).max(EXTENSION_INBOUND_HOOK_MAX_TIMEOUT_MS),
|
|
102
|
+
})
|
|
103
|
+
.strict()
|
|
104
|
+
export type ExtensionInboundHookCapability = z.output<typeof ExtensionInboundHookCapabilitySchema>
|
|
105
|
+
|
|
106
|
+
export const EXTENSION_JOB_MAX_RUNTIME = 50
|
|
107
|
+
|
|
108
|
+
export const ExtensionDeclaredJobSchema = z
|
|
109
|
+
.object({
|
|
110
|
+
id: z
|
|
111
|
+
.string()
|
|
112
|
+
.trim()
|
|
113
|
+
.regex(/^[a-z0-9][a-z0-9-]{0,39}$/u, '定时任务 id 只能使用小写字母、数字和连字符。'),
|
|
114
|
+
label: z.string().trim().min(1).max(200),
|
|
115
|
+
cron: z.string().trim().min(9).max(120),
|
|
116
|
+
timezone: z.string().trim().min(1).max(64).optional(),
|
|
117
|
+
})
|
|
118
|
+
.strict()
|
|
119
|
+
export type ExtensionDeclaredJob = z.output<typeof ExtensionDeclaredJobSchema>
|
|
120
|
+
|
|
121
|
+
export const ExtensionJobsCapabilitySchema = z
|
|
122
|
+
.object({
|
|
123
|
+
/** Fixed schedules; each fires in every channel bound to the agent that enables the extension. */
|
|
124
|
+
declared: z
|
|
125
|
+
.array(ExtensionDeclaredJobSchema)
|
|
126
|
+
.max(8)
|
|
127
|
+
.refine((jobs) => new Set(jobs.map(({ id }) => id)).size === jobs.length, '定时任务 id 不能重复。')
|
|
128
|
+
.optional(),
|
|
129
|
+
/** Jobs the extension creates at runtime (`ctx.nxt.jobs.schedule`), at most this many active per agent. */
|
|
130
|
+
runtime: z
|
|
131
|
+
.object({ maxActive: z.number().int().min(1).max(EXTENSION_JOB_MAX_RUNTIME) })
|
|
132
|
+
.strict()
|
|
133
|
+
.optional(),
|
|
134
|
+
})
|
|
135
|
+
.strict()
|
|
136
|
+
export type ExtensionJobsCapability = z.output<typeof ExtensionJobsCapabilitySchema>
|
|
137
|
+
|
|
138
|
+
const AdapterKeySchema = z.string().trim().min(1).max(64)
|
|
139
|
+
|
|
140
|
+
export const ExtensionPlatformCapabilitySchema = z
|
|
141
|
+
.object({
|
|
142
|
+
/** Typed actions an Adapter declares, as `{ adapter, action }`. */
|
|
143
|
+
actions: z
|
|
144
|
+
.array(z.object({ adapter: AdapterKeySchema, action: z.string().regex(/^[a-z0-9_]{1,64}$/u) }).strict())
|
|
145
|
+
.max(32)
|
|
146
|
+
.default([]),
|
|
147
|
+
/** Adapters whose raw API passthrough the extension may call. */
|
|
148
|
+
raw: z.array(AdapterKeySchema).max(8).default([]),
|
|
149
|
+
})
|
|
150
|
+
.strict()
|
|
151
|
+
export type ExtensionPlatformCapability = z.output<typeof ExtensionPlatformCapabilitySchema>
|
|
152
|
+
|
|
153
|
+
/** A header or environment value: literal text, or the current value of a `meta.role: 'secret'` config field. */
|
|
154
|
+
export const ExtensionMcpValueSchema = z.union([
|
|
155
|
+
z.string().max(4096),
|
|
156
|
+
z.object({ secret: z.string().trim().min(1).max(64) }).strict(),
|
|
157
|
+
])
|
|
158
|
+
export type ExtensionMcpValue = z.output<typeof ExtensionMcpValueSchema>
|
|
159
|
+
|
|
160
|
+
/** Becomes the model-facing tool prefix `mcp__<name>__`. */
|
|
161
|
+
export const ExtensionMcpServerNameSchema = z.string().regex(/^[A-Za-z0-9_-]{1,32}$/u)
|
|
162
|
+
|
|
163
|
+
const McpValueMapSchema = z
|
|
164
|
+
.record(z.string().regex(/^[A-Za-z_][A-Za-z0-9_-]{0,127}$/u), ExtensionMcpValueSchema)
|
|
165
|
+
.refine((value) => Object.keys(value).length <= 32, '最多 32 项。')
|
|
166
|
+
.default({})
|
|
167
|
+
|
|
168
|
+
export const ExtensionMcpServerSchema = z.discriminatedUnion('transport', [
|
|
169
|
+
z
|
|
170
|
+
.object({
|
|
171
|
+
transport: z.literal('streamable-http'),
|
|
172
|
+
name: ExtensionMcpServerNameSchema,
|
|
173
|
+
url: z
|
|
174
|
+
.string()
|
|
175
|
+
.url()
|
|
176
|
+
.refine((value) => /^https?:\/\//iu.test(value), 'MCP 地址必须是 http 或 https。'),
|
|
177
|
+
headers: McpValueMapSchema,
|
|
178
|
+
})
|
|
179
|
+
.strict(),
|
|
180
|
+
z
|
|
181
|
+
.object({
|
|
182
|
+
transport: z.literal('stdio'),
|
|
183
|
+
name: ExtensionMcpServerNameSchema,
|
|
184
|
+
command: z.string().trim().min(1).max(512),
|
|
185
|
+
args: z.array(z.string().max(4096)).max(64).default([]),
|
|
186
|
+
env: McpValueMapSchema,
|
|
187
|
+
})
|
|
188
|
+
.strict(),
|
|
189
|
+
])
|
|
190
|
+
export type ExtensionMcpServer = z.output<typeof ExtensionMcpServerSchema>
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* MCP servers the Host connects for the agent while the extension is enabled; their tools appear as
|
|
194
|
+
* `mcp__<name>__<tool>`. `stdio` runs a program on the Host and is only created from the administrator's form.
|
|
195
|
+
*/
|
|
196
|
+
export const ExtensionMcpCapabilitySchema = z
|
|
197
|
+
.object({
|
|
198
|
+
servers: z
|
|
199
|
+
.array(ExtensionMcpServerSchema)
|
|
200
|
+
.min(1)
|
|
201
|
+
.max(8)
|
|
202
|
+
.refine((value) => new Set(value.map(({ name }) => name)).size === value.length, 'MCP 服务名称不能重复。'),
|
|
203
|
+
})
|
|
204
|
+
.strict()
|
|
205
|
+
export type ExtensionMcpCapability = z.output<typeof ExtensionMcpCapabilitySchema>
|
|
206
|
+
|
|
207
|
+
/** One header or environment row of the administrator's form; secret rows become credential config fields. */
|
|
208
|
+
const McpFormValueSchema = z
|
|
209
|
+
.object({
|
|
210
|
+
name: z.string().regex(/^[A-Za-z_][A-Za-z0-9_-]{0,127}$/u, '名称只能包含字母、数字、下划线和连字符。'),
|
|
211
|
+
value: z.string().max(4096),
|
|
212
|
+
secret: z.boolean().default(false),
|
|
213
|
+
})
|
|
214
|
+
.strict()
|
|
215
|
+
|
|
216
|
+
export const McpServerFormSchema = z.discriminatedUnion('transport', [
|
|
217
|
+
z
|
|
218
|
+
.object({
|
|
219
|
+
transport: z.literal('streamable-http'),
|
|
220
|
+
name: ExtensionMcpServerNameSchema,
|
|
221
|
+
url: z.string().url(),
|
|
222
|
+
headers: z.array(McpFormValueSchema).max(32).default([]),
|
|
223
|
+
})
|
|
224
|
+
.strict(),
|
|
225
|
+
z
|
|
226
|
+
.object({
|
|
227
|
+
transport: z.literal('stdio'),
|
|
228
|
+
name: ExtensionMcpServerNameSchema,
|
|
229
|
+
command: z.string().trim().min(1).max(512),
|
|
230
|
+
args: z.array(z.string().max(4096)).max(64).default([]),
|
|
231
|
+
env: z.array(McpFormValueSchema).max(32).default([]),
|
|
232
|
+
})
|
|
233
|
+
.strict(),
|
|
234
|
+
])
|
|
235
|
+
export type McpServerForm = z.output<typeof McpServerFormSchema>
|
|
236
|
+
|
|
237
|
+
/** Secret config fields an MCP declaration reads. */
|
|
238
|
+
export const mcpSecretFields = (mcp: ExtensionMcpCapability | undefined): readonly string[] => [
|
|
239
|
+
...new Set(
|
|
240
|
+
(mcp?.servers ?? []).flatMap((server) =>
|
|
241
|
+
Object.values(server.transport === 'stdio' ? server.env : server.headers).flatMap((value) =>
|
|
242
|
+
typeof value === 'string' ? [] : [value.secret],
|
|
243
|
+
),
|
|
244
|
+
),
|
|
245
|
+
),
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
/** What a server connects to; changing it needs a new approval, unlike its credentials. */
|
|
249
|
+
const mcpTarget = (server: ExtensionMcpServer): string =>
|
|
250
|
+
server.transport === 'stdio'
|
|
251
|
+
? JSON.stringify(['stdio', server.name, server.command, server.args])
|
|
252
|
+
: JSON.stringify(['streamable-http', server.name, server.url])
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Optional Host capabilities of an agent-scope Revision. Every field is additive to Manifest V6: an absent field
|
|
256
|
+
* means the Revision cannot use that capability, exactly as before the field existed.
|
|
257
|
+
*/
|
|
258
|
+
export const ExtensionCapabilitiesSchema = z
|
|
259
|
+
.object({
|
|
260
|
+
network: ExtensionNetworkCapabilitySchema.optional(),
|
|
261
|
+
storage: ExtensionStorageCapabilitySchema.optional(),
|
|
262
|
+
assets: z
|
|
263
|
+
.object({ write: z.literal(true) })
|
|
264
|
+
.strict()
|
|
265
|
+
.optional(),
|
|
266
|
+
history: z
|
|
267
|
+
.object({ read: z.literal(true) })
|
|
268
|
+
.strict()
|
|
269
|
+
.optional(),
|
|
270
|
+
inboundHook: ExtensionInboundHookCapabilitySchema.optional(),
|
|
271
|
+
jobs: ExtensionJobsCapabilitySchema.optional(),
|
|
272
|
+
platform: ExtensionPlatformCapabilitySchema.optional(),
|
|
273
|
+
llm: ExtensionLlmCapabilitySchema.optional(),
|
|
274
|
+
mcp: ExtensionMcpCapabilitySchema.optional(),
|
|
275
|
+
context: z
|
|
276
|
+
.array(ExtensionContextContributionSchema)
|
|
277
|
+
.max(8)
|
|
278
|
+
.refine((value) => new Set(value.map(({ name }) => name)).size === value.length, '上下文名称不能重复。')
|
|
279
|
+
.optional(),
|
|
280
|
+
})
|
|
281
|
+
.strict()
|
|
282
|
+
export type ExtensionCapabilities = z.output<typeof ExtensionCapabilitiesSchema>
|
|
283
|
+
|
|
284
|
+
const NETWORK_MODE_RANK = { domains: 0, config: 1, unrestricted: 2 } as const
|
|
285
|
+
|
|
286
|
+
const networkExpands = (
|
|
287
|
+
previous: ExtensionNetworkCapability | undefined,
|
|
288
|
+
next: ExtensionNetworkCapability | undefined,
|
|
289
|
+
): boolean => {
|
|
290
|
+
if (next === undefined) return false
|
|
291
|
+
if (previous === undefined) return true
|
|
292
|
+
if (NETWORK_MODE_RANK[next.mode] !== NETWORK_MODE_RANK[previous.mode]) {
|
|
293
|
+
return NETWORK_MODE_RANK[next.mode] > NETWORK_MODE_RANK[previous.mode]
|
|
294
|
+
}
|
|
295
|
+
if (next.mode === 'domains' && previous.mode === 'domains') {
|
|
296
|
+
return next.domains.some((domain) => !previous.domains.includes(domain))
|
|
297
|
+
}
|
|
298
|
+
if (next.mode === 'config' && previous.mode === 'config') {
|
|
299
|
+
return next.fields.some((field) => !previous.fields.includes(field))
|
|
300
|
+
}
|
|
301
|
+
return false
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Whether `next` asks for something an approval of `previous` did not cover. Narrowing never needs approval;
|
|
306
|
+
* storage quota is bounded by the Host maximum and is not an expansion. Context contributions are capped text
|
|
307
|
+
* shown in the trace, so adding one is not an expansion either.
|
|
308
|
+
*/
|
|
309
|
+
export const extensionCapabilitiesExpand = (
|
|
310
|
+
previous: ExtensionCapabilities | undefined,
|
|
311
|
+
next: ExtensionCapabilities | undefined,
|
|
312
|
+
): boolean => {
|
|
313
|
+
if (next === undefined) return false
|
|
314
|
+
if (networkExpands(previous?.network, next.network)) return true
|
|
315
|
+
if (next.storage?.scopes.some((scope) => !(previous?.storage?.scopes ?? []).includes(scope))) return true
|
|
316
|
+
if (next.assets !== undefined && previous?.assets === undefined) return true
|
|
317
|
+
if (next.history !== undefined && previous?.history === undefined) return true
|
|
318
|
+
const platformKey = ({ adapter, action }: { readonly adapter: string; readonly action: string }) =>
|
|
319
|
+
`${adapter}:${action}`
|
|
320
|
+
const previousActions = new Set((previous?.platform?.actions ?? []).map(platformKey))
|
|
321
|
+
if ((next.platform?.actions ?? []).some((entry) => !previousActions.has(platformKey(entry)))) return true
|
|
322
|
+
if ((next.platform?.raw ?? []).some((adapter) => !(previous?.platform?.raw ?? []).includes(adapter))) return true
|
|
323
|
+
const hook = next.inboundHook
|
|
324
|
+
const previousHook = previous?.inboundHook
|
|
325
|
+
if (
|
|
326
|
+
hook !== undefined &&
|
|
327
|
+
(previousHook === undefined ||
|
|
328
|
+
(hook.reads === 'all' && previousHook.reads === 'triggered') ||
|
|
329
|
+
(hook.mayHide && !previousHook.mayHide) ||
|
|
330
|
+
(hook.mayForceTrigger && !previousHook.mayForceTrigger))
|
|
331
|
+
) {
|
|
332
|
+
return true
|
|
333
|
+
}
|
|
334
|
+
if (
|
|
335
|
+
next.jobs?.runtime !== undefined &&
|
|
336
|
+
(previous?.jobs?.runtime === undefined || next.jobs.runtime.maxActive > previous.jobs.runtime.maxActive)
|
|
337
|
+
) {
|
|
338
|
+
return true
|
|
339
|
+
}
|
|
340
|
+
const previousMcp = new Set((previous?.mcp?.servers ?? []).map(mcpTarget))
|
|
341
|
+
if ((next.mcp?.servers ?? []).some((server) => !previousMcp.has(mcpTarget(server)))) return true
|
|
342
|
+
if (
|
|
343
|
+
next.llm !== undefined &&
|
|
344
|
+
(previous?.llm === undefined ||
|
|
345
|
+
next.llm.maxCallsPerTurn > previous.llm.maxCallsPerTurn ||
|
|
346
|
+
next.llm.maxOutputTokens > previous.llm.maxOutputTokens)
|
|
347
|
+
) {
|
|
348
|
+
return true
|
|
349
|
+
}
|
|
350
|
+
return false
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Capability risk tiers shown on the approval page (§7 of the capability decision). */
|
|
354
|
+
export type ExtensionCapabilityRisk = 'normal' | 'sensitive' | 'high'
|
|
355
|
+
|
|
356
|
+
export interface ExtensionCapabilitySummaryItem {
|
|
357
|
+
readonly key: string
|
|
358
|
+
readonly risk: ExtensionCapabilityRisk
|
|
359
|
+
readonly label: string
|
|
360
|
+
readonly detail?: string
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** User-facing summary of declared capabilities, in a stable order, for approval and inspection pages. */
|
|
364
|
+
export const summarizeExtensionCapabilities = (
|
|
365
|
+
capabilities: ExtensionCapabilities | undefined,
|
|
366
|
+
): readonly ExtensionCapabilitySummaryItem[] => {
|
|
367
|
+
if (capabilities === undefined) return []
|
|
368
|
+
const items: ExtensionCapabilitySummaryItem[] = []
|
|
369
|
+
const network = capabilities.network
|
|
370
|
+
if (network?.mode === 'domains') {
|
|
371
|
+
items.push({ key: 'network', risk: 'normal', label: '访问指定网站', detail: network.domains.join('、') })
|
|
372
|
+
} else if (network?.mode === 'config') {
|
|
373
|
+
items.push({ key: 'network', risk: 'normal', label: '访问你在配置中填写的地址', detail: network.fields.join('、') })
|
|
374
|
+
} else if (network?.mode === 'unrestricted') {
|
|
375
|
+
items.push({ key: 'network', risk: 'high', label: '访问任意公网地址', detail: network.purpose })
|
|
376
|
+
}
|
|
377
|
+
if (capabilities.storage !== undefined) {
|
|
378
|
+
const shared = capabilities.storage.scopes.includes('shared')
|
|
379
|
+
items.push({
|
|
380
|
+
key: 'storage',
|
|
381
|
+
risk: shared ? 'sensitive' : 'normal',
|
|
382
|
+
label: shared ? '保存数据,并在启用它的智能体之间共享' : '为这个智能体保存数据',
|
|
383
|
+
})
|
|
384
|
+
}
|
|
385
|
+
if (capabilities.assets !== undefined)
|
|
386
|
+
items.push({ key: 'assets', risk: 'normal', label: '在当前频道生成图片或文件' })
|
|
387
|
+
if (capabilities.history !== undefined)
|
|
388
|
+
items.push({ key: 'history', risk: 'normal', label: '读取当前频道的聊天记录' })
|
|
389
|
+
const hook = capabilities.inboundHook
|
|
390
|
+
if (hook !== undefined) {
|
|
391
|
+
const effects = [
|
|
392
|
+
hook.mayHide ? '对智能体隐藏消息' : '',
|
|
393
|
+
hook.mayForceTrigger ? '让智能体回应原本不会回应的消息' : '',
|
|
394
|
+
]
|
|
395
|
+
.filter((effect) => effect !== '')
|
|
396
|
+
.join('、')
|
|
397
|
+
items.push({
|
|
398
|
+
key: 'inboundHook',
|
|
399
|
+
risk: hook.reads === 'all' ? 'high' : 'sensitive',
|
|
400
|
+
label: hook.reads === 'all' ? '查看频道里的全部消息' : '在智能体回应前查看要回应的消息',
|
|
401
|
+
...(effects === '' ? {} : { detail: `可以${effects}` }),
|
|
402
|
+
})
|
|
403
|
+
}
|
|
404
|
+
const jobs = capabilities.jobs
|
|
405
|
+
if (jobs?.declared !== undefined && jobs.declared.length > 0) {
|
|
406
|
+
items.push({
|
|
407
|
+
key: 'jobs.declared',
|
|
408
|
+
risk: 'normal',
|
|
409
|
+
label: '按固定计划唤醒智能体',
|
|
410
|
+
detail: jobs.declared.map(({ label }) => label).join('、'),
|
|
411
|
+
})
|
|
412
|
+
}
|
|
413
|
+
if (jobs?.runtime !== undefined) {
|
|
414
|
+
items.push({
|
|
415
|
+
key: 'jobs.runtime',
|
|
416
|
+
risk: 'sensitive',
|
|
417
|
+
label: '创建定时任务唤醒智能体',
|
|
418
|
+
detail: `同时最多 ${jobs.runtime.maxActive} 个`,
|
|
419
|
+
})
|
|
420
|
+
}
|
|
421
|
+
const platform = capabilities.platform
|
|
422
|
+
if (platform !== undefined && platform.actions.length > 0) {
|
|
423
|
+
items.push({
|
|
424
|
+
key: 'platform.actions',
|
|
425
|
+
// Actions such as muting or removing members affect other people, so every typed action is confirmed.
|
|
426
|
+
risk: 'high',
|
|
427
|
+
label: '在平台上执行操作',
|
|
428
|
+
detail: platform.actions.map(({ adapter, action }) => `${adapter} · ${action}`).join('、'),
|
|
429
|
+
})
|
|
430
|
+
}
|
|
431
|
+
if (platform !== undefined && platform.raw.length > 0) {
|
|
432
|
+
items.push({
|
|
433
|
+
key: 'platform.raw',
|
|
434
|
+
risk: 'high',
|
|
435
|
+
label: '直接调用平台原始接口',
|
|
436
|
+
detail: platform.raw.join('、'),
|
|
437
|
+
})
|
|
438
|
+
}
|
|
439
|
+
if (capabilities.llm !== undefined) {
|
|
440
|
+
items.push({
|
|
441
|
+
key: 'llm',
|
|
442
|
+
risk: 'sensitive',
|
|
443
|
+
label: '使用这个智能体的模型',
|
|
444
|
+
detail: `每轮最多 ${capabilities.llm.maxCallsPerTurn} 次,计入智能体用量`,
|
|
445
|
+
})
|
|
446
|
+
}
|
|
447
|
+
for (const server of capabilities.mcp?.servers ?? []) {
|
|
448
|
+
items.push(
|
|
449
|
+
server.transport === 'stdio'
|
|
450
|
+
? {
|
|
451
|
+
key: `mcp.${server.name}`,
|
|
452
|
+
risk: 'high',
|
|
453
|
+
label: `在本机运行 MCP 程序 ${server.name}`,
|
|
454
|
+
detail: [server.command, ...server.args].join(' '),
|
|
455
|
+
}
|
|
456
|
+
: {
|
|
457
|
+
key: `mcp.${server.name}`,
|
|
458
|
+
risk: 'sensitive',
|
|
459
|
+
label: `连接 MCP 服务 ${server.name}`,
|
|
460
|
+
detail: new URL(server.url).host,
|
|
461
|
+
},
|
|
462
|
+
)
|
|
463
|
+
}
|
|
464
|
+
if (capabilities.context !== undefined && capabilities.context.length > 0) {
|
|
465
|
+
items.push({
|
|
466
|
+
key: 'context',
|
|
467
|
+
risk: 'normal',
|
|
468
|
+
label: '向智能体提供补充说明',
|
|
469
|
+
detail: capabilities.context.map(({ name }) => name).join('、'),
|
|
470
|
+
})
|
|
471
|
+
}
|
|
472
|
+
return items
|
|
473
|
+
}
|