@nekro-nxt/contracts 0.0.0-stage → 0.1.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/src/domain.ts ADDED
@@ -0,0 +1,365 @@
1
+ import { z } from 'zod'
2
+
3
+ const brandedId = <Prefix extends string, Brand extends string>(prefix: Prefix, brand: Brand) =>
4
+ z
5
+ .string()
6
+ .regex(new RegExp(`^${prefix}_[0-9A-Za-z]+$`), `${brand} has an invalid format.`)
7
+ .brand(brand)
8
+
9
+ export const AgentIdSchema = brandedId('agt', 'AgentId')
10
+ export const AgentRevisionIdSchema = brandedId('arev', 'AgentRevisionId')
11
+ export const ConnectionIdSchema = brandedId('con', 'ConnectionId')
12
+ export const ConnectionEventIdSchema = brandedId('cev', 'ConnectionEventId')
13
+ export const ChannelIdSchema = brandedId('chn', 'ChannelId')
14
+ export const ChannelEventIdSchema = brandedId('evt', 'ChannelEventId')
15
+ export const PlatformIdentityIdSchema = brandedId('pid', 'PlatformIdentityId')
16
+ export const ChannelMemberIdSchema = brandedId('mbr', 'ChannelMemberId')
17
+ export const AdmissionIdSchema = brandedId('adm', 'AdmissionId')
18
+ export const OutboundIntentIdSchema = brandedId('out', 'OutboundIntentId')
19
+ export const LogicalMessageIdSchema = brandedId('msg', 'LogicalMessageId')
20
+ export const PhysicalDeliveryIdSchema = brandedId('phy', 'PhysicalDeliveryId')
21
+ export const AssetIdSchema = brandedId('ast', 'AssetId')
22
+ export const EpisodeIdSchema = brandedId('eps', 'EpisodeId')
23
+ export const EpisodeHandoffIdSchema = brandedId('hof', 'EpisodeHandoffId')
24
+ export const ExtensionIdSchema = brandedId('ext', 'ExtensionId')
25
+ export const ExtensionRevisionIdSchema = brandedId('xrv', 'ExtensionRevisionId')
26
+ export const DshPluginPackageIdSchema = brandedId('dsp', 'DshPluginPackageId')
27
+ export const DshPluginEntryIdSchema = brandedId('dse', 'DshPluginEntryId')
28
+ export const HostUiPageInstanceIdSchema = brandedId('hup', 'HostUiPageInstanceId')
29
+ export const AuthoringTaskIdSchema = brandedId('aut', 'AuthoringTaskId')
30
+ export const AuthoringAttemptIdSchema = brandedId('aua', 'AuthoringAttemptId')
31
+
32
+ export type AgentId = z.infer<typeof AgentIdSchema>
33
+ export type AgentRevisionId = z.infer<typeof AgentRevisionIdSchema>
34
+ export type ConnectionId = z.infer<typeof ConnectionIdSchema>
35
+ export type ConnectionEventId = z.infer<typeof ConnectionEventIdSchema>
36
+ export type ChannelId = z.infer<typeof ChannelIdSchema>
37
+ export type ChannelEventId = z.infer<typeof ChannelEventIdSchema>
38
+ export type PlatformIdentityId = z.infer<typeof PlatformIdentityIdSchema>
39
+ export type ChannelMemberId = z.infer<typeof ChannelMemberIdSchema>
40
+ export type AdmissionId = z.infer<typeof AdmissionIdSchema>
41
+ export type OutboundIntentId = z.infer<typeof OutboundIntentIdSchema>
42
+ export type LogicalMessageId = z.infer<typeof LogicalMessageIdSchema>
43
+ export type PhysicalDeliveryId = z.infer<typeof PhysicalDeliveryIdSchema>
44
+ export type AssetId = z.infer<typeof AssetIdSchema>
45
+ export type EpisodeId = z.infer<typeof EpisodeIdSchema>
46
+ export type EpisodeHandoffId = z.infer<typeof EpisodeHandoffIdSchema>
47
+ export type ExtensionId = z.infer<typeof ExtensionIdSchema>
48
+ export type ExtensionRevisionId = z.infer<typeof ExtensionRevisionIdSchema>
49
+ export type DshPluginPackageId = z.infer<typeof DshPluginPackageIdSchema>
50
+ export type DshPluginEntryId = z.infer<typeof DshPluginEntryIdSchema>
51
+ export type HostUiPageInstanceId = z.infer<typeof HostUiPageInstanceIdSchema>
52
+ export type AuthoringTaskId = z.infer<typeof AuthoringTaskIdSchema>
53
+ export type AuthoringAttemptId = z.infer<typeof AuthoringAttemptIdSchema>
54
+
55
+ export const AdapterActivityKeySchema = z
56
+ .string()
57
+ .trim()
58
+ .min(1)
59
+ .max(128)
60
+ .regex(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/u, 'Adapter activity key has an invalid format.')
61
+
62
+ export type AdapterActivityKey = z.infer<typeof AdapterActivityKeySchema>
63
+
64
+ const PromptTextSegmentSchema = z.object({ type: z.literal('text'), text: z.string() }).strict()
65
+
66
+ const PromptReferenceLabelSchema = z.string().trim().min(1).max(120)
67
+
68
+ const PromptReferenceSegmentSchema = z.discriminatedUnion('kind', [
69
+ z
70
+ .object({
71
+ type: z.literal('reference'),
72
+ kind: z.literal('platform-user'),
73
+ targetId: PlatformIdentityIdSchema,
74
+ labelSnapshot: PromptReferenceLabelSchema,
75
+ })
76
+ .strict(),
77
+ z
78
+ .object({
79
+ type: z.literal('reference'),
80
+ kind: z.literal('channel'),
81
+ targetId: ChannelIdSchema,
82
+ labelSnapshot: PromptReferenceLabelSchema,
83
+ })
84
+ .strict(),
85
+ z
86
+ .object({
87
+ type: z.literal('reference'),
88
+ kind: z.literal('extension'),
89
+ targetId: ExtensionIdSchema,
90
+ labelSnapshot: PromptReferenceLabelSchema,
91
+ })
92
+ .strict(),
93
+ ])
94
+
95
+ export const PromptSegmentSchema = z.union([PromptTextSegmentSchema, PromptReferenceSegmentSchema])
96
+
97
+ export const PROMPT_DOCUMENT_MAX_REFERENCES = 128
98
+ export const PROMPT_DOCUMENT_MAX_PLAIN_TEXT_LENGTH = 64 * 1024
99
+
100
+ export const PromptDocumentV1Schema = z
101
+ .object({ version: z.literal(1), segments: z.array(PromptSegmentSchema) })
102
+ .strict()
103
+ .superRefine((document, context) => {
104
+ const referenceCount = document.segments.filter((segment) => segment.type === 'reference').length
105
+ if (referenceCount > PROMPT_DOCUMENT_MAX_REFERENCES) {
106
+ context.addIssue({
107
+ code: 'custom',
108
+ message: `Prompt document must not contain more than ${PROMPT_DOCUMENT_MAX_REFERENCES} references.`,
109
+ path: ['segments'],
110
+ })
111
+ }
112
+ const plainTextLength = document.segments.reduce(
113
+ (length, segment) => length + (segment.type === 'text' ? segment.text.length : segment.labelSnapshot.length + 1),
114
+ 0,
115
+ )
116
+ if (plainTextLength > PROMPT_DOCUMENT_MAX_PLAIN_TEXT_LENGTH) {
117
+ context.addIssue({
118
+ code: 'custom',
119
+ message: `Prompt document plain-text projection must not exceed ${PROMPT_DOCUMENT_MAX_PLAIN_TEXT_LENGTH} characters.`,
120
+ path: ['segments'],
121
+ })
122
+ }
123
+ })
124
+
125
+ export type PromptSegment = z.output<typeof PromptSegmentSchema>
126
+ export type PromptDocumentV1 = z.output<typeof PromptDocumentV1Schema>
127
+
128
+ export const promptDocumentFromText = (text: string): PromptDocumentV1 =>
129
+ normalizePromptDocument({ version: 1, segments: text.length === 0 ? [] : [{ type: 'text', text }] })
130
+
131
+ export const promptDocumentPlainText = (document: PromptDocumentV1): string =>
132
+ document.segments.map((segment) => (segment.type === 'text' ? segment.text : `@${segment.labelSnapshot}`)).join('')
133
+
134
+ export const normalizePromptDocument = (input: unknown): PromptDocumentV1 => {
135
+ const parsed = PromptDocumentV1Schema.parse(input)
136
+ const segments: PromptSegment[] = []
137
+ for (const segment of parsed.segments) {
138
+ if (segment.type === 'text') {
139
+ if (segment.text.length === 0) continue
140
+ const previous = segments.at(-1)
141
+ if (previous?.type === 'text') {
142
+ segments[segments.length - 1] = { type: 'text', text: `${previous.text}${segment.text}` }
143
+ } else {
144
+ segments.push(segment)
145
+ }
146
+ continue
147
+ }
148
+ segments.push(segment)
149
+ }
150
+ return PromptDocumentV1Schema.parse({ version: 1, segments })
151
+ }
152
+
153
+ export type JsonValue = null | boolean | number | string | JsonValue[] | { readonly [key: string]: JsonValue }
154
+
155
+ export const JsonValueSchema = z.json()
156
+
157
+ export function parseJsonValue(input: unknown): JsonValue {
158
+ return JsonValueSchema.parse(input)
159
+ }
160
+
161
+ /** Package identity and source for one DSH extension visible in the current Host. */
162
+ export interface DshPluginCatalogEntry {
163
+ readonly packageName: string
164
+ readonly packageVersion: string
165
+ readonly origin: 'builtin' | 'profile' | 'dynamic' | 'installed'
166
+ readonly settingsNamespaces: readonly string[]
167
+ readonly loadError?: { readonly code: string; readonly message: string }
168
+ }
169
+
170
+ export type DshPluginInstallSource = 'registry' | 'tarball' | 'imported'
171
+ export type DshPluginActivationScope = 'host' | 'agent'
172
+
173
+ export interface DshPluginPackageRecord {
174
+ readonly id: DshPluginPackageId
175
+ readonly packageName: string
176
+ readonly packageVersion: string
177
+ readonly source: DshPluginInstallSource
178
+ readonly packageDigest: string
179
+ readonly integrity?: string
180
+ readonly lockfileDigest: string
181
+ readonly manifest: JsonValue
182
+ readonly approvedBuilds: readonly string[]
183
+ readonly installedAt: number
184
+ }
185
+
186
+ export interface DshPluginEntryRecord {
187
+ readonly id: DshPluginEntryId
188
+ readonly packageId: DshPluginPackageId
189
+ readonly entryKey: string
190
+ readonly moduleName: string
191
+ readonly suggestedScope: DshPluginActivationScope
192
+ readonly selectedScope?: DshPluginActivationScope
193
+ readonly config: JsonValue
194
+ readonly createdAt: number
195
+ }
196
+
197
+ export interface DshPluginActivationRecord {
198
+ readonly entryId: DshPluginEntryId
199
+ readonly targetKey: string
200
+ readonly target: DshPluginActivationScope
201
+ readonly agentId?: AgentId
202
+ readonly activatedAt: number
203
+ }
204
+
205
+ export interface DshPluginDiagnosticRecord {
206
+ readonly entryId: DshPluginEntryId
207
+ readonly targetKey: string
208
+ readonly status: 'active' | 'load-failed' | 'restore-failed' | 'dispose-failed'
209
+ readonly phase: 'import' | 'apply' | 'update' | 'dispose' | 'restore'
210
+ readonly message?: string
211
+ readonly observedAt: number
212
+ }
213
+
214
+ export interface DshSettingsNamespaceView {
215
+ readonly ns: string
216
+ readonly schema: unknown
217
+ readonly resolved: unknown
218
+ readonly base?: unknown
219
+ readonly user?: unknown
220
+ readonly applies: 'live' | 'restart'
221
+ readonly secrets: readonly { readonly path: readonly string[]; readonly set: boolean }[]
222
+ readonly revision: number
223
+ readonly writable: boolean
224
+ readonly owner?: { readonly packageName: string; readonly packageVersion: string }
225
+ }
226
+
227
+ export type DshSettingsPathOperation =
228
+ | { readonly op: 'set'; readonly path: readonly string[]; readonly value: unknown }
229
+ | { readonly op: 'unset'; readonly path: readonly string[] }
230
+
231
+ export interface DshSettingsMutationRequest {
232
+ readonly expectedRevision: number
233
+ readonly ops: readonly DshSettingsPathOperation[]
234
+ }
235
+
236
+ export interface DshCredentialView {
237
+ readonly configured: boolean
238
+ readonly source?: string
239
+ readonly writable: boolean
240
+ }
241
+
242
+ const utf8ByteLength = (value: string): number => new TextEncoder().encode(value).byteLength
243
+
244
+ export const RICH_EXTENSION_MAX_BYTES = 64 * 1024
245
+
246
+ export const RichExtensionSchema = JsonValueSchema.refine(
247
+ (value) => utf8ByteLength(JSON.stringify(value)) <= RICH_EXTENSION_MAX_BYTES,
248
+ `rich extension must not exceed ${RICH_EXTENSION_MAX_BYTES} bytes.`,
249
+ )
250
+
251
+ export const RichTargetUrlSchema = z
252
+ .url()
253
+ .max(2048)
254
+ .refine((value) => {
255
+ const protocol = new URL(value).protocol
256
+ return protocol === 'http:' || protocol === 'https:'
257
+ }, 'rich target URL must use HTTP or HTTPS.')
258
+
259
+ export const MessagePartSchema = z.discriminatedUnion('type', [
260
+ z.object({ type: z.literal('text'), text: z.string() }).strict(),
261
+ z.object({ type: z.literal('mention'), memberId: ChannelMemberIdSchema }).strict(),
262
+ z.object({ type: z.literal('image'), assetId: AssetIdSchema, alt: z.string().optional() }).strict(),
263
+ z.object({ type: z.literal('file'), assetId: AssetIdSchema, name: z.string().optional() }).strict(),
264
+ z.object({ type: z.literal('audio'), assetId: AssetIdSchema }).strict(),
265
+ z.object({ type: z.literal('quote'), messageId: LogicalMessageIdSchema }).strict(),
266
+ z
267
+ .object({
268
+ type: z.literal('rich'),
269
+ adapterKey: z.string().trim().min(1).max(64),
270
+ kind: z.string().trim().min(1).max(64),
271
+ summary: z.string().trim().min(1).max(500),
272
+ title: z.string().trim().min(1).max(200).optional(),
273
+ source: z.string().trim().min(1).max(80).optional(),
274
+ targetUrl: RichTargetUrlSchema.optional(),
275
+ previewAssetId: AssetIdSchema.optional(),
276
+ extension: RichExtensionSchema.optional(),
277
+ })
278
+ .strict(),
279
+ ])
280
+
281
+ export type MessagePart = z.infer<typeof MessagePartSchema>
282
+
283
+ export const MessagePartsSchema = z.array(MessagePartSchema)
284
+ export const NonEmptyMessagePartsSchema = MessagePartsSchema.min(1)
285
+
286
+ export function parseMessageParts(input: unknown): MessagePart[] {
287
+ return MessagePartsSchema.parse(input)
288
+ }
289
+
290
+ const collectRichAssetIds = (value: JsonValue | undefined, into: AssetId[]): void => {
291
+ if (value === null || value === undefined || typeof value === 'boolean' || typeof value === 'number') return
292
+ if (typeof value === 'string') return
293
+ if (Array.isArray(value)) {
294
+ for (const item of value) collectRichAssetIds(item, into)
295
+ return
296
+ }
297
+ for (const [key, child] of Object.entries(value)) {
298
+ if ((key === 'imageAssetId' || key === 'previewAssetId') && typeof child === 'string') {
299
+ const parsed = AssetIdSchema.safeParse(child)
300
+ if (parsed.success) into.push(parsed.data)
301
+ } else {
302
+ collectRichAssetIds(child, into)
303
+ }
304
+ }
305
+ }
306
+
307
+ export const messagePartAssetIds = (part: MessagePart): readonly AssetId[] => {
308
+ if (part.type === 'image' || part.type === 'file' || part.type === 'audio') return [part.assetId]
309
+ if (part.type !== 'rich') return []
310
+ const ids: AssetId[] = []
311
+ if (part.previewAssetId) ids.push(part.previewAssetId)
312
+ collectRichAssetIds(part.extension, ids)
313
+ return ids
314
+ }
315
+
316
+ export const messagePartAssetId = (part: MessagePart): AssetId | undefined => messagePartAssetIds(part)[0]
317
+
318
+ const asJsonRecord = (value: JsonValue | undefined): Readonly<Record<string, JsonValue>> | undefined =>
319
+ value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined
320
+
321
+ const richItemLine = (value: JsonValue): string | undefined => {
322
+ const item = asJsonRecord(value)
323
+ if (!item) return undefined
324
+ const sender = typeof item['sender'] === 'string' ? item['sender'] : undefined
325
+ const text = typeof item['text'] === 'string' ? item['text'] : undefined
326
+ const card = asJsonRecord(item['card'])
327
+ const cardSummary =
328
+ typeof card?.['summary'] === 'string'
329
+ ? card['summary']
330
+ : typeof card?.['title'] === 'string'
331
+ ? card['title']
332
+ : undefined
333
+ const imageName = typeof item['imageName'] === 'string' ? item['imageName'] : undefined
334
+ const imageText = typeof item['imageAssetId'] === 'string' ? `[图片${imageName ? ` ${imageName}` : ''}]` : undefined
335
+ const body = [text, cardSummary ? `[卡片] ${cardSummary}` : undefined, imageText]
336
+ .filter((part): part is string => Boolean(part && part.length > 0))
337
+ .join(' ')
338
+ if (!sender && !body) return undefined
339
+ return sender ? `${sender}:${body}` : body
340
+ }
341
+
342
+ /** Flatten a rich part for search and model context. */
343
+ export const richPartContextText = (part: Extract<MessagePart, { type: 'rich' }>): string => {
344
+ const header = [part.summary, part.title, part.source].filter(
345
+ (value, index, all): value is string =>
346
+ typeof value === 'string' && value.length > 0 && all.indexOf(value) === index,
347
+ )
348
+ const items = asJsonRecord(part.extension)?.['items']
349
+ const itemLines = Array.isArray(items)
350
+ ? items.flatMap((item) => {
351
+ const line = richItemLine(item)
352
+ return line === undefined ? [] : [line]
353
+ })
354
+ : []
355
+ return [...header, ...itemLines].join('\n')
356
+ }
357
+
358
+ export const messagePartsSearchText = (parts: readonly MessagePart[]): string =>
359
+ parts
360
+ .flatMap((part) => {
361
+ if (part.type === 'text') return [part.text]
362
+ if (part.type === 'rich') return [richPartContextText(part)]
363
+ return []
364
+ })
365
+ .join('\n')