@nekro-nxt/contracts 0.4.0 → 0.6.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/README.md +2 -0
- package/dist/index.mjs +3092 -164
- package/dist/index.mjs.map +1 -1
- package/lib/types/commands.d.ts +408 -0
- package/lib/types/commands.d.ts.map +1 -0
- package/lib/types/community-feedback-host.d.ts +442 -0
- package/lib/types/community-feedback-host.d.ts.map +1 -0
- package/lib/types/community-feedback.d.ts +1072 -0
- package/lib/types/community-feedback.d.ts.map +1 -0
- package/lib/types/community-personas.d.ts +4 -0
- package/lib/types/community-personas.d.ts.map +1 -1
- package/lib/types/community.d.ts +260 -13
- package/lib/types/community.d.ts.map +1 -1
- package/lib/types/config-schema.d.ts +16 -2
- package/lib/types/config-schema.d.ts.map +1 -1
- package/lib/types/credentials.d.ts +328 -0
- package/lib/types/credentials.d.ts.map +1 -0
- package/lib/types/domain.d.ts +44 -2
- package/lib/types/domain.d.ts.map +1 -1
- package/lib/types/extension-capabilities.d.ts +58 -3
- package/lib/types/extension-capabilities.d.ts.map +1 -1
- package/lib/types/extension-ui.d.ts +46 -4
- package/lib/types/extension-ui.d.ts.map +1 -1
- package/lib/types/host-api.d.ts +10805 -2887
- package/lib/types/host-api.d.ts.map +1 -1
- package/lib/types/index.d.ts +7 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/linked-accounts.d.ts +100 -0
- package/lib/types/linked-accounts.d.ts.map +1 -0
- package/lib/types/logs.d.ts +355 -0
- package/lib/types/logs.d.ts.map +1 -0
- package/lib/types/response-timing.d.ts +60 -0
- package/lib/types/response-timing.d.ts.map +1 -0
- package/lib/types/runtime-compatibility.d.ts +2 -2
- package/package.json +1 -1
- package/src/commands.ts +366 -0
- package/src/community-feedback-host.ts +121 -0
- package/src/community-feedback.ts +289 -0
- package/src/community.ts +63 -1
- package/src/config-schema.ts +33 -2
- package/src/credentials.ts +260 -0
- package/src/domain.ts +74 -8
- package/src/extension-capabilities.ts +78 -4
- package/src/extension-ui.ts +3 -0
- package/src/host-api.ts +1873 -163
- package/src/index.ts +8 -0
- package/src/linked-accounts.ts +77 -0
- package/src/logs.ts +138 -0
- package/src/management-api.ts +1 -1
- package/src/response-timing.ts +77 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { AgentIdSchema, ChannelIdSchema, CredentialIdSchema, ExtensionIdSchema } from './domain.js'
|
|
3
|
+
import { ExtensionNetworkDomainSchema } from './extension-capabilities.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Named credentials (智能体扩展配置与凭据 C1–C5): one entry is added once and reused by extensions, MCP servers and
|
|
7
|
+
* commands. The value is write-only; every view below carries metadata only. Accounts on platforms such as GitHub are
|
|
8
|
+
* linked accounts (`linked-accounts.ts`), granted per agent, not credentials.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const CREDENTIAL_NAME_MAX_CHARS = 64
|
|
12
|
+
export const CREDENTIAL_DESCRIPTION_MAX_CHARS = 500
|
|
13
|
+
export const CREDENTIAL_VALUE_MAX_CHARS = 16_384
|
|
14
|
+
/** Values shorter than this are not hidden from tool results; the form warns when one is saved. */
|
|
15
|
+
export const CREDENTIAL_REDACTABLE_MIN_CHARS = 8
|
|
16
|
+
|
|
17
|
+
/** How a credential reaches code: `request` only as a placeholder filled by `nxt.http.fetch`; `program` as the value. */
|
|
18
|
+
export const CredentialUsageSchema = z.enum(['request', 'program'])
|
|
19
|
+
export type CredentialUsage = z.output<typeof CredentialUsageSchema>
|
|
20
|
+
|
|
21
|
+
/** Agents that may use a credential: every agent, or the listed ones. */
|
|
22
|
+
export const CredentialAgentScopeSchema = z.union([
|
|
23
|
+
z.object({ all: z.literal(true) }).strict(),
|
|
24
|
+
z.object({ agentIds: z.array(AgentIdSchema).max(256) }).strict(),
|
|
25
|
+
])
|
|
26
|
+
export type CredentialAgentScope = z.output<typeof CredentialAgentScopeSchema>
|
|
27
|
+
|
|
28
|
+
/** Shown in redaction markers (`[凭据:名称]`), so brackets and control characters are not allowed. */
|
|
29
|
+
export const CredentialNameSchema = z
|
|
30
|
+
.string()
|
|
31
|
+
.trim()
|
|
32
|
+
.min(1, '名称不能为空。')
|
|
33
|
+
.max(CREDENTIAL_NAME_MAX_CHARS, `名称最多 ${CREDENTIAL_NAME_MAX_CHARS} 个字符。`)
|
|
34
|
+
// eslint-disable-next-line no-control-regex -- Control characters are exactly what the name must not contain.
|
|
35
|
+
.regex(/^[^\u0000-\u001f\u007f[\]]+$/u, '名称不能包含方括号或控制字符。')
|
|
36
|
+
|
|
37
|
+
export const CredentialDescriptionSchema = z
|
|
38
|
+
.string()
|
|
39
|
+
.trim()
|
|
40
|
+
.max(CREDENTIAL_DESCRIPTION_MAX_CHARS, `说明最多 ${CREDENTIAL_DESCRIPTION_MAX_CHARS} 个字符。`)
|
|
41
|
+
|
|
42
|
+
export const CredentialDomainsSchema = z.array(ExtensionNetworkDomainSchema).max(32)
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Names a credential's environment variable cannot take: the shell, the tools' own configuration, and the variables
|
|
46
|
+
* linked accounts give commands (`GH_TOKEN` and the `gh` settings).
|
|
47
|
+
*/
|
|
48
|
+
const RESERVED_COMMAND_ENV = new Set([
|
|
49
|
+
'PATH',
|
|
50
|
+
'HOME',
|
|
51
|
+
'USER',
|
|
52
|
+
'LOGNAME',
|
|
53
|
+
'SHELL',
|
|
54
|
+
'PWD',
|
|
55
|
+
'OLDPWD',
|
|
56
|
+
'TMPDIR',
|
|
57
|
+
'TEMP',
|
|
58
|
+
'TMP',
|
|
59
|
+
'LANG',
|
|
60
|
+
'TERM',
|
|
61
|
+
'NODE_OPTIONS',
|
|
62
|
+
'NODE_PATH',
|
|
63
|
+
'GH_TOKEN',
|
|
64
|
+
'GH_CONFIG_DIR',
|
|
65
|
+
'GH_PROMPT_DISABLED',
|
|
66
|
+
'GH_NO_UPDATE_NOTIFIER',
|
|
67
|
+
'GH_NO_EXTENSION_UPDATE_NOTIFIER',
|
|
68
|
+
'GIT_TERMINAL_PROMPT',
|
|
69
|
+
'GIT_CONFIG_COUNT',
|
|
70
|
+
])
|
|
71
|
+
const RESERVED_COMMAND_ENV_PREFIXES = ['LC_', 'LD_', 'DYLD_', 'GIT_CONFIG_', 'NEKRO_', 'DSH_']
|
|
72
|
+
|
|
73
|
+
/** The environment variable a program credential reaches commands under. */
|
|
74
|
+
export const CredentialEnvNameSchema = z
|
|
75
|
+
.string()
|
|
76
|
+
.trim()
|
|
77
|
+
.max(64)
|
|
78
|
+
.regex(/^[A-Z_][A-Z0-9_]*$/u, '环境变量名只能使用大写字母、数字和下划线,且不能以数字开头。')
|
|
79
|
+
.refine(
|
|
80
|
+
(name) =>
|
|
81
|
+
!RESERVED_COMMAND_ENV.has(name) && !RESERVED_COMMAND_ENV_PREFIXES.some((prefix) => name.startsWith(prefix)),
|
|
82
|
+
'这个环境变量名由系统使用,请换一个。',
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
export const CredentialValueSchema = z.string().min(1).max(CREDENTIAL_VALUE_MAX_CHARS)
|
|
86
|
+
|
|
87
|
+
/** What uses one credential right now: extension secret fields bound to it and agents whose commands receive it. */
|
|
88
|
+
export const CredentialUsersSchema = z
|
|
89
|
+
.object({
|
|
90
|
+
extensions: z.array(
|
|
91
|
+
z
|
|
92
|
+
.object({
|
|
93
|
+
extensionId: ExtensionIdSchema,
|
|
94
|
+
name: z.string(),
|
|
95
|
+
/** Agents whose own configuration binds it; empty when only the host configuration does. */
|
|
96
|
+
agentIds: z.array(AgentIdSchema),
|
|
97
|
+
host: z.boolean(),
|
|
98
|
+
})
|
|
99
|
+
.strict(),
|
|
100
|
+
),
|
|
101
|
+
/** Agents with commands enabled whose commands receive it. */
|
|
102
|
+
commandAgents: z.array(AgentIdSchema),
|
|
103
|
+
})
|
|
104
|
+
.strict()
|
|
105
|
+
export type CredentialUsers = z.output<typeof CredentialUsersSchema>
|
|
106
|
+
|
|
107
|
+
export const CredentialViewSchema = z
|
|
108
|
+
.object({
|
|
109
|
+
id: CredentialIdSchema,
|
|
110
|
+
name: CredentialNameSchema,
|
|
111
|
+
description: z.string(),
|
|
112
|
+
usage: CredentialUsageSchema,
|
|
113
|
+
domains: z.array(z.string()),
|
|
114
|
+
agentScope: CredentialAgentScopeSchema,
|
|
115
|
+
autoUse: z.boolean(),
|
|
116
|
+
/** Program credentials: the environment variable agents' commands receive it under, or `null` when they do not. */
|
|
117
|
+
envName: z.string().nullable(),
|
|
118
|
+
/** Agents an administrator let use it while automatic use is off. */
|
|
119
|
+
approvedAgentIds: z.array(AgentIdSchema),
|
|
120
|
+
/** False when the stored value is too short to hide from tool results, or cannot be read. */
|
|
121
|
+
redactable: z.boolean(),
|
|
122
|
+
/** False when the stored value cannot be read any more; the entry stays so it can be replaced. */
|
|
123
|
+
available: z.boolean(),
|
|
124
|
+
usedBy: CredentialUsersSchema,
|
|
125
|
+
createdAt: z.number().int().nonnegative(),
|
|
126
|
+
updatedAt: z.number().int().nonnegative(),
|
|
127
|
+
lastUsedAt: z.number().int().nonnegative().nullable(),
|
|
128
|
+
})
|
|
129
|
+
.strict()
|
|
130
|
+
export type CredentialView = z.output<typeof CredentialViewSchema>
|
|
131
|
+
|
|
132
|
+
/** A model provider listed with the credentials; `open` lets agents and extensions call it through the host. */
|
|
133
|
+
export const ModelProviderAccessSchema = z
|
|
134
|
+
.object({
|
|
135
|
+
provider: z.string().min(1),
|
|
136
|
+
name: z.string(),
|
|
137
|
+
open: z.boolean(),
|
|
138
|
+
})
|
|
139
|
+
.strict()
|
|
140
|
+
export type ModelProviderAccess = z.output<typeof ModelProviderAccessSchema>
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* An agent asked for a credential that does not exist yet. The settings link pre-fills the add form; saving a
|
|
144
|
+
* credential with this name wakes the agent in the channel it asked from.
|
|
145
|
+
*/
|
|
146
|
+
export const CredentialRequestViewSchema = z
|
|
147
|
+
.object({
|
|
148
|
+
id: z.string().min(1).max(64),
|
|
149
|
+
name: CredentialNameSchema,
|
|
150
|
+
purpose: z.string(),
|
|
151
|
+
domains: z.array(z.string()),
|
|
152
|
+
usage: CredentialUsageSchema,
|
|
153
|
+
agentId: AgentIdSchema,
|
|
154
|
+
channelId: ChannelIdSchema.nullable(),
|
|
155
|
+
createdAt: z.number().int().nonnegative(),
|
|
156
|
+
/** Settings route that opens the add form pre-filled, see {@link credentialRequestRoute}. */
|
|
157
|
+
route: z.string(),
|
|
158
|
+
})
|
|
159
|
+
.strict()
|
|
160
|
+
export type CredentialRequestView = z.output<typeof CredentialRequestViewSchema>
|
|
161
|
+
|
|
162
|
+
export const CredentialsViewSchema = z
|
|
163
|
+
.object({
|
|
164
|
+
credentials: z.array(CredentialViewSchema),
|
|
165
|
+
modelProviders: z.array(ModelProviderAccessSchema),
|
|
166
|
+
requests: z.array(CredentialRequestViewSchema),
|
|
167
|
+
})
|
|
168
|
+
.strict()
|
|
169
|
+
export type CredentialsView = z.output<typeof CredentialsViewSchema>
|
|
170
|
+
|
|
171
|
+
export const CreateCredentialRequestSchema = z
|
|
172
|
+
.object({
|
|
173
|
+
name: CredentialNameSchema,
|
|
174
|
+
description: CredentialDescriptionSchema.default(''),
|
|
175
|
+
agentScope: CredentialAgentScopeSchema.default({ all: true }),
|
|
176
|
+
autoUse: z.boolean().default(true),
|
|
177
|
+
usage: CredentialUsageSchema.default('request'),
|
|
178
|
+
domains: CredentialDomainsSchema.default([]),
|
|
179
|
+
/** Program credentials only. */
|
|
180
|
+
envName: CredentialEnvNameSchema.optional(),
|
|
181
|
+
value: CredentialValueSchema,
|
|
182
|
+
/** The agent request this form was opened from, if any. */
|
|
183
|
+
requestId: z.string().min(1).max(64).optional(),
|
|
184
|
+
})
|
|
185
|
+
.strict()
|
|
186
|
+
export type CreateCredentialRequest = z.input<typeof CreateCredentialRequestSchema>
|
|
187
|
+
|
|
188
|
+
/** Metadata changes; omitted fields keep their value. */
|
|
189
|
+
export const UpdateCredentialRequestSchema = z
|
|
190
|
+
.object({
|
|
191
|
+
name: CredentialNameSchema.optional(),
|
|
192
|
+
description: CredentialDescriptionSchema.optional(),
|
|
193
|
+
usage: CredentialUsageSchema.optional(),
|
|
194
|
+
domains: CredentialDomainsSchema.optional(),
|
|
195
|
+
agentScope: CredentialAgentScopeSchema.optional(),
|
|
196
|
+
autoUse: z.boolean().optional(),
|
|
197
|
+
/** `null` stops giving it to commands; only program credentials take one. */
|
|
198
|
+
envName: CredentialEnvNameSchema.nullable().optional(),
|
|
199
|
+
/** Replaces the agents allowed to use it while automatic use is off, for example to withdraw an approval. */
|
|
200
|
+
approvedAgentIds: z.array(AgentIdSchema).max(256).optional(),
|
|
201
|
+
})
|
|
202
|
+
.strict()
|
|
203
|
+
export type UpdateCredentialRequest = z.output<typeof UpdateCredentialRequestSchema>
|
|
204
|
+
|
|
205
|
+
export const ReplaceCredentialValueRequestSchema = z.object({ value: CredentialValueSchema }).strict()
|
|
206
|
+
export type ReplaceCredentialValueRequest = z.output<typeof ReplaceCredentialValueRequestSchema>
|
|
207
|
+
|
|
208
|
+
/** Removing a credential that is in use needs `confirm`; without it the result lists who uses it. */
|
|
209
|
+
export const RemoveCredentialRequestSchema = z.object({ confirm: z.boolean().default(false) }).strict()
|
|
210
|
+
|
|
211
|
+
export const RemoveCredentialResultSchema = z.discriminatedUnion('removed', [
|
|
212
|
+
z.object({ removed: z.literal(true), view: CredentialsViewSchema }).strict(),
|
|
213
|
+
z.object({ removed: z.literal(false), usedBy: CredentialUsersSchema }).strict(),
|
|
214
|
+
])
|
|
215
|
+
export type RemoveCredentialResult = z.output<typeof RemoveCredentialResultSchema>
|
|
216
|
+
|
|
217
|
+
export const CredentialConsumerKindSchema = z.enum(['extension', 'command', 'mcp', 'model'])
|
|
218
|
+
|
|
219
|
+
/** One recorded use. The newest 500 per credential are kept, none older than 90 days. */
|
|
220
|
+
export const CredentialUseSchema = z
|
|
221
|
+
.object({
|
|
222
|
+
at: z.number().int().nonnegative(),
|
|
223
|
+
agentId: AgentIdSchema.nullable(),
|
|
224
|
+
channelId: ChannelIdSchema.nullable(),
|
|
225
|
+
requesterMemberId: z.string().nullable(),
|
|
226
|
+
consumerKind: CredentialConsumerKindSchema,
|
|
227
|
+
consumerId: z.string(),
|
|
228
|
+
/** Extension display name or MCP server name, when it is still known. */
|
|
229
|
+
consumerName: z.string().nullable(),
|
|
230
|
+
targetHost: z.string().nullable(),
|
|
231
|
+
})
|
|
232
|
+
.strict()
|
|
233
|
+
export type CredentialUse = z.output<typeof CredentialUseSchema>
|
|
234
|
+
|
|
235
|
+
export const CredentialUsesViewSchema = z.object({ uses: z.array(CredentialUseSchema) }).strict()
|
|
236
|
+
|
|
237
|
+
export const SetModelProviderAccessRequestSchema = z.object({ open: z.boolean() }).strict()
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The settings route that opens the add form pre-filled from an agent's request: the credential section with `add=1`
|
|
241
|
+
* plus `name`, `purpose`, `domains` (comma separated), `usage` and `request` (the request id) as query parameters.
|
|
242
|
+
*/
|
|
243
|
+
export const credentialRequestRoute = (request: {
|
|
244
|
+
readonly id: string
|
|
245
|
+
readonly name: string
|
|
246
|
+
readonly purpose: string
|
|
247
|
+
readonly domains: readonly string[]
|
|
248
|
+
readonly usage: CredentialUsage
|
|
249
|
+
}): string => {
|
|
250
|
+
const query = new URLSearchParams({ add: '1', name: request.name, request: request.id, usage: request.usage })
|
|
251
|
+
if (request.purpose !== '') query.set('purpose', request.purpose)
|
|
252
|
+
if (request.domains.length > 0) query.set('domains', request.domains.join(','))
|
|
253
|
+
return `/settings/credentials?${query.toString()}`
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Secret fields of an extension configuration layer bound to a named credential, by field. */
|
|
257
|
+
export const BoundCredentialsSchema = z.record(z.string(), CredentialIdSchema)
|
|
258
|
+
|
|
259
|
+
/** Binds secret fields to named credentials; `null` removes the binding and leaves the field unset. */
|
|
260
|
+
export const CredentialBindingsRequestSchema = z.record(z.string(), CredentialIdSchema.nullable())
|
package/src/domain.ts
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
|
|
3
|
+
/** Rendered send-tool result consumed by both the agent and the product detail view. */
|
|
4
|
+
export const MessageDeliverySummarySchema = z
|
|
5
|
+
.object({
|
|
6
|
+
status: z.enum(['sent', 'partially-sent', 'failed', 'unknown']),
|
|
7
|
+
issues: z
|
|
8
|
+
.array(
|
|
9
|
+
z
|
|
10
|
+
.object({
|
|
11
|
+
status: z.enum(['failed', 'unknown']),
|
|
12
|
+
message: z.string().max(1000),
|
|
13
|
+
kind: z.enum(['transient', 'permanent', 'rate-limited', 'authentication', 'invalid']).optional(),
|
|
14
|
+
action: z.string().max(120).optional(),
|
|
15
|
+
retcode: z.number().int().safe().optional(),
|
|
16
|
+
})
|
|
17
|
+
.strict(),
|
|
18
|
+
)
|
|
19
|
+
.max(20),
|
|
20
|
+
omittedIssues: z.number().int().nonnegative().optional(),
|
|
21
|
+
/** Extensions that changed the message before it was sent, the content they produced and what they appended. */
|
|
22
|
+
processing: z
|
|
23
|
+
.object({
|
|
24
|
+
processedBy: z.array(z.string()),
|
|
25
|
+
parts: z.array(z.unknown()).optional(),
|
|
26
|
+
followUps: z.array(z.string()).optional(),
|
|
27
|
+
})
|
|
28
|
+
.strict()
|
|
29
|
+
.optional(),
|
|
30
|
+
})
|
|
31
|
+
.strict()
|
|
32
|
+
|
|
3
33
|
const brandedId = <Prefix extends string, Brand extends string>(prefix: Prefix, brand: Brand) =>
|
|
4
34
|
z
|
|
5
35
|
.string()
|
|
@@ -28,8 +58,14 @@ export const DshPluginEntryIdSchema = brandedId('dse', 'DshPluginEntryId')
|
|
|
28
58
|
export const HostUiPageInstanceIdSchema = brandedId('hup', 'HostUiPageInstanceId')
|
|
29
59
|
export const AuthoringTaskIdSchema = brandedId('aut', 'AuthoringTaskId')
|
|
30
60
|
export const AuthoringAttemptIdSchema = brandedId('aua', 'AuthoringAttemptId')
|
|
61
|
+
export const SubConversationIdSchema = brandedId('cnv', 'SubConversationId')
|
|
62
|
+
export const CredentialIdSchema = brandedId('crd', 'CredentialId')
|
|
63
|
+
export const LinkedAccountIdSchema = brandedId('acc', 'LinkedAccountId')
|
|
31
64
|
|
|
32
65
|
export type AgentId = z.infer<typeof AgentIdSchema>
|
|
66
|
+
export type SubConversationId = z.infer<typeof SubConversationIdSchema>
|
|
67
|
+
export type CredentialId = z.infer<typeof CredentialIdSchema>
|
|
68
|
+
export type LinkedAccountId = z.infer<typeof LinkedAccountIdSchema>
|
|
33
69
|
export type AgentRevisionId = z.infer<typeof AgentRevisionIdSchema>
|
|
34
70
|
export type ConnectionId = z.infer<typeof ConnectionIdSchema>
|
|
35
71
|
export type ConnectionEventId = z.infer<typeof ConnectionEventIdSchema>
|
|
@@ -205,8 +241,8 @@ export interface DshPluginActivationRecord {
|
|
|
205
241
|
export interface DshPluginDiagnosticRecord {
|
|
206
242
|
readonly entryId: DshPluginEntryId
|
|
207
243
|
readonly targetKey: string
|
|
208
|
-
readonly status: 'active' | 'load-failed' | 'restore-failed' | 'dispose-failed'
|
|
209
|
-
readonly phase: 'import' | 'apply' | 'update' | 'dispose' | 'restore'
|
|
244
|
+
readonly status: 'active' | 'pending' | 'load-failed' | 'restore-failed' | 'dispose-failed'
|
|
245
|
+
readonly phase: 'import' | 'dependency' | 'apply' | 'update' | 'dispose' | 'restore'
|
|
210
246
|
readonly message?: string
|
|
211
247
|
readonly observedAt: number
|
|
212
248
|
}
|
|
@@ -259,7 +295,18 @@ export const RichTargetUrlSchema = z
|
|
|
259
295
|
export const MessagePartSchema = z.discriminatedUnion('type', [
|
|
260
296
|
z.object({ type: z.literal('text'), text: z.string() }).strict(),
|
|
261
297
|
z.object({ type: z.literal('mention'), memberId: ChannelMemberIdSchema }).strict(),
|
|
262
|
-
z
|
|
298
|
+
z
|
|
299
|
+
.object({
|
|
300
|
+
type: z.literal('image'),
|
|
301
|
+
assetId: AssetIdSchema,
|
|
302
|
+
alt: z.string().optional(),
|
|
303
|
+
/**
|
|
304
|
+
* The platform reported the picture as a sticker. A hint only: a sticker dragged in from a desktop client and
|
|
305
|
+
* pictures from platforms without this signal arrive unmarked.
|
|
306
|
+
*/
|
|
307
|
+
sticker: z.literal(true).optional(),
|
|
308
|
+
})
|
|
309
|
+
.strict(),
|
|
263
310
|
z.object({ type: z.literal('file'), assetId: AssetIdSchema, name: z.string().optional() }).strict(),
|
|
264
311
|
z.object({ type: z.literal('audio'), assetId: AssetIdSchema }).strict(),
|
|
265
312
|
z.object({ type: z.literal('quote'), messageId: LogicalMessageIdSchema }).strict(),
|
|
@@ -287,19 +334,24 @@ export function parseMessageParts(input: unknown): MessagePart[] {
|
|
|
287
334
|
return MessagePartsSchema.parse(input)
|
|
288
335
|
}
|
|
289
336
|
|
|
290
|
-
const collectRichAssetIds = (value: JsonValue | undefined, into: AssetId[]): void => {
|
|
337
|
+
const collectRichAssetIds = (value: JsonValue | undefined, into: AssetId[], imagesOnly = false): void => {
|
|
291
338
|
if (value === null || value === undefined || typeof value === 'boolean' || typeof value === 'number') return
|
|
292
339
|
if (typeof value === 'string') return
|
|
293
340
|
if (Array.isArray(value)) {
|
|
294
|
-
for (const item of value) collectRichAssetIds(item, into)
|
|
341
|
+
for (const item of value) collectRichAssetIds(item, into, imagesOnly)
|
|
295
342
|
return
|
|
296
343
|
}
|
|
297
344
|
for (const [key, child] of Object.entries(value)) {
|
|
298
|
-
if (
|
|
345
|
+
if (
|
|
346
|
+
(key === 'imageAssetId' ||
|
|
347
|
+
key === 'previewAssetId' ||
|
|
348
|
+
(!imagesOnly && (key === 'audioAssetId' || key === 'fileAssetId'))) &&
|
|
349
|
+
typeof child === 'string'
|
|
350
|
+
) {
|
|
299
351
|
const parsed = AssetIdSchema.safeParse(child)
|
|
300
352
|
if (parsed.success) into.push(parsed.data)
|
|
301
353
|
} else {
|
|
302
|
-
collectRichAssetIds(child, into)
|
|
354
|
+
collectRichAssetIds(child, into, imagesOnly)
|
|
303
355
|
}
|
|
304
356
|
}
|
|
305
357
|
}
|
|
@@ -315,6 +367,15 @@ export const messagePartAssetIds = (part: MessagePart): readonly AssetId[] => {
|
|
|
315
367
|
|
|
316
368
|
export const messagePartAssetId = (part: MessagePart): AssetId | undefined => messagePartAssetIds(part)[0]
|
|
317
369
|
|
|
370
|
+
/** Only actual pictures enter visual context; forwarded files and recordings retain their own media kind. */
|
|
371
|
+
export const messagePartImageAssetIds = (part: MessagePart): readonly AssetId[] => {
|
|
372
|
+
if (part.type === 'image') return [part.assetId]
|
|
373
|
+
if (part.type !== 'rich') return []
|
|
374
|
+
const ids: AssetId[] = part.previewAssetId ? [part.previewAssetId] : []
|
|
375
|
+
collectRichAssetIds(part.extension, ids, true)
|
|
376
|
+
return ids
|
|
377
|
+
}
|
|
378
|
+
|
|
318
379
|
const asJsonRecord = (value: JsonValue | undefined): Readonly<Record<string, JsonValue>> | undefined =>
|
|
319
380
|
value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined
|
|
320
381
|
|
|
@@ -332,7 +393,12 @@ const richItemLine = (value: JsonValue): string | undefined => {
|
|
|
332
393
|
: undefined
|
|
333
394
|
const imageName = typeof item['imageName'] === 'string' ? item['imageName'] : undefined
|
|
334
395
|
const imageText = typeof item['imageAssetId'] === 'string' ? `[图片${imageName ? ` ${imageName}` : ''}]` : undefined
|
|
335
|
-
const
|
|
396
|
+
const audioText = typeof item['audioAssetId'] === 'string' ? `[语音 ${item['audioAssetId']}]` : undefined
|
|
397
|
+
const fileText =
|
|
398
|
+
typeof item['fileAssetId'] === 'string'
|
|
399
|
+
? `[${typeof item['fileMediaType'] === 'string' && item['fileMediaType'].startsWith('video/') ? '视频' : '文件'} ${item['fileAssetId']}${typeof item['fileName'] === 'string' ? `:${item['fileName']}` : ''}]`
|
|
400
|
+
: undefined
|
|
401
|
+
const body = [text, cardSummary ? `[卡片] ${cardSummary}` : undefined, imageText, audioText, fileText]
|
|
336
402
|
.filter((part): part is string => Boolean(part && part.length > 0))
|
|
337
403
|
.join(' ')
|
|
338
404
|
if (!sender && !body) return undefined
|
|
@@ -4,9 +4,18 @@ import { z } from 'zod'
|
|
|
4
4
|
* Host capability level this build implements. A Revision that declares `requires.sdk` above it is rejected at
|
|
5
5
|
* import with an "upgrade NekroNXT" message instead of an opaque schema error. Bump when a Manifest capability
|
|
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
|
-
* images, the search index and host-layer model calls.
|
|
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
|
+
* 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`).
|
|
8
11
|
*/
|
|
9
|
-
export const EXTENSION_SDK_LEVEL =
|
|
12
|
+
export const EXTENSION_SDK_LEVEL = 13
|
|
13
|
+
|
|
14
|
+
/** Outbound processing needs this level; packages that declare `permissions.agent.outbound` declare it. */
|
|
15
|
+
export const EXTENSION_OUTBOUND_SDK_LEVEL = 13
|
|
16
|
+
|
|
17
|
+
/** Lineage, `dataVersion` and administrator-only commands need this level; packages that carry them declare it. */
|
|
18
|
+
export const EXTENSION_GOVERNANCE_SDK_LEVEL = 10
|
|
10
19
|
|
|
11
20
|
/**
|
|
12
21
|
* The first NekroNXT version of each capability level, so users read "needs NekroNXT 0.5.0" rather than a level
|
|
@@ -16,6 +25,11 @@ export const EXTENSION_SDK_RELEASES: readonly { readonly level: number; readonly
|
|
|
16
25
|
{ level: 6, version: '0.3.0' },
|
|
17
26
|
{ level: 7, version: '0.4.0' },
|
|
18
27
|
{ level: 8, version: '0.5.0' },
|
|
28
|
+
{ level: 9, version: '0.6.0' },
|
|
29
|
+
{ level: 10, version: '0.6.0' },
|
|
30
|
+
{ level: 11, version: '0.6.0' },
|
|
31
|
+
{ level: 12, version: '0.7.0' },
|
|
32
|
+
{ level: 13, version: '0.7.0' },
|
|
19
33
|
]
|
|
20
34
|
|
|
21
35
|
/** The oldest NekroNXT version that supports `level`; undefined for a level no release supports yet. */
|
|
@@ -121,6 +135,32 @@ export const ExtensionInboundHookCapabilitySchema = z
|
|
|
121
135
|
.strict()
|
|
122
136
|
export type ExtensionInboundHookCapability = z.output<typeof ExtensionInboundHookCapabilitySchema>
|
|
123
137
|
|
|
138
|
+
export const EXTENSION_OUTBOUND_MAX_TIMEOUT_MS = 15_000
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Outbound processing of the agent's own messages. `read` sees each message after it was delivered; `transform` may
|
|
142
|
+
* replace its content or append messages before it is sent; `cancel` may stop it. `onTimeout` decides what happens
|
|
143
|
+
* when the handler times out or throws before sending: `send` (default) sends the message as it is.
|
|
144
|
+
*/
|
|
145
|
+
export const ExtensionOutboundCapabilitySchema = z
|
|
146
|
+
.object({
|
|
147
|
+
read: z.literal(true).optional(),
|
|
148
|
+
transform: z.literal(true).optional(),
|
|
149
|
+
cancel: z.literal(true).optional(),
|
|
150
|
+
timeoutMs: z.number().int().min(100).max(EXTENSION_OUTBOUND_MAX_TIMEOUT_MS),
|
|
151
|
+
onTimeout: z.enum(['send', 'cancel']).optional(),
|
|
152
|
+
})
|
|
153
|
+
.strict()
|
|
154
|
+
.refine(
|
|
155
|
+
(value) => value.read === true || value.transform === true || value.cancel === true,
|
|
156
|
+
'outbound 至少声明 read、transform 或 cancel 中的一项。',
|
|
157
|
+
)
|
|
158
|
+
export type ExtensionOutboundCapability = z.output<typeof ExtensionOutboundCapabilitySchema>
|
|
159
|
+
|
|
160
|
+
/** Whether the extension takes part before sending (and so in the agent's processing order). */
|
|
161
|
+
export const outboundProcessesBeforeSend = (outbound: ExtensionOutboundCapability | undefined): boolean =>
|
|
162
|
+
outbound?.transform === true || outbound?.cancel === true
|
|
163
|
+
|
|
124
164
|
export const EXTENSION_JOB_MAX_RUNTIME = 50
|
|
125
165
|
|
|
126
166
|
export const ExtensionDeclaredJobSchema = z
|
|
@@ -248,10 +288,14 @@ export const ExtensionModelsCapabilitySchema = z
|
|
|
248
288
|
.strict()
|
|
249
289
|
export type ExtensionModelsCapability = z.output<typeof ExtensionModelsCapabilitySchema>
|
|
250
290
|
|
|
251
|
-
/**
|
|
291
|
+
/**
|
|
292
|
+
* A header or environment value: literal text, the current value of a `meta.role: 'secret'` config field, or a named
|
|
293
|
+
* credential (by id or name) that the agent may use.
|
|
294
|
+
*/
|
|
252
295
|
export const ExtensionMcpValueSchema = z.union([
|
|
253
296
|
z.string().max(4096),
|
|
254
297
|
z.object({ secret: z.string().trim().min(1).max(64) }).strict(),
|
|
298
|
+
z.object({ credential: z.string().trim().min(1).max(64) }).strict(),
|
|
255
299
|
])
|
|
256
300
|
export type ExtensionMcpValue = z.output<typeof ExtensionMcpValueSchema>
|
|
257
301
|
|
|
@@ -308,6 +352,8 @@ const McpFormValueSchema = z
|
|
|
308
352
|
name: z.string().regex(/^[A-Za-z_][A-Za-z0-9_-]{0,127}$/u, '名称只能包含字母、数字、下划线和连字符。'),
|
|
309
353
|
value: z.string().max(4096),
|
|
310
354
|
secret: z.boolean().default(false),
|
|
355
|
+
/** Takes the value from this named credential instead; `value` and `secret` are then ignored. */
|
|
356
|
+
credential: z.string().trim().min(1).max(64).optional(),
|
|
311
357
|
})
|
|
312
358
|
.strict()
|
|
313
359
|
|
|
@@ -337,7 +383,7 @@ export const mcpSecretFields = (mcp: ExtensionMcpCapability | undefined): readon
|
|
|
337
383
|
...new Set(
|
|
338
384
|
(mcp?.servers ?? []).flatMap((server) =>
|
|
339
385
|
Object.values(server.transport === 'stdio' ? server.env : server.headers).flatMap((value) =>
|
|
340
|
-
typeof value === 'string' ? [] : [value.secret],
|
|
386
|
+
typeof value === 'string' || !('secret' in value) ? [] : [value.secret],
|
|
341
387
|
),
|
|
342
388
|
),
|
|
343
389
|
),
|
|
@@ -366,6 +412,7 @@ export const ExtensionCapabilitiesSchema = z
|
|
|
366
412
|
.strict()
|
|
367
413
|
.optional(),
|
|
368
414
|
inboundHook: ExtensionInboundHookCapabilitySchema.optional(),
|
|
415
|
+
outbound: ExtensionOutboundCapabilitySchema.optional(),
|
|
369
416
|
jobs: ExtensionJobsCapabilitySchema.optional(),
|
|
370
417
|
platform: ExtensionPlatformCapabilitySchema.optional(),
|
|
371
418
|
llm: ExtensionLlmCapabilitySchema.optional(),
|
|
@@ -477,6 +524,18 @@ export const extensionCapabilitiesExpand = (
|
|
|
477
524
|
) {
|
|
478
525
|
return true
|
|
479
526
|
}
|
|
527
|
+
const outbound = next.outbound
|
|
528
|
+
const previousOutbound = previous?.outbound
|
|
529
|
+
if (
|
|
530
|
+
outbound !== undefined &&
|
|
531
|
+
(previousOutbound === undefined ||
|
|
532
|
+
(outbound.read === true && previousOutbound.read !== true) ||
|
|
533
|
+
(outbound.transform === true && previousOutbound.transform !== true) ||
|
|
534
|
+
(outbound.cancel === true && previousOutbound.cancel !== true) ||
|
|
535
|
+
(outbound.onTimeout === 'cancel' && previousOutbound.onTimeout !== 'cancel'))
|
|
536
|
+
) {
|
|
537
|
+
return true
|
|
538
|
+
}
|
|
480
539
|
if (
|
|
481
540
|
next.jobs?.runtime !== undefined &&
|
|
482
541
|
(previous?.jobs?.runtime === undefined || next.jobs.runtime.maxActive > previous.jobs.runtime.maxActive)
|
|
@@ -591,6 +650,21 @@ export const summarizeExtensionCapabilities = (
|
|
|
591
650
|
...(effects === '' ? {} : { detail: `可以${effects}` }),
|
|
592
651
|
})
|
|
593
652
|
}
|
|
653
|
+
const outbound = capabilities.outbound
|
|
654
|
+
if (outbound !== undefined) {
|
|
655
|
+
if (outbound.transform === true) {
|
|
656
|
+
items.push({ key: 'outbound.transform', risk: 'high', label: '在智能体的消息发出前修改内容或追加消息' })
|
|
657
|
+
}
|
|
658
|
+
if (outbound.cancel === true) {
|
|
659
|
+
items.push({ key: 'outbound.cancel', risk: 'high', label: '取消智能体要发出的消息' })
|
|
660
|
+
}
|
|
661
|
+
if (outbound.read === true) {
|
|
662
|
+
items.push({ key: 'outbound.read', risk: 'sensitive', label: '查看智能体发出的每条消息' })
|
|
663
|
+
}
|
|
664
|
+
if (outbound.onTimeout === 'cancel' && (outbound.transform === true || outbound.cancel === true)) {
|
|
665
|
+
items.push({ key: 'outbound.onTimeout', risk: 'high', label: '这个扩展出错时,智能体的消息会发不出去' })
|
|
666
|
+
}
|
|
667
|
+
}
|
|
594
668
|
const jobs = capabilities.jobs
|
|
595
669
|
if (jobs?.declared !== undefined && jobs.declared.length > 0) {
|
|
596
670
|
items.push({
|
package/src/extension-ui.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
HostLayerCapabilitiesSchema,
|
|
5
5
|
hostLayerAsCapabilities,
|
|
6
6
|
EXTENSION_SDK_BASE_LEVEL,
|
|
7
|
+
EXTENSION_OUTBOUND_SDK_LEVEL,
|
|
7
8
|
} from './extension-capabilities.js'
|
|
8
9
|
import { ExtensionIdSchema, ExtensionRevisionIdSchema, HostUiPageInstanceIdSchema } from './domain.js'
|
|
9
10
|
import { ConfigSchemaDocumentSchema } from './config-schema.js'
|
|
@@ -554,6 +555,8 @@ export const requiredExtensionSdkLevel = (
|
|
|
554
555
|
permissions?.host?.index === undefined && permissions?.agent?.index === undefined ? undefined : '检索索引',
|
|
555
556
|
permissions?.host?.models === undefined ? undefined : '本机层模型调用',
|
|
556
557
|
].filter((name): name is string => name !== undefined)
|
|
558
|
+
if (permissions?.agent?.outbound !== undefined)
|
|
559
|
+
return { level: EXTENSION_OUTBOUND_SDK_LEVEL, capabilities: ['出站处理'] }
|
|
557
560
|
return level8.length > 0
|
|
558
561
|
? { level: 8, capabilities: [...new Set(level8)] }
|
|
559
562
|
: { level: EXTENSION_SDK_BASE_LEVEL, capabilities: [] }
|