Unify protocol types and modes

This commit is contained in:
weishu
2026-01-03 14:43:59 +08:00
parent bfca058967
commit e2028b6914
20 changed files with 360 additions and 393 deletions
+11 -99
View File
@@ -7,61 +7,17 @@
* - No E2E encryption; data is stored as JSON in SQLite
*/
import { AgentStateSchema, MetadataSchema } from '@hapi/protocol/schemas'
import type { ClaudePermissionMode, DecryptedMessage, ModelMode, PermissionMode, Session, SyncEvent } from '@hapi/protocol/types'
import { z } from 'zod'
import type { Server } from 'socket.io'
import type { Store } from '../store'
import type { RpcRegistry } from '../socket/rpcRegistry'
import type { SSEManager } from '../sse/sseManager'
import { extractTodoWriteTodosFromMessageContent, TodosSchema, type TodoItem } from './todos'
import { extractTodoWriteTodosFromMessageContent, TodosSchema } from './todos'
export type ConnectionStatus = 'disconnected' | 'connected'
export const MetadataSchema = z.object({
path: z.string(),
host: z.string(),
version: z.string().optional(),
name: z.string().optional(),
os: z.string().optional(),
summary: z.object({
text: z.string(),
updatedAt: z.number()
}).optional(),
machineId: z.string().optional(),
tools: z.array(z.string()).optional(),
flavor: z.string().nullish(),
worktree: z.object({
basePath: z.string(),
branch: z.string(),
name: z.string(),
worktreePath: z.string().optional(),
createdAt: z.number().optional()
}).optional()
}).passthrough()
export type Metadata = z.infer<typeof MetadataSchema>
export const AgentStateSchema = z.object({
controlledByUser: z.boolean().nullish(),
requests: z.record(z.string(), z.object({
tool: z.string(),
arguments: z.unknown(),
createdAt: z.number().nullish()
}).passthrough()).nullish(),
completedRequests: z.record(z.string(), z.object({
tool: z.string(),
arguments: z.unknown(),
createdAt: z.number().nullish(),
completedAt: z.number().nullish(),
status: z.enum(['canceled', 'denied', 'approved']),
reason: z.string().optional(),
mode: z.string().optional(),
decision: z.enum(['approved', 'approved_for_session', 'denied', 'abort']).optional(),
allowTools: z.array(z.string()).optional(),
answers: z.record(z.string(), z.array(z.string())).optional()
}).passthrough()).nullish()
}).passthrough()
export type AgentState = z.infer<typeof AgentStateSchema>
export type { Session, SyncEvent } from '@hapi/protocol/types'
const machineMetadataSchema = z.object({
host: z.string().optional(),
@@ -70,25 +26,6 @@ const machineMetadataSchema = z.object({
displayName: z.string().optional()
}).passthrough()
export interface Session {
id: string
namespace: string
seq: number
createdAt: number
updatedAt: number
active: boolean
activeAt: number
metadata: Metadata | null
metadataVersion: number
agentState: AgentState | null
agentStateVersion: number
thinking: boolean
thinkingAt: number
todos?: TodoItem[]
permissionMode?: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' | 'read-only' | 'safe-yolo' | 'yolo'
modelMode?: 'default' | 'sonnet' | 'opus'
}
export interface Machine {
id: string
namespace: string
@@ -109,14 +46,6 @@ export interface Machine {
daemonStateVersion: number
}
export interface DecryptedMessage {
id: string
seq: number
localId: string | null
content: unknown
createdAt: number
}
export type FetchMessagesResult =
| { ok: true; messages: DecryptedMessage[] }
| { ok: false; status: number | null; error: string }
@@ -139,23 +68,6 @@ export type RpcPathExistsResponse = {
exists: Record<string, boolean>
}
export type SyncEventType =
| 'session-added'
| 'session-updated'
| 'session-removed'
| 'message-received'
| 'machine-updated'
| 'connection-changed'
export interface SyncEvent {
type: SyncEventType
namespace?: string
sessionId?: string
machineId?: string
data?: unknown
message?: DecryptedMessage
}
export type SyncEventListener = (event: SyncEvent) => void
function clampAliveTime(t: number): number | null {
@@ -383,8 +295,8 @@ export class SyncEngine {
time: number
thinking?: boolean
mode?: 'local' | 'remote'
permissionMode?: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' | 'read-only' | 'safe-yolo' | 'yolo'
modelMode?: 'default' | 'sonnet' | 'opus'
permissionMode?: PermissionMode
modelMode?: ModelMode
}): void {
const t = clampAliveTime(payload.time)
if (!t) return
@@ -697,7 +609,7 @@ export class SyncEngine {
async approvePermission(
sessionId: string,
requestId: string,
mode?: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan',
mode?: ClaudePermissionMode,
allowTools?: string[],
decision?: 'approved' | 'approved_for_session' | 'denied' | 'abort',
answers?: Record<string, string[]>
@@ -739,7 +651,7 @@ export class SyncEngine {
async setPermissionMode(
sessionId: string,
mode: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' | 'read-only' | 'safe-yolo' | 'yolo'
mode: PermissionMode
): Promise<void> {
const session = this.sessions.get(sessionId)
if (session) {
@@ -748,7 +660,7 @@ export class SyncEngine {
}
}
async setModelMode(sessionId: string, model: 'default' | 'sonnet' | 'opus'): Promise<void> {
async setModelMode(sessionId: string, model: ModelMode): Promise<void> {
const session = this.sessions.get(sessionId)
if (session) {
session.modelMode = model
@@ -810,8 +722,8 @@ export class SyncEngine {
async applySessionConfig(
sessionId: string,
config: {
permissionMode?: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' | 'read-only' | 'safe-yolo' | 'yolo'
modelMode?: 'default' | 'sonnet' | 'opus'
permissionMode?: PermissionMode
modelMode?: ModelMode
}
): Promise<void> {
const result = await this.sessionRpc(sessionId, 'set-session-config', config)
+4 -11
View File
@@ -1,15 +1,8 @@
import { z } from 'zod'
import { TodoItemSchema, TodosSchema } from '@hapi/protocol/schemas'
import type { TodoItem } from '@hapi/protocol/types'
export const TodoItemSchema = z.object({
content: z.string(),
status: z.enum(['pending', 'in_progress', 'completed']),
priority: z.enum(['high', 'medium', 'low']),
id: z.string()
}).passthrough()
export type TodoItem = z.infer<typeof TodoItemSchema>
export const TodosSchema = z.array(TodoItemSchema)
export { TodoItemSchema, TodosSchema }
export type { TodoItem }
function isObject(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === 'object'
+8 -7
View File
@@ -1,5 +1,8 @@
import { isModelModeAllowedForFlavor, isPermissionModeAllowedForFlavor } from '@hapi/protocol'
import { ModelModeSchema, PermissionModeSchema } from '@hapi/protocol/schemas'
import { Hono } from 'hono'
import { z } from 'zod'
import type { ModelMode } from '@hapi/protocol/types'
import type { SyncEngine, Session } from '../../sync/syncEngine'
import type { WebAppEnv } from '../middleware/auth'
import { requireSessionFromParam, requireSyncEngine } from './guards'
@@ -27,7 +30,7 @@ type SessionSummary = {
metadata: SessionSummaryMetadata | null
todoProgress: { completed: number; total: number } | null
pendingRequestsCount: number
modelMode?: 'default' | 'sonnet' | 'opus'
modelMode?: ModelMode
}
function toSessionSummary(session: Session): SessionSummary {
@@ -60,11 +63,11 @@ function toSessionSummary(session: Session): SessionSummary {
}
const permissionModeSchema = z.object({
mode: z.enum(['default', 'acceptEdits', 'bypassPermissions', 'plan', 'read-only', 'safe-yolo', 'yolo'])
mode: PermissionModeSchema
})
const modelModeSchema = z.object({
model: z.enum(['default', 'sonnet', 'opus'])
model: ModelModeSchema
})
const renameSessionSchema = z.object({
@@ -181,14 +184,12 @@ export function createSessionsRoutes(getSyncEngine: () => SyncEngine | null): Ho
const flavor = sessionResult.session.metadata?.flavor ?? 'claude'
const mode = parsed.data.mode
const claudeModes = new Set(['default', 'acceptEdits', 'bypassPermissions', 'plan'])
const codexModes = new Set(['default', 'read-only', 'safe-yolo', 'yolo'])
if (flavor === 'gemini') {
return c.json({ error: 'Permission mode not supported for Gemini sessions' }, 400)
}
if (flavor === 'codex' ? !codexModes.has(mode) : !claudeModes.has(mode)) {
if (!isPermissionModeAllowedForFlavor(mode, flavor)) {
return c.json({ error: 'Invalid permission mode for session flavor' }, 400)
}
@@ -219,7 +220,7 @@ export function createSessionsRoutes(getSyncEngine: () => SyncEngine | null): Ho
}
const flavor = sessionResult.session.metadata?.flavor ?? 'claude'
if (flavor !== 'claude') {
if (!isModelModeAllowedForFlavor(parsed.data.model, flavor)) {
return c.json({ error: 'Model mode is only supported for Claude sessions' }, 400)
}