feat: add remote channels and richer notes
This commit is contained in:
@@ -4,9 +4,23 @@ import { agentRuntimeSelectionSchema } from './runtime-selection-contracts'
|
||||
export const assistantIdSchema = z.string().uuid()
|
||||
export const workModeSchema = z.enum(['ask', 'plan', 'execute'])
|
||||
export const interactiveWorkModes = ['ask', 'execute'] as const
|
||||
export const projectKindSchema = z.enum(['user', 'channel'])
|
||||
export const projectChannels = [
|
||||
'weixin',
|
||||
'wecom',
|
||||
'dingtalk'
|
||||
] as const
|
||||
export const projectChannelSchema = z.enum(projectChannels)
|
||||
export const projectChannelLabels: Record<ProjectChannel, string> = {
|
||||
weixin: '微信 ClawBot',
|
||||
wecom: '企业微信',
|
||||
dingtalk: '钉钉'
|
||||
}
|
||||
|
||||
export type WorkMode = z.infer<typeof workModeSchema>
|
||||
export type InteractiveWorkMode = (typeof interactiveWorkModes)[number]
|
||||
export type ProjectKind = z.infer<typeof projectKindSchema>
|
||||
export type ProjectChannel = z.infer<typeof projectChannelSchema>
|
||||
|
||||
export function normalizeInteractiveWorkMode(
|
||||
workMode: WorkMode | undefined
|
||||
@@ -121,6 +135,14 @@ export const conversationSnapshotSchema = z
|
||||
id: assistantIdSchema,
|
||||
projectId: assistantIdSchema.optional(),
|
||||
runtimeSelection: agentRuntimeSelectionSchema.optional(),
|
||||
remote: z
|
||||
.object({
|
||||
channel: projectChannelSchema,
|
||||
accountDisplay: z.string().trim().min(1).max(200),
|
||||
conversationType: z.enum(['direct', 'group'])
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
title: z.string().trim().min(1).max(200),
|
||||
updatedAt: z.number().int().nonnegative(),
|
||||
messages: z
|
||||
@@ -184,6 +206,8 @@ export const conversationSnapshotsSchema = z
|
||||
|
||||
export type AssistantProject = ProjectCreateInput & {
|
||||
id: string
|
||||
kind: ProjectKind
|
||||
channel?: ProjectChannel
|
||||
status: 'active' | 'archived'
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
|
||||
@@ -11,6 +11,9 @@ describe('channel settings contracts', () => {
|
||||
it('accepts bounded strict WeCom and DingTalk updates', () => {
|
||||
expect(
|
||||
channelSettingsApplySchema.parse({
|
||||
weixin: {
|
||||
enabled: false
|
||||
},
|
||||
wecom: {
|
||||
enabled: true,
|
||||
botId: ' bot-id ',
|
||||
@@ -27,6 +30,9 @@ describe('channel settings contracts', () => {
|
||||
}
|
||||
})
|
||||
).toEqual({
|
||||
weixin: {
|
||||
enabled: false
|
||||
},
|
||||
wecom: {
|
||||
enabled: true,
|
||||
botId: 'bot-id',
|
||||
@@ -69,6 +75,13 @@ describe('channel settings contracts', () => {
|
||||
|
||||
it('models public credential source and runtime status without secrets', () => {
|
||||
const snapshot = channelSettingsSnapshotSchema.parse({
|
||||
weixin: {
|
||||
enabled: true,
|
||||
bindingConfigured: true,
|
||||
source: 'encrypted',
|
||||
accountDisplay: '微信用户 ****1234',
|
||||
status: { state: 'running' }
|
||||
},
|
||||
wecom: {
|
||||
enabled: true,
|
||||
botId: 'bot-id',
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
projectChannelSchema,
|
||||
type ProjectChannel
|
||||
} from './assistant-contracts'
|
||||
|
||||
export const CHANNEL_SETTINGS_LIMITS = {
|
||||
maximumIdentifierLength: 256,
|
||||
@@ -8,8 +12,12 @@ export const CHANNEL_SETTINGS_LIMITS = {
|
||||
maximumWarningLength: 500
|
||||
} as const
|
||||
|
||||
export const managedChannelSchema = z.enum(['wecom', 'dingtalk'])
|
||||
export type ManagedChannel = z.infer<typeof managedChannelSchema>
|
||||
export const managedChannelSchema = projectChannelSchema
|
||||
export type ManagedChannel = ProjectChannel
|
||||
export const credentialChannelSchema = z.enum(['wecom', 'dingtalk'])
|
||||
export type CredentialChannel = z.infer<
|
||||
typeof credentialChannelSchema
|
||||
>
|
||||
|
||||
const identifierSchema = z
|
||||
.string()
|
||||
@@ -68,14 +76,27 @@ export type DingTalkChannelSettingsInput = z.infer<
|
||||
typeof dingTalkChannelSettingsInputSchema
|
||||
>
|
||||
|
||||
export const weixinChannelSettingsInputSchema = z
|
||||
.object({
|
||||
enabled: z.boolean()
|
||||
})
|
||||
.strict()
|
||||
export type WeixinChannelSettingsInput = z.infer<
|
||||
typeof weixinChannelSettingsInputSchema
|
||||
>
|
||||
|
||||
export const channelSettingsApplySchema = z
|
||||
.object({
|
||||
weixin: weixinChannelSettingsInputSchema.optional(),
|
||||
wecom: weComChannelSettingsInputSchema.optional(),
|
||||
dingtalk: dingTalkChannelSettingsInputSchema.optional()
|
||||
})
|
||||
.strict()
|
||||
.refine(
|
||||
(input) => input.wecom !== undefined || input.dingtalk !== undefined,
|
||||
(input) =>
|
||||
input.weixin !== undefined ||
|
||||
input.wecom !== undefined ||
|
||||
input.dingtalk !== undefined,
|
||||
'至少需要提供一个通道设置'
|
||||
)
|
||||
export type ChannelSettingsApply = z.infer<
|
||||
@@ -147,8 +168,22 @@ export type DingTalkChannelSettings = z.infer<
|
||||
typeof dingTalkChannelSettingsSchema
|
||||
>
|
||||
|
||||
export const weixinChannelSettingsSchema = z
|
||||
.object({
|
||||
enabled: z.boolean(),
|
||||
bindingConfigured: z.boolean(),
|
||||
source: z.enum(['none', 'encrypted']),
|
||||
accountDisplay: z.string().trim().min(1).max(64).optional(),
|
||||
status: channelRuntimeStatusSchema
|
||||
})
|
||||
.strict()
|
||||
export type WeixinChannelSettings = z.infer<
|
||||
typeof weixinChannelSettingsSchema
|
||||
>
|
||||
|
||||
export const channelSettingsSnapshotSchema = z
|
||||
.object({
|
||||
weixin: weixinChannelSettingsSchema,
|
||||
wecom: weComChannelSettingsSchema,
|
||||
dingtalk: dingTalkChannelSettingsSchema,
|
||||
warning: z
|
||||
|
||||
+63
-2
@@ -36,12 +36,24 @@ import {
|
||||
type ExpertCreateInput,
|
||||
type ExpertUpdateInput
|
||||
} from './assistant-contracts'
|
||||
import type {
|
||||
MagicNoteDetail,
|
||||
MagicNoteCreateInput,
|
||||
MagicNoteEntryCreateInput,
|
||||
MagicNoteEntryUpdateInput,
|
||||
MagicNotesSnapshot,
|
||||
MagicNoteUpdateInput,
|
||||
MagicTodoCreateInput,
|
||||
MagicTodoItem,
|
||||
MagicTodosSnapshot,
|
||||
MagicTodoUpdateInput
|
||||
} from './magic-notes-contracts'
|
||||
import type {
|
||||
ChannelConnectionTestResult,
|
||||
ChannelSettingsApply,
|
||||
ChannelSettingsSnapshot,
|
||||
CredentialChannel,
|
||||
DingTalkChannelSettingsInput,
|
||||
ManagedChannel,
|
||||
WeComChannelSettingsInput
|
||||
} from './channel-settings-contracts'
|
||||
import type {
|
||||
@@ -58,6 +70,12 @@ import type {
|
||||
EmbeddingIndexStatus,
|
||||
EmbeddingSettingsSnapshot
|
||||
} from './embedding-contracts'
|
||||
import type { WeixinBindingSnapshot } from './weixin-channel-contracts'
|
||||
import type {
|
||||
RemoteChannelActivity,
|
||||
RemoteChannelApproval,
|
||||
RemoteChannelApprovalDecision
|
||||
} from './remote-channel-contracts'
|
||||
import {
|
||||
agentRuntimeSelectionSchema,
|
||||
type AgentRuntimeSelection
|
||||
@@ -948,9 +966,31 @@ export type DesktopApi = {
|
||||
getSnapshot: () => Promise<ChannelSettingsSnapshot>
|
||||
apply: (input: ChannelSettingsApply) => Promise<ChannelSettingsSnapshot>
|
||||
testConnection: (
|
||||
channel: ManagedChannel,
|
||||
channel: CredentialChannel,
|
||||
settings?: WeComChannelSettingsInput | DingTalkChannelSettingsInput
|
||||
) => Promise<ChannelConnectionTestResult>
|
||||
getWeixinBinding: () => Promise<WeixinBindingSnapshot>
|
||||
startWeixinBinding: () => Promise<WeixinBindingSnapshot>
|
||||
submitWeixinVerification: (
|
||||
code: string
|
||||
) => Promise<WeixinBindingSnapshot>
|
||||
disconnectWeixin: () => Promise<WeixinBindingSnapshot>
|
||||
onWeixinBindingChanged: (
|
||||
listener: (snapshot: WeixinBindingSnapshot) => void
|
||||
) => () => void
|
||||
respondRemoteApproval: (
|
||||
approvalId: string,
|
||||
decision: RemoteChannelApprovalDecision
|
||||
) => Promise<boolean>
|
||||
getPendingRemoteApprovals: () => Promise<
|
||||
RemoteChannelApproval[]
|
||||
>
|
||||
onRemoteApproval: (
|
||||
listener: (approval: RemoteChannelApproval) => void
|
||||
) => () => void
|
||||
onRemoteActivity: (
|
||||
listener: (activity: RemoteChannelActivity) => void
|
||||
) => () => void
|
||||
}
|
||||
updates?: {
|
||||
getSettings: () => Promise<ApplicationSettings>
|
||||
@@ -1003,6 +1043,7 @@ export type DesktopApi = {
|
||||
conversations: {
|
||||
list: () => Promise<ConversationSnapshot[]>
|
||||
replace: (conversations: ConversationSnapshot[]) => Promise<void>
|
||||
onChanged: (listener: () => void) => () => void
|
||||
}
|
||||
workspace: {
|
||||
getChanges: (projectId: string) => Promise<WorkspaceChanges>
|
||||
@@ -1129,6 +1170,26 @@ export type DesktopApi = {
|
||||
readClipboard: () => Promise<ContextAttachment>
|
||||
remove: (contextId: string) => Promise<void>
|
||||
}
|
||||
magicNotes: {
|
||||
list: (projectId?: string) => Promise<MagicNotesSnapshot>
|
||||
get: (noteId: string) => Promise<MagicNoteDetail>
|
||||
create: (input: MagicNoteCreateInput) => Promise<MagicNoteDetail>
|
||||
update: (input: MagicNoteUpdateInput) => Promise<MagicNoteDetail>
|
||||
remove: (noteId: string) => Promise<void>
|
||||
createEntry: (
|
||||
input: MagicNoteEntryCreateInput
|
||||
) => Promise<MagicNoteDetail>
|
||||
updateEntry: (
|
||||
input: MagicNoteEntryUpdateInput
|
||||
) => Promise<MagicNoteDetail>
|
||||
removeEntry: (entryId: string) => Promise<MagicNoteDetail>
|
||||
analyze: (entryId: string) => Promise<MagicNoteDetail>
|
||||
listTodos: (projectId?: string) => Promise<MagicTodosSnapshot>
|
||||
createTodo: (input: MagicTodoCreateInput) => Promise<MagicTodoItem>
|
||||
updateTodo: (input: MagicTodoUpdateInput) => Promise<MagicTodoItem>
|
||||
removeTodo: (todoId: string) => Promise<void>
|
||||
analyzeTodo: (todoId: string) => Promise<MagicTodoItem>
|
||||
}
|
||||
knowledge: {
|
||||
getSnapshot: (libraryId?: string) => Promise<KnowledgeSnapshot>
|
||||
createLibrary: (
|
||||
|
||||
@@ -30,6 +30,17 @@ export const ipcChannels = {
|
||||
channelSettingsGet: 'settings:channels:get',
|
||||
channelSettingsApply: 'settings:channels:apply',
|
||||
channelSettingsTest: 'settings:channels:test',
|
||||
weixinBindingGet: 'settings:channels:weixin:binding:get',
|
||||
weixinBindingStart: 'settings:channels:weixin:binding:start',
|
||||
weixinBindingVerify: 'settings:channels:weixin:binding:verify',
|
||||
weixinBindingDisconnect:
|
||||
'settings:channels:weixin:binding:disconnect',
|
||||
weixinBindingChanged:
|
||||
'settings:channels:weixin:binding:changed',
|
||||
remoteChannelApprovalRespond: 'channels:remote-approval:respond',
|
||||
remoteChannelApprovalList: 'channels:remote-approval:list',
|
||||
remoteChannelApprovalRequested: 'channels:remote-approval:requested',
|
||||
remoteChannelActivity: 'channels:remote-activity',
|
||||
applicationSettingsGet: 'settings:application:get',
|
||||
applicationSettingsUpdate: 'settings:application:update',
|
||||
versionCheck: 'application:update:check',
|
||||
@@ -57,6 +68,7 @@ export const ipcChannels = {
|
||||
projectsDelete: 'projects:delete',
|
||||
conversationsList: 'conversations:list',
|
||||
conversationsReplace: 'conversations:replace',
|
||||
conversationsChanged: 'conversations:changed',
|
||||
workspaceChangesGet: 'workspace:changes:get',
|
||||
workspaceDirectoryList: 'workspace:directory:list',
|
||||
workspaceFileRead: 'workspace:file:read',
|
||||
@@ -108,6 +120,20 @@ export const ipcChannels = {
|
||||
contextCaptureWindow: 'context:capture-window',
|
||||
contextReadClipboard: 'context:read-clipboard',
|
||||
contextRemove: 'context:remove',
|
||||
magicNotesList: 'magic-notes:list',
|
||||
magicNotesGet: 'magic-notes:get',
|
||||
magicNotesCreate: 'magic-notes:create',
|
||||
magicNotesUpdate: 'magic-notes:update',
|
||||
magicNotesDelete: 'magic-notes:delete',
|
||||
magicNotesCreateEntry: 'magic-notes:create-entry',
|
||||
magicNotesUpdateEntry: 'magic-notes:update-entry',
|
||||
magicNotesDeleteEntry: 'magic-notes:delete-entry',
|
||||
magicNotesAnalyze: 'magic-notes:analyze',
|
||||
magicTodosList: 'magic-todos:list',
|
||||
magicTodosCreate: 'magic-todos:create',
|
||||
magicTodosUpdate: 'magic-todos:update',
|
||||
magicTodosDelete: 'magic-todos:delete',
|
||||
magicTodosAnalyze: 'magic-todos:analyze',
|
||||
knowledgeSnapshot: 'knowledge:snapshot',
|
||||
knowledgeCreateLibrary: 'knowledge:library:create',
|
||||
knowledgeUpdateLibrary: 'knowledge:library:update',
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const MAGIC_NOTE_MAX_IMAGES = 12
|
||||
export const MAGIC_NOTE_MAX_IMAGE_BYTES = 2 * 1024 * 1024
|
||||
export const MAGIC_NOTE_MAX_TOTAL_IMAGE_BYTES = 8 * 1024 * 1024
|
||||
export const MAGIC_NOTE_MAX_TEXT_BYTES = 500 * 1024
|
||||
|
||||
export function magicNoteImageDataBytes(dataUrl: string): number {
|
||||
const payload = dataUrl.slice(dataUrl.indexOf(',') + 1)
|
||||
const padding = payload.endsWith('==')
|
||||
? 2
|
||||
: payload.endsWith('=')
|
||||
? 1
|
||||
: 0
|
||||
return Math.floor((payload.length * 3) / 4) - padding
|
||||
}
|
||||
|
||||
const magicNoteIdSchema = z.string().uuid()
|
||||
const imageDataUrlSchema = z
|
||||
.string()
|
||||
.max(Math.ceil((MAGIC_NOTE_MAX_IMAGE_BYTES * 4) / 3) + 128)
|
||||
.regex(
|
||||
/^data:image\/(?:jpeg|png|gif|webp);base64,[A-Za-z0-9+/]+={0,2}$/,
|
||||
'只支持本地 JPEG、PNG、GIF 或 WebP 图片'
|
||||
)
|
||||
|
||||
const magicNoteAttributesSchema = z
|
||||
.object({
|
||||
bold: z.literal(true).optional(),
|
||||
italic: z.literal(true).optional(),
|
||||
underline: z.literal(true).optional(),
|
||||
strike: z.literal(true).optional(),
|
||||
code: z.literal(true).optional(),
|
||||
header: z.union([z.literal(1), z.literal(2), z.literal(3)]).optional(),
|
||||
blockquote: z.literal(true).optional(),
|
||||
'code-block': z.literal(true).optional(),
|
||||
list: z
|
||||
.enum(['ordered', 'bullet', 'checked', 'unchecked'])
|
||||
.optional(),
|
||||
align: z.enum(['center', 'right', 'justify']).optional(),
|
||||
indent: z.number().int().min(1).max(8).optional()
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const magicNoteRichContentSchema = z
|
||||
.object({
|
||||
version: z.literal(1),
|
||||
ops: z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
insert: z.union([
|
||||
z.string().max(200_000),
|
||||
z.object({ image: imageDataUrlSchema }).strict()
|
||||
]),
|
||||
attributes: magicNoteAttributesSchema.optional()
|
||||
})
|
||||
.strict()
|
||||
)
|
||||
.min(1)
|
||||
.max(10_000)
|
||||
})
|
||||
.strict()
|
||||
.superRefine((content, context) => {
|
||||
const encoder = new TextEncoder()
|
||||
let textBytes = 0
|
||||
const imageData: string[] = []
|
||||
for (const operation of content.ops) {
|
||||
if (typeof operation.insert === 'string') {
|
||||
textBytes += encoder.encode(operation.insert).byteLength
|
||||
} else {
|
||||
imageData.push(operation.insert.image)
|
||||
}
|
||||
}
|
||||
if (textBytes > MAGIC_NOTE_MAX_TEXT_BYTES) {
|
||||
context.addIssue({
|
||||
code: 'custom',
|
||||
message: '每条记录的文字内容不能超过 500 KB'
|
||||
})
|
||||
}
|
||||
if (imageData.length > MAGIC_NOTE_MAX_IMAGES) {
|
||||
context.addIssue({
|
||||
code: 'custom',
|
||||
message: `每条记录最多包含 ${MAGIC_NOTE_MAX_IMAGES} 张图片`
|
||||
})
|
||||
}
|
||||
const estimatedBytes = imageData.reduce(
|
||||
(total, dataUrl) => total + magicNoteImageDataBytes(dataUrl),
|
||||
0
|
||||
)
|
||||
if (estimatedBytes > MAGIC_NOTE_MAX_TOTAL_IMAGE_BYTES) {
|
||||
context.addIssue({
|
||||
code: 'custom',
|
||||
message: '一篇笔记中的图片总大小不能超过 8 MB'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export type MagicNoteRichContent = z.infer<
|
||||
typeof magicNoteRichContentSchema
|
||||
>
|
||||
|
||||
export const magicNoteScopeSchema = z
|
||||
.object({
|
||||
projectId: magicNoteIdSchema.optional()
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const magicNoteCreateSchema = z
|
||||
.object({
|
||||
projectId: magicNoteIdSchema.optional(),
|
||||
title: z.string().trim().min(1).max(100)
|
||||
})
|
||||
.strict()
|
||||
export type MagicNoteCreateInput = z.infer<typeof magicNoteCreateSchema>
|
||||
|
||||
export const magicNoteUpdateSchema = z
|
||||
.object({
|
||||
noteId: magicNoteIdSchema,
|
||||
title: z.string().trim().min(1).max(100).optional(),
|
||||
pinned: z.boolean().optional(),
|
||||
expectedRevision: z.number().int().nonnegative()
|
||||
})
|
||||
.strict()
|
||||
.refine((input) => input.title !== undefined || input.pinned !== undefined, {
|
||||
message: '没有可更新的笔记字段'
|
||||
})
|
||||
export type MagicNoteUpdateInput = z.infer<typeof magicNoteUpdateSchema>
|
||||
|
||||
export const magicNoteDeleteSchema = z
|
||||
.object({
|
||||
noteId: magicNoteIdSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const magicNoteEntryCreateSchema = z
|
||||
.object({
|
||||
noteId: magicNoteIdSchema,
|
||||
content: magicNoteRichContentSchema
|
||||
})
|
||||
.strict()
|
||||
export type MagicNoteEntryCreateInput = z.infer<
|
||||
typeof magicNoteEntryCreateSchema
|
||||
>
|
||||
|
||||
export const magicNoteEntryUpdateSchema = z
|
||||
.object({
|
||||
entryId: magicNoteIdSchema,
|
||||
content: magicNoteRichContentSchema,
|
||||
expectedRevision: z.number().int().nonnegative()
|
||||
})
|
||||
.strict()
|
||||
export type MagicNoteEntryUpdateInput = z.infer<
|
||||
typeof magicNoteEntryUpdateSchema
|
||||
>
|
||||
|
||||
export const magicNoteEntryDeleteSchema = z
|
||||
.object({
|
||||
entryId: magicNoteIdSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const magicNoteAnalyzeSchema = z
|
||||
.object({
|
||||
entryId: magicNoteIdSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const magicTodoCreateSchema = z
|
||||
.object({
|
||||
projectId: magicNoteIdSchema.optional(),
|
||||
title: z.string().trim().min(1).max(120),
|
||||
instructions: z.string().trim().max(20_000)
|
||||
})
|
||||
.strict()
|
||||
export type MagicTodoCreateInput = z.infer<typeof magicTodoCreateSchema>
|
||||
|
||||
export const magicTodoUpdateSchema = z
|
||||
.object({
|
||||
todoId: magicNoteIdSchema,
|
||||
title: z.string().trim().min(1).max(120).optional(),
|
||||
instructions: z.string().trim().max(20_000).optional(),
|
||||
completed: z.boolean().optional(),
|
||||
expectedRevision: z.number().int().nonnegative()
|
||||
})
|
||||
.strict()
|
||||
.refine(
|
||||
(input) =>
|
||||
input.title !== undefined ||
|
||||
input.instructions !== undefined ||
|
||||
input.completed !== undefined,
|
||||
{ message: '没有可更新的待办字段' }
|
||||
)
|
||||
export type MagicTodoUpdateInput = z.infer<typeof magicTodoUpdateSchema>
|
||||
|
||||
export const magicTodoIdSchema = z
|
||||
.object({
|
||||
todoId: magicNoteIdSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
export type MagicNoteCommentKind = 'summary' | 'suggestion' | 'warning'
|
||||
|
||||
export type MagicNoteComment = {
|
||||
id: string
|
||||
kind: MagicNoteCommentKind
|
||||
content: string
|
||||
}
|
||||
|
||||
export type MagicNoteEntry = {
|
||||
id: string
|
||||
noteId: string
|
||||
content: MagicNoteRichContent
|
||||
plainText: string
|
||||
comments: MagicNoteComment[]
|
||||
analyzedAt?: string
|
||||
revision: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export type MagicNoteSummary = {
|
||||
id: string
|
||||
projectId?: string
|
||||
title: string
|
||||
preview: string
|
||||
entryCount: number
|
||||
pinned: boolean
|
||||
revision: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export type MagicNoteDetail = MagicNoteSummary & {
|
||||
entries: MagicNoteEntry[]
|
||||
}
|
||||
|
||||
export type MagicNotesSnapshot = {
|
||||
notes: MagicNoteSummary[]
|
||||
}
|
||||
|
||||
export type MagicTodoItem = {
|
||||
id: string
|
||||
projectId?: string
|
||||
noteId?: string
|
||||
entryId?: string
|
||||
noteTitle?: string
|
||||
sourceIndex?: number
|
||||
source: 'note' | 'manual'
|
||||
title: string
|
||||
instructions: string
|
||||
completed: boolean
|
||||
comments: MagicNoteComment[]
|
||||
analyzedAt?: string
|
||||
revision: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export type MagicTodosSnapshot = {
|
||||
todos: MagicTodoItem[]
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { z } from 'zod'
|
||||
import { projectChannelSchema } from './assistant-contracts'
|
||||
|
||||
export const remoteChannelApprovalDecisionSchema = z.enum([
|
||||
'deny',
|
||||
'once'
|
||||
])
|
||||
export type RemoteChannelApprovalDecision = z.infer<
|
||||
typeof remoteChannelApprovalDecisionSchema
|
||||
>
|
||||
|
||||
export const remoteChannelApprovalSchema = z
|
||||
.object({
|
||||
approvalId: z.string().uuid(),
|
||||
requestId: z.string().uuid(),
|
||||
kind: z.enum(['request', 'tool']),
|
||||
channel: projectChannelSchema,
|
||||
channelLabel: z.string().trim().min(1).max(64),
|
||||
senderDisplay: z.string().trim().min(1).max(200),
|
||||
projectName: z.string().trim().min(1).max(120),
|
||||
rootPath: z.string().max(4_096),
|
||||
title: z.string().trim().min(1).max(300),
|
||||
description: z.string().trim().min(1).max(8_000),
|
||||
toolName: z.string().trim().min(1).max(200).optional(),
|
||||
argumentSummary: z.string().max(4_000).optional(),
|
||||
expiresAt: z.string().datetime({ offset: true })
|
||||
})
|
||||
.strict()
|
||||
export type RemoteChannelApproval = z.infer<
|
||||
typeof remoteChannelApprovalSchema
|
||||
>
|
||||
|
||||
export const remoteChannelApprovalResponseSchema = z
|
||||
.object({
|
||||
approvalId: z.string().uuid(),
|
||||
decision: remoteChannelApprovalDecisionSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const remoteChannelActivitySchema = z
|
||||
.object({
|
||||
requestId: z.string().uuid(),
|
||||
conversationId: z.string().uuid(),
|
||||
channel: projectChannelSchema,
|
||||
kind: z.enum(['request', 'approval', 'tool', 'result']),
|
||||
title: z.string().trim().min(1).max(240),
|
||||
detail: z.string().max(4_000),
|
||||
status: z.enum([
|
||||
'pending',
|
||||
'running',
|
||||
'completed',
|
||||
'failed',
|
||||
'denied',
|
||||
'cancelled'
|
||||
]),
|
||||
callId: z.string().min(1).max(256).optional()
|
||||
})
|
||||
.strict()
|
||||
export type RemoteChannelActivity = z.infer<
|
||||
typeof remoteChannelActivitySchema
|
||||
>
|
||||
@@ -0,0 +1,49 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const weixinBindingStatusSchema = z.enum([
|
||||
'stopped',
|
||||
'starting',
|
||||
'pending',
|
||||
'scanned',
|
||||
'verification_required',
|
||||
'connected',
|
||||
'expired',
|
||||
'failed'
|
||||
])
|
||||
export type WeixinBindingStatus = z.infer<
|
||||
typeof weixinBindingStatusSchema
|
||||
>
|
||||
|
||||
export const weixinBindingSnapshotSchema = z
|
||||
.object({
|
||||
status: weixinBindingStatusSchema,
|
||||
qrPayload: z.string().min(1).max(4_096).optional(),
|
||||
qrExpiresAt: z.string().datetime({ offset: true }).optional(),
|
||||
accountDisplay: z.string().trim().min(1).max(64).optional(),
|
||||
detail: z.string().trim().min(1).max(512).optional()
|
||||
})
|
||||
.strict()
|
||||
export type WeixinBindingSnapshot = z.infer<
|
||||
typeof weixinBindingSnapshotSchema
|
||||
>
|
||||
|
||||
export const weixinVerificationInputSchema = z
|
||||
.object({
|
||||
code: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(32)
|
||||
.regex(/^[0-9]+$/u, '验证码只能包含数字')
|
||||
})
|
||||
.strict()
|
||||
export type WeixinVerificationInput = z.infer<
|
||||
typeof weixinVerificationInputSchema
|
||||
>
|
||||
|
||||
export function weixinAccountDisplay(value: string): string | undefined {
|
||||
const normalized = value.trim()
|
||||
return normalized
|
||||
? `微信用户 ****${normalized.slice(-4)}`
|
||||
: undefined
|
||||
}
|
||||
Reference in New Issue
Block a user