@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.
Files changed (50) hide show
  1. package/README.md +2 -0
  2. package/dist/index.mjs +3092 -164
  3. package/dist/index.mjs.map +1 -1
  4. package/lib/types/commands.d.ts +408 -0
  5. package/lib/types/commands.d.ts.map +1 -0
  6. package/lib/types/community-feedback-host.d.ts +442 -0
  7. package/lib/types/community-feedback-host.d.ts.map +1 -0
  8. package/lib/types/community-feedback.d.ts +1072 -0
  9. package/lib/types/community-feedback.d.ts.map +1 -0
  10. package/lib/types/community-personas.d.ts +4 -0
  11. package/lib/types/community-personas.d.ts.map +1 -1
  12. package/lib/types/community.d.ts +260 -13
  13. package/lib/types/community.d.ts.map +1 -1
  14. package/lib/types/config-schema.d.ts +16 -2
  15. package/lib/types/config-schema.d.ts.map +1 -1
  16. package/lib/types/credentials.d.ts +328 -0
  17. package/lib/types/credentials.d.ts.map +1 -0
  18. package/lib/types/domain.d.ts +44 -2
  19. package/lib/types/domain.d.ts.map +1 -1
  20. package/lib/types/extension-capabilities.d.ts +58 -3
  21. package/lib/types/extension-capabilities.d.ts.map +1 -1
  22. package/lib/types/extension-ui.d.ts +46 -4
  23. package/lib/types/extension-ui.d.ts.map +1 -1
  24. package/lib/types/host-api.d.ts +10805 -2887
  25. package/lib/types/host-api.d.ts.map +1 -1
  26. package/lib/types/index.d.ts +7 -0
  27. package/lib/types/index.d.ts.map +1 -1
  28. package/lib/types/linked-accounts.d.ts +100 -0
  29. package/lib/types/linked-accounts.d.ts.map +1 -0
  30. package/lib/types/logs.d.ts +355 -0
  31. package/lib/types/logs.d.ts.map +1 -0
  32. package/lib/types/response-timing.d.ts +60 -0
  33. package/lib/types/response-timing.d.ts.map +1 -0
  34. package/lib/types/runtime-compatibility.d.ts +2 -2
  35. package/package.json +1 -1
  36. package/src/commands.ts +366 -0
  37. package/src/community-feedback-host.ts +121 -0
  38. package/src/community-feedback.ts +289 -0
  39. package/src/community.ts +63 -1
  40. package/src/config-schema.ts +33 -2
  41. package/src/credentials.ts +260 -0
  42. package/src/domain.ts +74 -8
  43. package/src/extension-capabilities.ts +78 -4
  44. package/src/extension-ui.ts +3 -0
  45. package/src/host-api.ts +1873 -163
  46. package/src/index.ts +8 -0
  47. package/src/linked-accounts.ts +77 -0
  48. package/src/logs.ts +138 -0
  49. package/src/management-api.ts +1 -1
  50. package/src/response-timing.ts +77 -0
