feat: add remote channels and richer notes

This commit is contained in:
lofyer
2026-08-09 15:48:52 +08:00
parent 417a9fccb6
commit 6c891f3522
69 changed files with 13012 additions and 499 deletions
+119 -12
View File
@@ -1,8 +1,13 @@
import { z } from 'zod'
import {
weixinBindingStatusSchema,
weixinVerificationInputSchema
} from '../../shared/weixin-channel-contracts'
export const WECHAT_SIDECAR_MAX_TEXT_LENGTH = 8_000
export const WECHAT_SIDECAR_MAX_QR_PAYLOAD_LENGTH = 4_096
export const WECHAT_SIDECAR_MAX_QR_TTL_MS = 5 * 60 * 1_000
export const WECHAT_SIDECAR_PROTOCOL_VERSION = 1
function containsControlCharacter(value: string): boolean {
for (const character of value) {
@@ -37,15 +42,7 @@ const textSchema = z
.min(1)
.max(WECHAT_SIDECAR_MAX_TEXT_LENGTH)
export const wechatSidecarStatusSchema = z.enum([
'stopped',
'starting',
'pending',
'scanned',
'connected',
'expired',
'failed'
])
export const wechatSidecarStatusSchema = weixinBindingStatusSchema
export type WechatSidecarStatus = z.infer<
typeof wechatSidecarStatusSchema
@@ -82,7 +79,42 @@ export const wechatSidecarInboundTextMessageSchema = z
})
.strict()
export const wechatSidecarReplyMessageSchema = z
export const wechatSidecarVerificationRequiredMessageSchema = z
.object({
type: z.literal('verification_required'),
prompt: z.string().trim().min(1).max(256)
})
.strict()
export const wechatSidecarConnectedMessageSchema = z
.object({
type: z.literal('connected'),
accountId: identifierSchema,
userId: identifierSchema
})
.strict()
export const wechatSidecarReplyResultMessageSchema = z
.object({
type: z.literal('reply_result'),
replyId: identifierSchema,
ok: z.boolean(),
error: z.string().trim().min(1).max(512).optional()
})
.strict()
.superRefine((result, context) => {
if (result.ok === (result.error !== undefined)) {
context.addIssue({
code: 'custom',
path: ['error'],
message: result.ok
? '成功回复不能包含错误'
: '失败回复必须包含错误'
})
}
})
export const wechatSidecarReplyCommandSchema = z
.object({
type: z.literal('reply'),
replyId: identifierSchema,
@@ -96,7 +128,9 @@ export const wechatSidecarMessageSchema = z.discriminatedUnion('type', [
wechatSidecarStatusMessageSchema,
wechatSidecarQrMessageSchema,
wechatSidecarInboundTextMessageSchema,
wechatSidecarReplyMessageSchema
wechatSidecarVerificationRequiredMessageSchema,
wechatSidecarConnectedMessageSchema,
wechatSidecarReplyResultMessageSchema
])
export type WechatSidecarMessage = z.infer<
@@ -106,6 +140,69 @@ export type WechatSidecarQrMessage = z.infer<
typeof wechatSidecarQrMessageSchema
>
export const wechatSidecarStartLoginCommandSchema = z
.object({
type: z.literal('start_login')
})
.strict()
export const wechatSidecarSubmitVerificationCommandSchema = z
.object({
type: z.literal('submit_verification'),
code: weixinVerificationInputSchema.shape.code
})
.strict()
export const wechatSidecarDisconnectCommandSchema = z
.object({
type: z.literal('disconnect')
})
.strict()
export const wechatSidecarShutdownCommandSchema = z
.object({
type: z.literal('shutdown')
})
.strict()
export const wechatSidecarCommandSchema = z.discriminatedUnion('type', [
wechatSidecarStartLoginCommandSchema,
wechatSidecarSubmitVerificationCommandSchema,
wechatSidecarReplyCommandSchema,
wechatSidecarDisconnectCommandSchema,
wechatSidecarShutdownCommandSchema
])
export type WechatSidecarCommand = z.infer<
typeof wechatSidecarCommandSchema
>
export const wechatSidecarStartAccountCommandSchema = z
.object({
type: z.literal('start_account'),
accountId: identifierSchema,
userId: identifierSchema,
baseUrl: z.string().url().max(2_048),
token: z.string().trim().min(1).max(4_096)
})
.strict()
export const wechatSidecarCredentialMessageSchema = z
.object({
type: z.literal('credential'),
accountId: identifierSchema,
userId: identifierSchema,
baseUrl: z.string().url().max(2_048),
token: z.string().trim().min(1).max(4_096)
})
.strict()
export type WechatSidecarStartAccountCommand = z.infer<
typeof wechatSidecarStartAccountCommandSchema
>
export type WechatSidecarCredentialMessage = z.infer<
typeof wechatSidecarCredentialMessageSchema
>
const allowedTransitions: Readonly<
Record<WechatSidecarStatus, ReadonlySet<WechatSidecarStatus>>
> = {
@@ -120,11 +217,19 @@ const allowedTransitions: Readonly<
]),
scanned: new Set([
'scanned',
'verification_required',
'connected',
'expired',
'failed',
'stopped'
]),
verification_required: new Set([
'verification_required',
'scanned',
'expired',
'failed',
'stopped'
]),
connected: new Set(['connected', 'failed', 'stopped']),
expired: new Set(['expired', 'starting', 'stopped']),
failed: new Set(['failed', 'starting', 'stopped'])
@@ -201,7 +306,9 @@ export class WechatQrStateMachine {
expire(now = Date.now()): boolean {
this.assertTimestamp(now)
if (
(this.status === 'pending' || this.status === 'scanned') &&
(this.status === 'pending' ||
this.status === 'scanned' ||
this.status === 'verification_required') &&
this.qr &&
Date.parse(this.qr.expiresAt) <= now
) {