feat(im): resume project session history (#1286)

Add shared project and session selection across IM adapters, preserve bindings on
failed restoration, and synchronize permissions with desktop clients.

Fixes #1286
This commit is contained in:
程序员阿江(Relakkes)
2026-09-08 13:56:16 +08:00
parent 82447fcf23
commit 59c7857beb
24 changed files with 2023 additions and 124 deletions
+30 -2
View File
@@ -16,6 +16,8 @@ import {
parsePermissionCommand,
} from '../common/permission.js'
import { SessionStore } from '../common/session-store.js'
import { syncImPermissionState } from '../common/permission-sync.js'
import { SessionSelectionController } from '../common/session-selection.js'
import { createAdapterClient } from '../common/adapter-client.js'
import { restoreStoredSessionBinding } from '../common/session-recovery.js'
import { isAllowedUser, tryPair } from '../common/pairing.js'
@@ -66,6 +68,21 @@ attachmentStore.gc().catch((err) => {
console.warn('[WeChat] AttachmentStore.gc failed:', err instanceof Error ? err.message : err)
})
const sessionSelectionController = new SessionSelectionController({
httpClient,
bridge,
sessionStore,
sendNotice: async (chatId, text) => { await sendText(chatId, text) },
onServerMessage: handleServerMessage,
clearTransientState: clearTransientChatState,
clearProjectSelection: (chatId) => { pendingProjectSelection.delete(chatId) },
isBusy: (chatId) => {
const runtime = getRuntimeState(chatId)
return runtime.state !== 'idle' || runtime.pendingPermissionCount > 0
|| (pendingPermissions.get(chatId)?.size ?? 0) > 0
},
})
type ChatRuntimeState = {
state: 'idle' | 'thinking' | 'streaming' | 'tool_executing' | 'permission_pending'
verb?: string
@@ -277,6 +294,7 @@ async function ensureSession(chatId: string): Promise<boolean> {
}
async function createSessionForChat(chatId: string, workDir: string): Promise<boolean> {
sessionSelectionController.clear(chatId)
try {
bridge.resetSession(chatId)
clearTransientChatState(chatId)
@@ -297,6 +315,7 @@ async function createSessionForChat(chatId: string, workDir: string): Promise<bo
}
async function showProjectPicker(chatId: string): Promise<void> {
sessionSelectionController.clear(chatId)
try {
const projects = await httpClient.listRecentProjects()
if (projects.length === 0) {
@@ -315,6 +334,7 @@ async function showProjectPicker(chatId: string): Promise<void> {
}
async function startNewSession(chatId: string, query?: string): Promise<void> {
sessionSelectionController.clear(chatId)
bridge.resetSession(chatId)
sessionStore.delete(chatId)
clearTransientChatState(chatId)
@@ -351,10 +371,12 @@ async function startNewSession(chatId: string, query?: string): Promise<void> {
async function handleServerMessage(chatId: string, msg: ServerMessage): Promise<void> {
const runtime = getRuntimeState(chatId)
if (syncImPermissionState(chatId, msg, runtime, pendingPermissions)) return
switch (msg.type) {
case 'connected':
break
case 'status':
runtime.state = msg.state
runtime.verb = typeof msg.verb === 'string' ? msg.verb : undefined
@@ -491,6 +513,7 @@ async function routeUserMessage(message: WechatMessage): Promise<void> {
}
clearTransientChatState(chatId)
const sent = bridge.sendUserMessage(chatId, '/clear')
if (sent) getRuntimeState(chatId).state = 'thinking'
await sendText(chatId, sent ? '已清空当前会话上下文。' : '无法发送 /clear,请先发送 /new 重新连接会话。')
return
}
@@ -511,6 +534,8 @@ async function routeUserMessage(message: WechatMessage): Promise<void> {
await sendText(chatId, sent ? `${formatPermissionDecisionStatus(permissionDecision)}。` : '权限响应发送失败,请检查会话状态。')
return
}
if (!hasAttachments && await sessionSelectionController.handleInput(chatId, text)) return
if (!hasAttachments && pendingProjectSelection.has(chatId)) {
await startNewSession(chatId, text)
return
@@ -523,6 +548,7 @@ async function routeUserMessage(message: WechatMessage): Promise<void> {
if (!effectiveText && attachments.length === 0) return
typingController.start(chatId)
const sent = bridge.sendUserMessage(chatId, effectiveText, attachments.length ? attachments : undefined)
if (sent) getRuntimeState(chatId).state = 'thinking'
if (!sent) await sendText(chatId, '消息发送失败,连接可能已断开。请发送 /new 重新开始。')
})
}
@@ -612,9 +638,9 @@ function redactChatId(chatId: string): string {
console.log('[WeChat] Starting adapter...')
console.log(`[WeChat] Account: ${accountId}`)
void pollLoop()
if (import.meta.main || process.argv.includes('--wechat')) void pollLoop()
process.on('SIGINT', () => {
if (import.meta.main || process.argv.includes('--wechat')) process.on('SIGINT', () => {
console.log('[WeChat] Shutting down...')
stopped = true
typingController.destroy()
@@ -622,3 +648,5 @@ process.on('SIGINT', () => {
dedup.destroy()
process.exit(0)
})
export { bridge, dedup, sessionStore, sessionSelectionController, handleServerMessage, getRuntimeState, clearTransientChatState, createSessionForChat, showProjectPicker, routeUserMessage, startNewSession, typingController }