@@ -0,0 +1,60 @@
1
+ import { z } from 'zod';
2
+ /** Bounds of the numeric response timing settings; the host rejects values outside them. */
3
+ export declare const RESPONSE_TIMING_LIMITS: {
4
+ readonly waitMaxQuietSeconds: {
5
+ readonly min: 5;
6
+ readonly max: 300;
7
+ };
8
+ readonly waitMaxTotalSeconds: {
9
+ readonly min: 10;
10
+ readonly max: 900;
11
+ };
12
+ readonly debounceSeconds: {
13
+ readonly min: 0;
14
+ readonly max: 30;
15
+ };
16
+ readonly debounceMaxSeconds: {
17
+ readonly min: 1;
18
+ readonly max: 120;
19
+ };
20
+ readonly debounceMaxMessages: {
21
+ readonly min: 1;
22
+ readonly max: 20;
23
+ };
24
+ readonly attentionSeconds: {
25
+ readonly min: 5;
26
+ readonly max: 600;
27
+ };
28
+ readonly midWakesPerMinute: {
29
+ readonly min: 1;
30
+ readonly max: 30;
31
+ };
32
+ };
33
+ /**
34
+ * Per-binding timing of when the agent speaks (Decision 2026-10-31 响应时机与智能响应). Every field is optional: an
35
+ * omitted one uses {@link RESPONSE_TIMING_DEFAULTS}. Waiting applies to every policy that answers messages; the rest
36
+ * only to `smart`.
37
+ */
38
+ export declare const ResponseTimingSettingsSchema: z.ZodObject<{
39
+ waitEnabled: z.ZodOptional<z.ZodBoolean>;
40
+ waitMaxQuietSeconds: z.ZodOptional<z.ZodNumber>;
41
+ waitMaxTotalSeconds: z.ZodOptional<z.ZodNumber>;
42
+ debounceSeconds: z.ZodOptional<z.ZodNumber>;
43
+ debounceMaxSeconds: z.ZodOptional<z.ZodNumber>;
44
+ debounceMaxMessages: z.ZodOptional<z.ZodNumber>;
45
+ nameWake: z.ZodOptional<z.ZodBoolean>;
46
+ attentionWake: z.ZodOptional<z.ZodBoolean>;
47
+ attentionSeconds: z.ZodOptional<z.ZodNumber>;
48
+ midWakesPerMinute: z.ZodOptional<z.ZodNumber>;
49
+ }, z.core.$strict>;
50
+ export type ResponseTimingSettings = z.infer<typeof ResponseTimingSettingsSchema>;
51
+ export type ResolvedResponseTiming = {
52
+ readonly [K in keyof ResponseTimingSettings]-?: NonNullable<ResponseTimingSettings[K]>;
53
+ };
54
+ export declare const RESPONSE_TIMING_DEFAULTS: ResolvedResponseTiming;
55
+ /** Shortest quiet period a wait may ask for, whatever the channel allows. */
56
+ export declare const RESPONSE_WAIT_MIN_QUIET_SECONDS = 5;
57
+ export declare const resolveResponseTiming: (settings: ResponseTimingSettings | undefined) => ResolvedResponseTiming;
58
+ /** Extra names that wake the agent in smart mode; the display name always counts too. */
59
+ export declare const AgentAliasesSchema: z.ZodPipe<z.ZodArray<z.ZodString>, z.ZodTransform<string[], string[]>>;
60
+ //# sourceMappingURL=response-timing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"response-timing.d.ts","sourceRoot":"","sources":["../../src/response-timing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,4FAA4F;AAC5F,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAQzB,CAAA;AAKV;;;;GAIG;AACH,eAAO,MAAM,4BAA4B;;;;;;;;;;;kBAsB9B,CAAA;AAEX,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAA;AAEjF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,EAAE,CAAC,IAAI,MAAM,sBAAsB,CAAC,CAAC,GAAG,WAAW,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC;CACvF,CAAA;AAED,eAAO,MAAM,wBAAwB,EAAE,sBAWtC,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,+BAA+B,IAAI,CAAA;AAEhD,eAAO,MAAM,qBAAqB,GAAI,UAAU,sBAAsB,GAAG,SAAS,KAAG,sBAGnF,CAAA;AAEF,yFAAyF;AACzF,eAAO,MAAM,kBAAkB,wEAGiB,CAAA"}
@@ -18,10 +18,10 @@ export declare const RuntimeCompatibilityDiagnosticSchema: z.ZodObject<{
18
18
  isolated: "isolated";
19
19
  }>;
20
20
  phase: z.ZodEnum<{
21
+ dependency: "dependency";
21
22
  dispose: "dispose";
22
23
  restore: "restore";
23
24
  configuration: "configuration";
24
- dependency: "dependency";
25
25
  build: "build";
26
26
  load: "load";
27
27
  render: "render";
@@ -55,10 +55,10 @@ export declare const HostUpgradeSummarySchema: z.ZodObject<{
55
55
  isolated: "isolated";
56
56
  }>;
57
57
  phase: z.ZodEnum<{
58
+ dependency: "dependency";
58
59
  dispose: "dispose";
59
60
  restore: "restore";
60
61
  configuration: "configuration";
61
- dependency: "dependency";
62
62
  build: "build";
63
63
  load: "load";
64
64
  render: "render";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nekro-nxt/contracts",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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()