@nekro-nxt/contracts 0.4.0 → 0.5.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.
@@ -0,0 +1,289 @@
1
+ import { z } from 'zod'
2
+
3
+ export const COMMUNITY_FEEDBACK_STATUSES = [
4
+ 'new',
5
+ 'needs_info',
6
+ 'triaged',
7
+ 'planned',
8
+ 'fixed',
9
+ 'duplicate',
10
+ 'wontfix',
11
+ 'withdrawn',
12
+ ] as const
13
+ export const CommunityFeedbackStatusSchema = z.enum(COMMUNITY_FEEDBACK_STATUSES)
14
+ export type CommunityFeedbackStatus = z.output<typeof CommunityFeedbackStatusSchema>
15
+ export const COMMUNITY_FEEDBACK_STATUS_LABELS: Readonly<Record<CommunityFeedbackStatus, string>> = {
16
+ new: '新反馈',
17
+ needs_info: '需要补充',
18
+ triaged: '已确认',
19
+ planned: '已计划',
20
+ fixed: '已修复',
21
+ duplicate: '重复',
22
+ wontfix: '不处理',
23
+ withdrawn: '已撤回',
24
+ }
25
+ export const CommunityFeedbackKindSchema = z.enum(['bug', 'suggestion', 'question'])
26
+ export const CommunityFeedbackIdSchema = z
27
+ .string()
28
+ .regex(/^fbk_[0-9a-z]{26}$/u)
29
+ .brand<'CommunityFeedbackId'>()
30
+ export const CommunityFeedbackRefSchema = z.string().regex(/^FB-[1-9][0-9]*$/u)
31
+ export const CommunityFeedbackTargetSchema = z.union([
32
+ z.literal('nxt'),
33
+ z.object({ extensionId: z.string().regex(/^ext_[0-9A-Za-z]+$/u), version: z.string().max(120).optional() }).strict(),
34
+ ])
35
+ const version = z.string().trim().min(1).max(120)
36
+ const timestamp = z.number().int().nonnegative()
37
+
38
+ export const CommunityFeedbackExtensionDiagnosticSchema = z
39
+ .object({
40
+ extensionId: z.string().max(100),
41
+ version,
42
+ accessLevel: z.string().max(80).optional(),
43
+ activation: z.enum(['host', 'agent']).optional(),
44
+ })
45
+ .strict()
46
+ export const CommunityFeedbackDiagnosticEntrySchema = z
47
+ .object({
48
+ kind: z.enum(['compatibility', 'extension', 'connection']),
49
+ summary: z.string().max(2000),
50
+ extensionId: z.string().max(100).optional(),
51
+ })
52
+ .strict()
53
+
54
+ /** An explicit allowlist: credentials, account grants and arbitrary host configuration never cross this boundary. */
55
+ export const CommunityFeedbackDiagnosticsSchema = z
56
+ .object({
57
+ nxtVersion: version.optional(),
58
+ releaseChannel: z.enum(['stable', 'preview', 'development']).optional(),
59
+ hostKind: z.enum(['desktop', 'server']).optional(),
60
+ platform: z.string().max(80).optional(),
61
+ arch: z.string().max(40).optional(),
62
+ dshVersion: version.optional(),
63
+ adapters: z.array(z.string().max(100)).max(64).optional(),
64
+ agent: z
65
+ .object({
66
+ accessLevel: z.string().max(80),
67
+ subagents: z.boolean(),
68
+ fileTools: z.boolean(),
69
+ webSearch: z.boolean(),
70
+ dynamicCreation: z.boolean(),
71
+ developmentShell: z.boolean(),
72
+ unrestrictedFileAccess: z.boolean(),
73
+ scheduledTasks: z.boolean(),
74
+ codeRun: z.boolean(),
75
+ commandNetwork: z.boolean(),
76
+ communityFeedbackAuto: z.boolean(),
77
+ })
78
+ .strict()
79
+ .optional(),
80
+ extensions: z.array(CommunityFeedbackExtensionDiagnosticSchema).max(256).optional(),
81
+ entries: z.array(CommunityFeedbackDiagnosticEntrySchema).max(32).optional(),
82
+ instanceId: z.string().min(1).max(100).optional(),
83
+ })
84
+ .strict()
85
+ export type CommunityFeedbackDiagnostics = z.output<typeof CommunityFeedbackDiagnosticsSchema>
86
+
87
+ export const CommunityFeedbackExcerptSchema = z
88
+ .array(
89
+ z
90
+ .object({
91
+ speaker: z.string().max(80),
92
+ text: z.string().max(6000),
93
+ })
94
+ .strict(),
95
+ )
96
+ .max(8)
97
+ export type CommunityFeedbackExcerpt = z.output<typeof CommunityFeedbackExcerptSchema>
98
+
99
+ export const CommunityFeedbackCreateSchema = z
100
+ .object({
101
+ clientRequestId: z.string().min(1).max(160),
102
+ kind: CommunityFeedbackKindSchema,
103
+ target: CommunityFeedbackTargetSchema,
104
+ title: z.string().trim().min(1).max(120),
105
+ body: z.string().trim().min(1).max(8000),
106
+ diagnostics: CommunityFeedbackDiagnosticsSchema.default({}),
107
+ excerpt: CommunityFeedbackExcerptSchema.optional(),
108
+ excerptSharedWithAuthor: z.boolean().default(false),
109
+ source: z.enum(['assistant', 'assistant_auto', 'form', 'web']),
110
+ instanceId: z.string().min(1).max(100).optional(),
111
+ })
112
+ .strict()
113
+ export type CommunityFeedbackCreate = z.output<typeof CommunityFeedbackCreateSchema>
114
+ export const CommunityFeedbackMessageInputSchema = z
115
+ .object({
116
+ body: z.string().trim().min(1).max(4000),
117
+ audience: z.enum(['everyone', 'staff']).default('everyone'),
118
+ })
119
+ .strict()
120
+ export const CommunityFeedbackSupplementSchema = z
121
+ .object({
122
+ diagnostics: CommunityFeedbackDiagnosticsSchema.optional(),
123
+ excerpt: CommunityFeedbackExcerptSchema.optional(),
124
+ excerptSharedWithAuthor: z.boolean().optional(),
125
+ })
126
+ .strict()
127
+ export const CommunityFeedbackWithdrawSchema = z.object({ deleteText: z.boolean().default(false) }).strict()
128
+ export const CommunityFeedbackConfirmSchema = z.object({ clientVersion: version }).strict()
129
+ export const CommunityFeedbackReopenSchema = z.object({ note: z.string().trim().min(1).max(2000) }).strict()
130
+ export const CommunityFeedbackAffectedSchema = z
131
+ .object({
132
+ diagnostics: CommunityFeedbackDiagnosticsSchema.optional(),
133
+ nxtVersion: version.optional(),
134
+ })
135
+ .strict()
136
+ export const CommunityFeedbackTransitionSchema = z
137
+ .object({
138
+ status: CommunityFeedbackStatusSchema,
139
+ fixedIn: version.optional(),
140
+ duplicateOf: CommunityFeedbackRefSchema.optional(),
141
+ note: z.string().trim().max(2000).optional(),
142
+ })
143
+ .strict()
144
+ export type CommunityFeedbackTransition = z.output<typeof CommunityFeedbackTransitionSchema>
145
+ export const CommunityFeedbackLabelsSchema = z
146
+ .object({
147
+ labels: z.array(z.string().trim().min(1).max(40)).max(20),
148
+ priority: z.enum(['low', 'normal', 'high']).nullable(),
149
+ })
150
+ .strict()
151
+ export const CommunityFeedbackLinkSchema = z
152
+ .object({
153
+ githubUrl: z
154
+ .string()
155
+ .url()
156
+ .max(1000)
157
+ .refine((value) => /^https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/[1-9][0-9]*$/u.test(value))
158
+ .nullable(),
159
+ })
160
+ .strict()
161
+ export const CommunityFeedbackVisibilitySchema = z.object({ visibility: z.enum(['private', 'public']) }).strict()
162
+
163
+ export const CommunityFeedbackEventDataSchema = z
164
+ .object({
165
+ from: CommunityFeedbackStatusSchema.optional(),
166
+ to: CommunityFeedbackStatusSchema.optional(),
167
+ fixedIn: version.nullable().optional(),
168
+ duplicateOf: CommunityFeedbackRefSchema.nullable().optional(),
169
+ note: z.string().max(2000).optional(),
170
+ labels: z.array(z.string()).optional(),
171
+ priority: z.enum(['low', 'normal', 'high']).nullable().optional(),
172
+ visibility: z.enum(['private', 'public']).optional(),
173
+ clientVersion: version.optional(),
174
+ messageId: z.string().optional(),
175
+ audience: z.enum(['everyone', 'staff']).optional(),
176
+ deleteText: z.boolean().optional(),
177
+ affected: z.boolean().optional(),
178
+ })
179
+ .strict()
180
+ export const CommunityFeedbackEventSchema = z.object({
181
+ seq: z.number().int().positive(),
182
+ ref: CommunityFeedbackRefSchema,
183
+ type: z.enum([
184
+ 'created',
185
+ 'message',
186
+ 'status_changed',
187
+ 'labeled',
188
+ 'priority_changed',
189
+ 'linked_issue',
190
+ 'duplicate_marked',
191
+ 'fixed_in_set',
192
+ 'visibility_changed',
193
+ 'affected_added',
194
+ 'withdrawn',
195
+ 'reopened',
196
+ 'supplemented',
197
+ 'fix_confirmed',
198
+ ]),
199
+ actorKind: z.enum(['user', 'admin', 'author', 'token', 'system']).optional(),
200
+ actor: z.string().optional(),
201
+ tokenId: z.string().optional(),
202
+ data: CommunityFeedbackEventDataSchema,
203
+ createdAt: timestamp,
204
+ title: z.string().optional(),
205
+ status: CommunityFeedbackStatusSchema.optional(),
206
+ })
207
+ export type CommunityFeedbackEvent = z.output<typeof CommunityFeedbackEventSchema>
208
+ export const CommunityFeedbackMessageSchema = z.object({
209
+ id: z.string(),
210
+ authorKind: z.enum(['user', 'admin', 'author', 'system']),
211
+ author: z.string().nullable(),
212
+ audience: z.enum(['everyone', 'staff']),
213
+ body: z.string(),
214
+ createdAt: timestamp,
215
+ })
216
+
217
+ export const CommunityFeedbackSummarySchema = z.object({
218
+ id: CommunityFeedbackIdSchema,
219
+ ref: CommunityFeedbackRefSchema,
220
+ title: z.string(),
221
+ kind: CommunityFeedbackKindSchema.optional(),
222
+ target: CommunityFeedbackTargetSchema.optional(),
223
+ status: CommunityFeedbackStatusSchema,
224
+ fixedIn: version.nullable(),
225
+ affectedCount: z.number().int().nonnegative(),
226
+ source: CommunityFeedbackCreateSchema.shape.source.optional(),
227
+ confirmedCount: z.number().int().nonnegative().optional(),
228
+ createdAt: timestamp.optional(),
229
+ updatedAt: timestamp.optional(),
230
+ })
231
+ export type CommunityFeedbackSummary = z.output<typeof CommunityFeedbackSummarySchema>
232
+
233
+ /** Optional private fields are emitted only by the server's role-specific projection. */
234
+ export const CommunityFeedbackSchema = CommunityFeedbackSummarySchema.extend({
235
+ access: z.enum(['owner', 'admin', 'author', 'subscriber', 'public']),
236
+ body: z.string().optional(),
237
+ diagnostics: CommunityFeedbackDiagnosticsSchema.optional(),
238
+ excerpt: CommunityFeedbackExcerptSchema.nullable().optional(),
239
+ excerptSharedWithAuthor: z.boolean().optional(),
240
+ messages: z.array(CommunityFeedbackMessageSchema).optional(),
241
+ events: z.array(CommunityFeedbackEventSchema).optional(),
242
+ submitter: z.string().optional(),
243
+ instanceId: z.string().nullable().optional(),
244
+ priority: z.enum(['low', 'normal', 'high']).nullable().optional(),
245
+ labels: z.array(z.string()).optional(),
246
+ visibility: z.enum(['private', 'public']).optional(),
247
+ duplicateOf: CommunityFeedbackRefSchema.nullable().optional(),
248
+ githubUrl: z.string().nullable().optional(),
249
+ closedAt: timestamp.nullable().optional(),
250
+ purgedAt: timestamp.nullable().optional(),
251
+ confirmed: z.boolean().default(false),
252
+ affected: z.boolean().default(false),
253
+ affectedDiagnostics: z
254
+ .array(
255
+ z.object({
256
+ user: z.string(),
257
+ diagnostics: CommunityFeedbackDiagnosticsSchema.nullable(),
258
+ nxtVersion: version.nullable(),
259
+ createdAt: timestamp,
260
+ }),
261
+ )
262
+ .optional(),
263
+ })
264
+ export type CommunityFeedback = z.output<typeof CommunityFeedbackSchema>
265
+ export const CommunityFeedbackListSchema = z.object({
266
+ items: z.array(CommunityFeedbackSummarySchema),
267
+ cursor: z.string().nullable(),
268
+ })
269
+ export const CommunityFeedbackChangesSchema = z.object({
270
+ events: z.array(CommunityFeedbackEventSchema),
271
+ cursor: z.number().int().nonnegative(),
272
+ hasMore: z.boolean(),
273
+ })
274
+ export type CommunityFeedbackChanges = z.output<typeof CommunityFeedbackChangesSchema>
275
+ export const CommunityFeedbackDigestSchema = z.object({
276
+ since: timestamp,
277
+ counts: z.object({
278
+ status: z.record(CommunityFeedbackStatusSchema, z.number().int().nonnegative()),
279
+ kind: z.record(CommunityFeedbackKindSchema, z.number().int().nonnegative()),
280
+ target: z.record(z.enum(['nxt', 'extension']), z.number().int().nonnegative()),
281
+ extensions: z.record(z.string(), z.number().int().nonnegative()),
282
+ }),
283
+ newItems: z.array(CommunityFeedbackSummarySchema),
284
+ awaitingReply: z.array(CommunityFeedbackSummarySchema),
285
+ mostAffected: z.array(CommunityFeedbackSummarySchema),
286
+ unconfirmedFixes: z.array(CommunityFeedbackSummarySchema),
287
+ fixedCount: z.number().int().nonnegative(),
288
+ })
289
+ export type CommunityFeedbackDigest = z.output<typeof CommunityFeedbackDigestSchema>
package/src/community.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod'
2
- import { ExtensionIdSchema } from './domain.js'
2
+ import { ExtensionIdSchema, ExtensionRevisionIdSchema } from './domain.js'
3
3
 
