@nekro-nxt/contracts 0.3.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.
- package/README.md +2 -0
- package/dist/index.mjs +2512 -78
- 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 +251 -13
- package/lib/types/community.d.ts.map +1 -1
- package/lib/types/domain.d.ts +33 -0
- package/lib/types/domain.d.ts.map +1 -1
- package/lib/types/extension-capabilities.d.ts +105 -3
- package/lib/types/extension-capabilities.d.ts.map +1 -1
- package/lib/types/extension-ui.d.ts +86 -7
- package/lib/types/extension-ui.d.ts.map +1 -1
- package/lib/types/host-api.d.ts +11109 -3874
- package/lib/types/host-api.d.ts.map +1 -1
- package/lib/types/index.d.ts +4 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/logs.d.ts +355 -0
- package/lib/types/logs.d.ts.map +1 -0
- 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 +56 -1
- package/src/domain.ts +59 -6
- package/src/extension-capabilities.ts +159 -8
- package/src/extension-ui.ts +40 -0
- package/src/host-api.ts +1540 -67
- package/src/index.ts +5 -0
- package/src/logs.ts +138 -0
- package/src/management-api.ts +1 -1
package/src/commands.ts
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import {
|
|
3
|
+
AgentIdSchema,
|
|
4
|
+
AgentRevisionIdSchema,
|
|
5
|
+
ChannelEventIdSchema,
|
|
6
|
+
ChannelIdSchema,
|
|
7
|
+
ConnectionIdSchema,
|
|
8
|
+
ExtensionRevisionIdSchema,
|
|
9
|
+
MessagePartSchema,
|
|
10
|
+
OutboundIntentIdSchema,
|
|
11
|
+
type AgentId,
|
|
12
|
+
type ChannelId,
|
|
13
|
+
type ChannelMemberId,
|
|
14
|
+
type MessagePart,
|
|
15
|
+
} from './domain.js'
|
|
16
|
+
|
|
17
|
+
export const COMMAND_DEFAULT_TIMEOUT_SECONDS = 60
|
|
18
|
+
export const COMMAND_MAX_TIMEOUT_SECONDS = 600
|
|
19
|
+
/**
|
|
20
|
+
* Who may run a command. `channel-admin` is granted per channel; `admin` is an instance administrator (the signed-in
|
|
21
|
+
* console or a platform account listed in Settings → Administrators) and also holds every channel's `channel-admin`.
|
|
22
|
+
*/
|
|
23
|
+
export const CommandPermissionSchema = z.enum(['public', 'channel-admin', 'admin'])
|
|
24
|
+
export type CommandPermission = z.output<typeof CommandPermissionSchema>
|
|
25
|
+
const nameSchema = z
|
|
26
|
+
.string()
|
|
27
|
+
.min(1)
|
|
28
|
+
.max(64)
|
|
29
|
+
.regex(/^[\p{L}\p{N}_-]+$/u)
|
|
30
|
+
const scalarSchema = z.union([z.string(), z.number().finite(), z.boolean()])
|
|
31
|
+
export const CommandParameterSchema = z
|
|
32
|
+
.object({
|
|
33
|
+
name: nameSchema,
|
|
34
|
+
description: z.string().max(500).optional(),
|
|
35
|
+
kind: z.enum(['positional', 'option']),
|
|
36
|
+
type: z.enum(['string', 'number', 'integer', 'boolean']),
|
|
37
|
+
required: z.boolean().optional(),
|
|
38
|
+
default: scalarSchema.optional(),
|
|
39
|
+
choices: z.array(scalarSchema).min(1).max(100).optional(),
|
|
40
|
+
min: z.number().finite().optional(),
|
|
41
|
+
max: z.number().finite().optional(),
|
|
42
|
+
})
|
|
43
|
+
.strict()
|
|
44
|
+
export const CommandDefinitionSchema = z
|
|
45
|
+
.object({
|
|
46
|
+
name: nameSchema,
|
|
47
|
+
aliases: z.array(nameSchema).max(16).default([]),
|
|
48
|
+
description: z.string().min(1).max(1000),
|
|
49
|
+
parameters: z.array(CommandParameterSchema).max(32).default([]),
|
|
50
|
+
examples: z.array(z.string().max(2000)).max(16).default([]),
|
|
51
|
+
minimumPermission: CommandPermissionSchema.default('public'),
|
|
52
|
+
enabledByDefault: z.boolean().default(true),
|
|
53
|
+
/**
|
|
54
|
+
* Where the result goes when an administrator runs the command from the console (Web or Desktop) in an external
|
|
55
|
+
* channel. `local` (default) shows it in the console only; `channel` also sends it through the bot account, and
|
|
56
|
+
* the console labels the command accordingly. Platform-originated commands always reply in their conversation.
|
|
57
|
+
*/
|
|
58
|
+
consoleReply: z.enum(['local', 'channel']).default('local'),
|
|
59
|
+
/**
|
|
60
|
+
* How long an extension handler may run before it is cancelled and recorded as timed out, so a stuck handler
|
|
61
|
+
* cannot hold the channel's command queue. Built-in commands ignore it.
|
|
62
|
+
*/
|
|
63
|
+
timeoutSeconds: z.number().int().min(1).max(COMMAND_MAX_TIMEOUT_SECONDS).default(COMMAND_DEFAULT_TIMEOUT_SECONDS),
|
|
64
|
+
})
|
|
65
|
+
.strict()
|
|
66
|
+
.superRefine((definition, ctx) => {
|
|
67
|
+
const names = [definition.name, ...definition.aliases]
|
|
68
|
+
if (new Set(names).size !== names.length) ctx.addIssue({ code: 'custom', message: '命令名称和别名不能重复。' })
|
|
69
|
+
if (new Set(definition.parameters.map((p) => p.name)).size !== definition.parameters.length)
|
|
70
|
+
ctx.addIssue({ code: 'custom', message: '参数名称不能重复。' })
|
|
71
|
+
let optional = false
|
|
72
|
+
for (const p of definition.parameters) {
|
|
73
|
+
if (p.min !== undefined && p.max !== undefined && p.min > p.max)
|
|
74
|
+
ctx.addIssue({ code: 'custom', message: `参数 ${p.name} 的范围无效。` })
|
|
75
|
+
if (p.kind === 'positional') {
|
|
76
|
+
if (optional && p.required) ctx.addIssue({ code: 'custom', message: '必填位置参数必须在可选参数之前。' })
|
|
77
|
+
if (!p.required || p.default !== undefined) optional = true
|
|
78
|
+
}
|
|
79
|
+
if (p.default !== undefined) {
|
|
80
|
+
try {
|
|
81
|
+
if (validateValue(p, String(p.default)) !== p.default) throw new Error('默认值类型不匹配。')
|
|
82
|
+
} catch {
|
|
83
|
+
ctx.addIssue({ code: 'custom', message: `参数 ${p.name} 的默认值无效。` })
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
export type CommandDefinition = z.output<typeof CommandDefinitionSchema>
|
|
89
|
+
/** What an extension author writes: defaults are optional. */
|
|
90
|
+
export type CommandDefinitionInput = z.input<typeof CommandDefinitionSchema>
|
|
91
|
+
export const CommandPolicySchema = z
|
|
92
|
+
.object({ enabled: z.boolean().optional(), permission: CommandPermissionSchema.optional() })
|
|
93
|
+
.strict()
|
|
94
|
+
export type CommandPolicy = z.output<typeof CommandPolicySchema>
|
|
95
|
+
export const CommandExecutionIdentitySchema = z
|
|
96
|
+
.object({
|
|
97
|
+
source: z.enum(['web', 'platform']),
|
|
98
|
+
connectionId: ConnectionIdSchema.optional(),
|
|
99
|
+
platformUserId: z.string().optional(),
|
|
100
|
+
displayName: z.string(),
|
|
101
|
+
viewerId: z.string().optional(),
|
|
102
|
+
})
|
|
103
|
+
.strict()
|
|
104
|
+
export type CommandExecutionIdentity = z.output<typeof CommandExecutionIdentitySchema>
|
|
105
|
+
export const CommandResultSchema = z
|
|
106
|
+
.object({
|
|
107
|
+
parts: z
|
|
108
|
+
.array(MessagePartSchema)
|
|
109
|
+
.min(1)
|
|
110
|
+
.max(32)
|
|
111
|
+
.refine(
|
|
112
|
+
(parts) => parts.every((p) => ['text', 'image', 'file'].includes(p.type)),
|
|
113
|
+
'命令结果仅支持文本、图片和文件。',
|
|
114
|
+
),
|
|
115
|
+
summary: z.string().max(2000).optional(),
|
|
116
|
+
})
|
|
117
|
+
.strict()
|
|
118
|
+
export type CommandResult = z.output<typeof CommandResultSchema>
|
|
119
|
+
/** An execution may send a few progress replies before its final result. */
|
|
120
|
+
export const COMMAND_MAX_PROGRESS_REPLIES = 5
|
|
121
|
+
export const CommandReplySchema = z
|
|
122
|
+
.object({
|
|
123
|
+
/** Position within the execution; the final result always has the highest ordinal. */
|
|
124
|
+
ordinal: z.number().int().nonnegative(),
|
|
125
|
+
kind: z.enum(['progress', 'final']),
|
|
126
|
+
parts: CommandResultSchema.shape.parts,
|
|
127
|
+
/** Set once the reply has an Outbound; absent with `error` means planning failed. */
|
|
128
|
+
intentId: OutboundIntentIdSchema.optional(),
|
|
129
|
+
error: z.string().optional(),
|
|
130
|
+
})
|
|
131
|
+
.strict()
|
|
132
|
+
export type CommandReply = z.output<typeof CommandReplySchema>
|
|
133
|
+
export const CommandExecutionRecordSchema = z
|
|
134
|
+
.object({
|
|
135
|
+
id: z.string().min(1),
|
|
136
|
+
channelId: ChannelIdSchema,
|
|
137
|
+
source: z.enum(['web', 'platform']),
|
|
138
|
+
requestId: z.string().min(1).max(200),
|
|
139
|
+
eventId: ChannelEventIdSchema.optional(),
|
|
140
|
+
rawText: z.string(),
|
|
141
|
+
identity: CommandExecutionIdentitySchema,
|
|
142
|
+
commandName: z.string().optional(),
|
|
143
|
+
commandVersion: z.string().optional(),
|
|
144
|
+
extensionRevisionId: ExtensionRevisionIdSchema.optional(),
|
|
145
|
+
agentId: AgentIdSchema.optional(),
|
|
146
|
+
agentRevisionId: AgentRevisionIdSchema.optional(),
|
|
147
|
+
status: z.enum(['pending', 'running', 'succeeded', 'failed', 'uncertain']),
|
|
148
|
+
result: CommandResultSchema.optional(),
|
|
149
|
+
error: z.string().optional(),
|
|
150
|
+
/** Replies in send order. A platform command's replies go to its conversation; see `replyLocal`. */
|
|
151
|
+
replies: z
|
|
152
|
+
.array(CommandReplySchema)
|
|
153
|
+
.max(COMMAND_MAX_PROGRESS_REPLIES + 1)
|
|
154
|
+
.default([]),
|
|
155
|
+
/** No reply is sent on purpose (unauthorized or unknown platform command, or console-local result). */
|
|
156
|
+
replySuppressed: z.boolean().optional(),
|
|
157
|
+
/** The result was kept in the console on purpose and never sent to the platform. */
|
|
158
|
+
replyLocal: z.boolean().optional(),
|
|
159
|
+
createdAt: z.number().int().nonnegative(),
|
|
160
|
+
completedAt: z.number().int().nonnegative().optional(),
|
|
161
|
+
})
|
|
162
|
+
.strict()
|
|
163
|
+
export type CommandExecutionRecord = z.output<typeof CommandExecutionRecordSchema>
|
|
164
|
+
/** Replies that still need an Outbound or a recorded failure. */
|
|
165
|
+
export const undeliveredCommandReplies = (record: CommandExecutionRecord): readonly CommandReply[] =>
|
|
166
|
+
record.replySuppressed === true
|
|
167
|
+
? []
|
|
168
|
+
: record.replies.filter((reply) => reply.intentId === undefined && reply.error === undefined)
|
|
169
|
+
export interface CommandExecutionContext {
|
|
170
|
+
readonly channelId: ChannelId
|
|
171
|
+
readonly agentId?: AgentId
|
|
172
|
+
readonly identity: CommandExecutionIdentity
|
|
173
|
+
readonly arguments: Readonly<Record<string, string | number | boolean>>
|
|
174
|
+
readonly signal: AbortSignal
|
|
175
|
+
/**
|
|
176
|
+
* Sends a progress reply now; the handler's return value is the final result. At most
|
|
177
|
+
* COMMAND_MAX_PROGRESS_REPLIES per execution, delivered in order without making the handler wait for receipts.
|
|
178
|
+
*/
|
|
179
|
+
readonly reply: (result: CommandResult) => void
|
|
180
|
+
readonly host: {
|
|
181
|
+
status(): {
|
|
182
|
+
readonly bound: boolean
|
|
183
|
+
readonly responseMode?: string
|
|
184
|
+
readonly paused?: boolean
|
|
185
|
+
readonly running: boolean
|
|
186
|
+
}
|
|
187
|
+
/** Resolves with `stopped: false` when nothing was running. */
|
|
188
|
+
stop(): Promise<{ readonly stopped: boolean }>
|
|
189
|
+
reset(mode: 'clear' | 'compact'): Promise<void>
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
export type CommandHandler = (context: CommandExecutionContext) => CommandResult | Promise<CommandResult>
|
|
193
|
+
export const CommandCatalogEntrySchema = z
|
|
194
|
+
.object({
|
|
195
|
+
definition: CommandDefinitionSchema,
|
|
196
|
+
fullName: z.string(),
|
|
197
|
+
source: z.string(),
|
|
198
|
+
extensionRevisionId: ExtensionRevisionIdSchema.optional(),
|
|
199
|
+
enabled: z.boolean(),
|
|
200
|
+
permission: CommandPermissionSchema,
|
|
201
|
+
usage: z.string(),
|
|
202
|
+
available: z.boolean(),
|
|
203
|
+
unavailableReason: z.string().optional(),
|
|
204
|
+
globalPolicy: CommandPolicySchema.optional(),
|
|
205
|
+
channelPolicy: CommandPolicySchema.optional(),
|
|
206
|
+
})
|
|
207
|
+
.strict()
|
|
208
|
+
export type CommandCatalogEntry = z.output<typeof CommandCatalogEntrySchema>
|
|
209
|
+
|
|
210
|
+
/** Shell-like quoting without evaluation or interpolation. */
|
|
211
|
+
export function tokenizeCommand(text: string): string[] {
|
|
212
|
+
const tokens: string[] = []
|
|
213
|
+
let value = '',
|
|
214
|
+
quote = '',
|
|
215
|
+
escaped = false,
|
|
216
|
+
started = false
|
|
217
|
+
for (const char of text) {
|
|
218
|
+
if (escaped) {
|
|
219
|
+
value += char
|
|
220
|
+
escaped = false
|
|
221
|
+
started = true
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
if (char === '\\') {
|
|
225
|
+
escaped = true
|
|
226
|
+
started = true
|
|
227
|
+
continue
|
|
228
|
+
}
|
|
229
|
+
if (quote) {
|
|
230
|
+
if (char === quote) quote = ''
|
|
231
|
+
else value += char
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
if (char === '"' || char === "'") {
|
|
235
|
+
quote = char
|
|
236
|
+
started = true
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
if (/\s/u.test(char)) {
|
|
240
|
+
if (started) tokens.push(value)
|
|
241
|
+
value = ''
|
|
242
|
+
started = false
|
|
243
|
+
continue
|
|
244
|
+
}
|
|
245
|
+
value += char
|
|
246
|
+
started = true
|
|
247
|
+
}
|
|
248
|
+
if (escaped || quote) throw new Error('引号或转义未闭合。')
|
|
249
|
+
if (started) tokens.push(value)
|
|
250
|
+
return tokens
|
|
251
|
+
}
|
|
252
|
+
function validateValue(p: z.output<typeof CommandParameterSchema>, raw: string): string | number | boolean {
|
|
253
|
+
let value: string | number | boolean = raw
|
|
254
|
+
if (p.type === 'boolean') {
|
|
255
|
+
if (!['true', 'false'].includes(raw)) throw new Error(`参数 ${p.name} 必须为 true 或 false。`)
|
|
256
|
+
value = raw === 'true'
|
|
257
|
+
} else if (p.type !== 'string') {
|
|
258
|
+
value = Number(raw)
|
|
259
|
+
if (raw.trim() === '' || !Number.isFinite(value) || (p.type === 'integer' && !Number.isSafeInteger(value)))
|
|
260
|
+
throw new Error(`参数 ${p.name} 必须为${p.type === 'integer' ? '整数' : '数字'}。`)
|
|
261
|
+
}
|
|
262
|
+
const size = typeof value === 'string' ? [...value].length : typeof value === 'number' ? value : undefined
|
|
263
|
+
if (size !== undefined && ((p.min !== undefined && size < p.min) || (p.max !== undefined && size > p.max)))
|
|
264
|
+
throw new Error(`参数 ${p.name} 超出允许范围。`)
|
|
265
|
+
if (p.choices !== undefined && !p.choices.includes(value))
|
|
266
|
+
throw new Error(`参数 ${p.name} 必须为:${p.choices.join('、')}。`)
|
|
267
|
+
return value
|
|
268
|
+
}
|
|
269
|
+
export function parseCommandArguments(
|
|
270
|
+
definition: CommandDefinition,
|
|
271
|
+
tokens: readonly string[],
|
|
272
|
+
): Readonly<Record<string, string | number | boolean>> {
|
|
273
|
+
const args = new Map<string, string | number | boolean>()
|
|
274
|
+
const positions = definition.parameters.filter((p) => p.kind === 'positional')
|
|
275
|
+
let index = 0,
|
|
276
|
+
options = true
|
|
277
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
278
|
+
const token = tokens[i]!
|
|
279
|
+
if (options && token === '--') {
|
|
280
|
+
options = false
|
|
281
|
+
continue
|
|
282
|
+
}
|
|
283
|
+
let p: CommandDefinition['parameters'][number] | undefined,
|
|
284
|
+
raw = token
|
|
285
|
+
if (options && token.startsWith('--')) {
|
|
286
|
+
const equal = token.indexOf('=')
|
|
287
|
+
const key = token.slice(2, equal < 0 ? undefined : equal)
|
|
288
|
+
p = definition.parameters.find((p) => p.kind === 'option' && p.name === key)
|
|
289
|
+
if (!p) throw new Error(`未知选项:--${key}`)
|
|
290
|
+
if (equal >= 0) raw = token.slice(equal + 1)
|
|
291
|
+
else if (p.type === 'boolean' && (tokens[i + 1] === undefined || !['true', 'false'].includes(tokens[i + 1]!)))
|
|
292
|
+
raw = 'true'
|
|
293
|
+
else {
|
|
294
|
+
const next = tokens[++i]
|
|
295
|
+
if (next === undefined || next.startsWith('--')) throw new Error(`选项 --${key} 缺少值。`)
|
|
296
|
+
raw = next
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
p = positions[index++]
|
|
300
|
+
if (!p) throw new Error(`多余参数:${token}`)
|
|
301
|
+
}
|
|
302
|
+
if (args.has(p.name)) throw new Error(`重复参数:${p.name}`)
|
|
303
|
+
args.set(p.name, validateValue(p, raw))
|
|
304
|
+
}
|
|
305
|
+
for (const p of definition.parameters) {
|
|
306
|
+
if (args.has(p.name)) continue
|
|
307
|
+
if (p.default !== undefined) args.set(p.name, p.default)
|
|
308
|
+
else if (p.required) throw new Error(`缺少参数:${p.kind === 'option' ? '--' : ''}${p.name}`)
|
|
309
|
+
}
|
|
310
|
+
return Object.fromEntries(args)
|
|
311
|
+
}
|
|
312
|
+
export function commandUsage(definition: CommandDefinition, name = definition.name): string {
|
|
313
|
+
return (
|
|
314
|
+
'/' +
|
|
315
|
+
name +
|
|
316
|
+
definition.parameters
|
|
317
|
+
.map((p) => {
|
|
318
|
+
const value =
|
|
319
|
+
p.kind === 'option' ? `--${p.name} ${p.type === 'boolean' ? '[true|false]' : `<${p.name}>`}` : `<${p.name}>`
|
|
320
|
+
return ' ' + (p.required ? value : `[${value}]`)
|
|
321
|
+
})
|
|
322
|
+
.join('')
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
export type CommandRecognition =
|
|
326
|
+
| { readonly kind: 'command'; readonly text: string }
|
|
327
|
+
| { readonly kind: 'escaped'; readonly parts: readonly MessagePart[] }
|
|
328
|
+
| { readonly kind: 'chat' }
|
|
329
|
+
export interface CommandRecognitionOptions {
|
|
330
|
+
/** External groups: only text addressed to this account is a command. */
|
|
331
|
+
readonly requireMention?: boolean
|
|
332
|
+
/**
|
|
333
|
+
* The platform itself says this message mentions the account (QQ's @ events, WeCom group callbacks), even when the
|
|
334
|
+
* mention is not a leading mention part. Such adapters may also leave a leading `@name` as plain text.
|
|
335
|
+
*/
|
|
336
|
+
readonly addressed?: boolean
|
|
337
|
+
}
|
|
338
|
+
export function recognizeCommand(
|
|
339
|
+
parts: readonly MessagePart[],
|
|
340
|
+
botMemberId?: ChannelMemberId,
|
|
341
|
+
options: CommandRecognitionOptions = {},
|
|
342
|
+
): CommandRecognition {
|
|
343
|
+
let input = parts
|
|
344
|
+
while (input.length > 1 && input[0]?.type === 'text' && input[0].text.trim() === '') input = input.slice(1)
|
|
345
|
+
let addressed = options.addressed === true
|
|
346
|
+
let mentionPart = false
|
|
347
|
+
if (input[0]?.type === 'mention') {
|
|
348
|
+
// A leading mention of someone else addresses them, never this account.
|
|
349
|
+
if (botMemberId === undefined || input[0].memberId !== botMemberId) return { kind: 'chat' }
|
|
350
|
+
input = input.slice(1)
|
|
351
|
+
addressed = true
|
|
352
|
+
mentionPart = true
|
|
353
|
+
}
|
|
354
|
+
if (options.requireMention === true && !addressed) return { kind: 'chat' }
|
|
355
|
+
if (input.length === 0 || input.some((p) => p.type !== 'text')) return { kind: 'chat' }
|
|
356
|
+
let text = input
|
|
357
|
+
.map((p) => (p.type === 'text' ? p.text : ''))
|
|
358
|
+
.join('')
|
|
359
|
+
.trimStart()
|
|
360
|
+
if (options.addressed === true && !mentionPart) text = text.replace(/^@\S+\s+/u, '')
|
|
361
|
+
if (text.startsWith('//')) return { kind: 'escaped', parts: [{ type: 'text', text: text.slice(1) }] }
|
|
362
|
+
if (!text.startsWith('/')) return { kind: 'chat' }
|
|
363
|
+
return { kind: 'command', text }
|
|
364
|
+
}
|
|
365
|
+
/** The command name a recognized text invokes (`/name args` → `name`). */
|
|
366
|
+
export const commandNameOf = (text: string): string => /^\/(\S*)/u.exec(text.trimStart())?.[1] ?? ''
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { AgentIdSchema, ChannelIdSchema, LogicalMessageIdSchema } from './domain.js'
|
|
3
|
+
import {
|
|
4
|
+
CommunityFeedbackCreateSchema,
|
|
5
|
+
CommunityFeedbackDiagnosticsSchema,
|
|
6
|
+
CommunityFeedbackExcerptSchema,
|
|
7
|
+
CommunityFeedbackKindSchema,
|
|
8
|
+
CommunityFeedbackRefSchema,
|
|
9
|
+
CommunityFeedbackStatusSchema,
|
|
10
|
+
CommunityFeedbackTargetSchema,
|
|
11
|
+
} from './community-feedback.js'
|
|
12
|
+
|
|
13
|
+
export const CommunityFeedbackDraftIdSchema = z
|
|
14
|
+
.string()
|
|
15
|
+
.regex(/^fbd_[0-9A-Za-z]+$/u)
|
|
16
|
+
.brand<'CommunityFeedbackDraftId'>()
|
|
17
|
+
export type CommunityFeedbackDraftId = z.output<typeof CommunityFeedbackDraftIdSchema>
|
|
18
|
+
export const CommunityFeedbackDraftStateSchema = z.enum(['awaiting', 'sending', 'sent', 'failed', 'discarded'])
|
|
19
|
+
export const CommunityFeedbackDraftSchema = z
|
|
20
|
+
.object({
|
|
21
|
+
id: CommunityFeedbackDraftIdSchema,
|
|
22
|
+
communityUrl: z.string().url(),
|
|
23
|
+
communityUserId: z.string().min(1).nullable(),
|
|
24
|
+
agentId: AgentIdSchema.nullable(),
|
|
25
|
+
channelId: ChannelIdSchema.nullable(),
|
|
26
|
+
kind: CommunityFeedbackKindSchema,
|
|
27
|
+
target: CommunityFeedbackTargetSchema,
|
|
28
|
+
title: z.string().max(120),
|
|
29
|
+
body: z.string().max(8000),
|
|
30
|
+
excerpt: CommunityFeedbackExcerptSchema.nullable(),
|
|
31
|
+
excerptSharedWithAuthor: z.boolean(),
|
|
32
|
+
diagnostics: CommunityFeedbackDiagnosticsSchema,
|
|
33
|
+
state: CommunityFeedbackDraftStateSchema,
|
|
34
|
+
requestedBy: z.string(),
|
|
35
|
+
auto: z.boolean(),
|
|
36
|
+
error: z.string().nullable(),
|
|
37
|
+
createdAt: z.number().int().nonnegative(),
|
|
38
|
+
updatedAt: z.number().int().nonnegative(),
|
|
39
|
+
})
|
|
40
|
+
.strict()
|
|
41
|
+
export type CommunityFeedbackDraft = z.output<typeof CommunityFeedbackDraftSchema>
|
|
42
|
+
|
|
43
|
+
export const CommunityFeedbackItemSchema = z
|
|
44
|
+
.object({
|
|
45
|
+
ref: CommunityFeedbackRefSchema,
|
|
46
|
+
communityUrl: z.string().url(),
|
|
47
|
+
communityUserId: z.string().min(1),
|
|
48
|
+
channelId: ChannelIdSchema.nullable(),
|
|
49
|
+
draftId: CommunityFeedbackDraftIdSchema.nullable(),
|
|
50
|
+
title: z.string(),
|
|
51
|
+
kind: CommunityFeedbackKindSchema.nullable(),
|
|
52
|
+
target: CommunityFeedbackTargetSchema.nullable(),
|
|
53
|
+
status: CommunityFeedbackStatusSchema,
|
|
54
|
+
fixedIn: z.string().nullable(),
|
|
55
|
+
unread: z.boolean(),
|
|
56
|
+
lastEventSeq: z.number().int().nonnegative(),
|
|
57
|
+
createdAt: z.number().int().nonnegative(),
|
|
58
|
+
updatedAt: z.number().int().nonnegative(),
|
|
59
|
+
})
|
|
60
|
+
.strict()
|
|
61
|
+
export type CommunityFeedbackItem = z.output<typeof CommunityFeedbackItemSchema>
|
|
62
|
+
|
|
63
|
+
export const CommunityFeedbackDraftInputSchema = z
|
|
64
|
+
.object({
|
|
65
|
+
kind: CommunityFeedbackKindSchema,
|
|
66
|
+
target: CommunityFeedbackTargetSchema,
|
|
67
|
+
title: z.string().trim().min(1).max(120),
|
|
68
|
+
body: z.string().trim().min(1).max(8000),
|
|
69
|
+
conversationRefs: z.array(LogicalMessageIdSchema).max(8).optional(),
|
|
70
|
+
})
|
|
71
|
+
.strict()
|
|
72
|
+
export type CommunityFeedbackDraftInput = z.output<typeof CommunityFeedbackDraftInputSchema>
|
|
73
|
+
export const CommunityFeedbackDraftUpdateSchema = z
|
|
74
|
+
.object({
|
|
75
|
+
title: z.string().trim().min(1).max(120),
|
|
76
|
+
body: z.string().trim().min(1).max(8000),
|
|
77
|
+
includeExcerpt: z.boolean(),
|
|
78
|
+
excerptSharedWithAuthor: z.boolean().default(false),
|
|
79
|
+
expectedUpdatedAt: z.number().int().nonnegative(),
|
|
80
|
+
})
|
|
81
|
+
.strict()
|
|
82
|
+
export const CommunityFeedbackSettingsSchema = z
|
|
83
|
+
.object({
|
|
84
|
+
enabled: z.boolean(),
|
|
85
|
+
supported: z.boolean(),
|
|
86
|
+
signedIn: z.boolean(),
|
|
87
|
+
communityUrl: z.string().url(),
|
|
88
|
+
communityUserId: z.string().nullable(),
|
|
89
|
+
consentedAt: z.number().int().nonnegative().nullable(),
|
|
90
|
+
})
|
|
91
|
+
.strict()
|
|
92
|
+
export type CommunityFeedbackSettings = z.output<typeof CommunityFeedbackSettingsSchema>
|
|
93
|
+
export const CommunityFeedbackSettingsUpdateSchema = z
|
|
94
|
+
.object({ enabled: z.boolean(), consent: z.boolean().default(false) })
|
|
95
|
+
.strict()
|
|
96
|
+
export const CommunityFeedbackSnapshotSchema = z
|
|
97
|
+
.object({
|
|
98
|
+
enabled: z.boolean(),
|
|
99
|
+
signedIn: z.boolean(),
|
|
100
|
+
pendingDrafts: z.number().int().nonnegative(),
|
|
101
|
+
unread: z.number().int().nonnegative(),
|
|
102
|
+
})
|
|
103
|
+
.strict()
|
|
104
|
+
export const CommunityFeedbackLocalListSchema = z
|
|
105
|
+
.object({
|
|
106
|
+
drafts: z.array(CommunityFeedbackDraftSchema),
|
|
107
|
+
items: z.array(CommunityFeedbackItemSchema),
|
|
108
|
+
syncError: z.string().optional(),
|
|
109
|
+
})
|
|
110
|
+
.strict()
|
|
111
|
+
export const CommunityFeedbackPayloadSchema = z
|
|
112
|
+
.object({ payload: CommunityFeedbackCreateSchema, redactions: z.array(z.string()) })
|
|
113
|
+
.strict()
|
|
114
|
+
export const CommunityFeedbackLastPayloadSchema = z
|
|
115
|
+
.object({
|
|
116
|
+
ref: CommunityFeedbackRefSchema,
|
|
117
|
+
sentAt: z.number().int().nonnegative(),
|
|
118
|
+
payload: CommunityFeedbackCreateSchema,
|
|
119
|
+
})
|
|
120
|
+
.strict()
|
|
121
|
+
.nullable()
|