4
4
  /**
5
5
  * NekroNXT 社区在本机的投影。Host 代理社区开放接口并把结果归一到这些结构;社区新增的字段在 Host 处丢弃,
@@ -90,6 +90,8 @@ export const CommunityExtensionSortSchema = z.enum(['updated', 'score', 'downloa
90
90
  export const CommunityReleaseIdSchema = z.string().regex(/^rel_[0-9A-Za-z]+$/u)
91
91
 
92
92
  export const CommunityAccountSchema = z.object({
93
+ /** Immutable community identity; absent when connected to a community predating feedback support. */
94
+ id: z.string().min(1).max(100).optional(),
93
95
  handle: z.string().min(1).max(80),
94
96
  displayName: z.string().max(200),
95
97
  avatarUrl: z.string().url().nullable(),
@@ -100,6 +102,7 @@ export const CommunityEnvironmentSchema = z.enum(['production', 'staging', 'deve
100
102
  export type CommunityEnvironment = z.output<typeof CommunityEnvironmentSchema>
101
103
 
102
104
  export const CommunityStatusSchema = z.object({
105
+ features: z.array(z.string()).optional(),
103
106
  communityUrl: z.string().url(),
104
107
  /** 社区自我声明的环境;连不上或旧版社区时为 null。 */
105
108
  environment: CommunityEnvironmentSchema.nullable(),
@@ -131,6 +134,12 @@ export const CommunityReleaseSchema = z.object({
131
134
  requiresSdk: z.number().int().positive().nullable(),
132
135
  notes: z.string().max(2000),
133
136
  createdAt: z.number().int(),
137
+ /** Release number on the community line; absent from older communities. */
138
+ number: z.number().int().positive().optional(),
139
+ /** Revision the release contains and its lineage, so NekroNXT can tell fast-forward from divergence. */
140
+ revisionId: ExtensionRevisionIdSchema.optional(),
141
+ parentRevisionId: ExtensionRevisionIdSchema.nullable().optional(),
142
+ ancestry: z.array(ExtensionRevisionIdSchema).max(256).optional(),
134
143
  })
135
144
  export type CommunityRelease = z.output<typeof CommunityReleaseSchema>
136
145
 
@@ -157,6 +166,20 @@ export const CommunityExtensionSummarySchema = z.object({
157
166
  downloads: z.number().int().nonnegative(),
158
167
  updatedAt: z.number().int(),
159
168
  pageUrl: z.string().url(),
169
+ /**
170
+ * The extension and version this one is a branch of; null for an original line or an older community. Without a
171
+ * displayName the origin is not public on the community.
172
+ */
173
+ branchOf: z
174
+ .object({
175
+ extensionId: ExtensionIdSchema,
176
+ revisionId: ExtensionRevisionIdSchema.nullable(),
177
+ displayName: z.string().max(200).nullable(),
178
+ })
179
+ .nullable()
180
+ .optional(),
181
+ /** Public branches made from this extension. */
182
+ branchCount: z.number().int().nonnegative().optional(),
160
183
  })
161
184
  export type CommunityExtensionSummary = z.output<typeof CommunityExtensionSummarySchema>
162
185
 
@@ -220,6 +243,38 @@ export const CommunitySourceSchema = z.object({
220
243
  })
221
244
  export type CommunitySource = z.output<typeof CommunitySourceSchema>
222
245
 
246
+ /**
247
+ * How this machine's current version of one extension relates to the community's newest public release, with the
248
+ * action that relation calls for. `role` says whether this machine may publish the line or only follows it.
249
+ */
250
+ export const CommunitySyncItemSchema = z.object({
251
+ extensionId: ExtensionIdSchema,
252
+ displayName: z.string(),
253
+ communityUrl: z.string().url(),
254
+ role: z.enum(['owner', 'subscriber']),
255
+ currentRevisionId: ExtensionRevisionIdSchema,
256
+ currentVersionLabel: z.string(),
257
+ /** Release the current version is, when it is one. */
258
+ currentRelease: z.object({ id: CommunityReleaseIdSchema, number: z.number().int().positive().nullable() }).nullable(),
259
+ /** `remote-ahead`: update; `local-ahead`: publish (owner) or local changes (subscriber); `diverged`: merge first. */
260
+ status: z.enum(['same', 'remote-ahead', 'local-ahead', 'diverged', 'unrelated', 'unknown']),
261
+ found: z.boolean(),
262
+ delisted: z.boolean(),
263
+ delistedReason: z.string().nullable(),
264
+ /** The release the current version came from was withdrawn by its author. */
265
+ releaseWithdrawn: z.boolean(),
266
+ latest: CommunityReleaseSchema.nullable(),
267
+ /** The user chose to keep local changes over `latest`; it is not offered again. */
268
+ dismissed: z.boolean(),
269
+ addedPermissions: z.array(CommunityPermissionItemSchema),
270
+ })
271
+ export type CommunitySyncItem = z.output<typeof CommunitySyncItemSchema>
272
+
273
+ export const CommunitySyncSchema = z.object({
274
+ checkedAt: z.number().int().nullable(),
275
+ items: z.array(CommunitySyncItemSchema),
276
+ })
277
+
223
278
  export const CommunityInstalledItemSchema = z.object({
224
279
  extensionId: ExtensionIdSchema,
225
280
  displayName: z.string(),
package/src/domain.ts CHANGED
@@ -1,5 +1,26 @@
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
+ })
22
+ .strict()
23
+
3
24
  const brandedId = <Prefix extends string, Brand extends string>(prefix: Prefix, brand: Brand) =>
4
25
  z
5
26
  .string()
@@ -28,8 +49,10 @@ export const DshPluginEntryIdSchema = brandedId('dse', 'DshPluginEntryId')
28
49
  export const HostUiPageInstanceIdSchema = brandedId('hup', 'HostUiPageInstanceId')
29
50
  export const AuthoringTaskIdSchema = brandedId('aut', 'AuthoringTaskId')
30
51
  export const AuthoringAttemptIdSchema = brandedId('aua', 'AuthoringAttemptId')
52
+ export const SubConversationIdSchema = brandedId('cnv', 'SubConversationId')
31
53
 
32
54
  export type AgentId = z.infer<typeof AgentIdSchema>
55
+ export type SubConversationId = z.infer<typeof SubConversationIdSchema>
33
56
  export type AgentRevisionId = z.infer<typeof AgentRevisionIdSchema>
34
57
  export type ConnectionId = z.infer<typeof ConnectionIdSchema>
35
58
  export type ConnectionEventId = z.infer<typeof ConnectionEventIdSchema>
@@ -259,7 +282,18 @@ export const RichTargetUrlSchema = z
259
282
  export const MessagePartSchema = z.discriminatedUnion('type', [
260
283
  z.object({ type: z.literal('text'), text: z.string() }).strict(),
261
284
  z.object({ type: z.literal('mention'), memberId: ChannelMemberIdSchema }).strict(),
262
- z.object({ type: z.literal('image'), assetId: AssetIdSchema, alt: z.string().optional() }).strict(),
285
+ z
286
+ .object({
287
+ type: z.literal('image'),
288
+ assetId: AssetIdSchema,
289
+ alt: z.string().optional(),
290
+ /**
291
+ * The platform reported the picture as a sticker. A hint only: a sticker dragged in from a desktop client and
292
+ * pictures from platforms without this signal arrive unmarked.
293
+ */
294
+ sticker: z.literal(true).optional(),
295
+ })
296
+ .strict(),
263
297
  z.object({ type: z.literal('file'), assetId: AssetIdSchema, name: z.string().optional() }).strict(),
264
298
  z.object({ type: z.literal('audio'), assetId: AssetIdSchema }).strict(),
265
299
  z.object({ type: z.literal('quote'), messageId: LogicalMessageIdSchema }).strict(),
@@ -287,19 +321,24 @@ export function parseMessageParts(input: unknown): MessagePart[] {
287
321
  return MessagePartsSchema.parse(input)
288
322
  }
289
323
 
290
- const collectRichAssetIds = (value: JsonValue | undefined, into: AssetId[]): void => {
324
+ const collectRichAssetIds = (value: JsonValue | undefined, into: AssetId[], imagesOnly = false): void => {
291
325
  if (value === null || value === undefined || typeof value === 'boolean' || typeof value === 'number') return
292
326
  if (typeof value === 'string') return
293
327
  if (Array.isArray(value)) {
294
- for (const item of value) collectRichAssetIds(item, into)
328
+ for (const item of value) collectRichAssetIds(item, into, imagesOnly)
295
329
  return
296
330
  }
297
331
  for (const [key, child] of Object.entries(value)) {
298
- if ((key === 'imageAssetId' || key === 'previewAssetId') && typeof child === 'string') {
332
+ if (
333
+ (key === 'imageAssetId' ||
334
+ key === 'previewAssetId' ||
335
+ (!imagesOnly && (key === 'audioAssetId' || key === 'fileAssetId'))) &&
336
+ typeof child === 'string'
337
+ ) {
299
338
  const parsed = AssetIdSchema.safeParse(child)
300
339
  if (parsed.success) into.push(parsed.data)
301
340
  } else {
302
- collectRichAssetIds(child, into)
341
+ collectRichAssetIds(child, into, imagesOnly)
303
342
  }
304
343
  }
305
344
  }
@@ -315,6 +354,15 @@ export const messagePartAssetIds = (part: MessagePart): readonly AssetId[] => {
315
354
 
316
355
  export const messagePartAssetId = (part: MessagePart): AssetId | undefined => messagePartAssetIds(part)[0]
317
356
 
357
+ /** Only actual pictures enter visual context; forwarded files and recordings retain their own media kind. */
358
+ export const messagePartImageAssetIds = (part: MessagePart): readonly AssetId[] => {
359
+ if (part.type === 'image') return [part.assetId]
360
+ if (part.type !== 'rich') return []
361
+ const ids: AssetId[] = part.previewAssetId ? [part.previewAssetId] : []
362
+ collectRichAssetIds(part.extension, ids, true)
363
+ return ids
364
+ }
365
+
318
366
  const asJsonRecord = (value: JsonValue | undefined): Readonly<Record<string, JsonValue>> | undefined =>
319
367
  value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined
320
368
 
@@ -332,7 +380,12 @@ const richItemLine = (value: JsonValue): string | undefined => {
332
380
  : undefined
333
381
  const imageName = typeof item['imageName'] === 'string' ? item['imageName'] : undefined
334
382
  const imageText = typeof item['imageAssetId'] === 'string' ? `[图片${imageName ? ` ${imageName}` : ''}]` : undefined
335
- const body = [text, cardSummary ? `[卡片] ${cardSummary}` : undefined, imageText]
383
+ const audioText = typeof item['audioAssetId'] === 'string' ? `[语音 ${item['audioAssetId']}]` : undefined
384
+ const fileText =
385
+ typeof item['fileAssetId'] === 'string'
386
+ ? `[${typeof item['fileMediaType'] === 'string' && item['fileMediaType'].startsWith('video/') ? '视频' : '文件'} ${item['fileAssetId']}${typeof item['fileName'] === 'string' ? `:${item['fileName']}` : ''}]`
387
+ : undefined
388
+ const body = [text, cardSummary ? `[卡片] ${cardSummary}` : undefined, imageText, audioText, fileText]
336
389
  .filter((part): part is string => Boolean(part && part.length > 0))
337
390
  .join(' ')
338
391
  if (!sender && !body) return undefined
@@ -4,9 +4,13 @@ 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.
8
9
  */
9
- export const EXTENSION_SDK_LEVEL = 8
10
+ export const EXTENSION_SDK_LEVEL = 11
11
+
12
+ /** Lineage, `dataVersion` and administrator-only commands need this level; packages that carry them declare it. */
13
+ export const EXTENSION_GOVERNANCE_SDK_LEVEL = 10
10
14
 
11
15
  /**
12
16
  * The first NekroNXT version of each capability level, so users read "needs NekroNXT 0.5.0" rather than a level
@@ -16,6 +20,9 @@ export const EXTENSION_SDK_RELEASES: readonly { readonly level: number; readonly
16
20
  { level: 6, version: '0.3.0' },
17
21
  { level: 7, version: '0.4.0' },
18
22
  { level: 8, version: '0.5.0' },
23
+ { level: 9, version: '0.6.0' },
24
+ { level: 10, version: '0.6.0' },
25
+ { level: 11, version: '0.6.0' },
19
26
  ]
20
27
 
21
28
  /** The oldest NekroNXT version that supports `level`; undefined for a level no release supports yet. */