mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
WeChat and DingTalk were using different pairing, attachment, and response state paths, which made the new IM channels behave differently from Feishu and Telegram. Align the shared pairing model, wire inbound media into the existing attachment bridge, and map platform response capabilities to their real APIs: WeChat block streaming plus typing, DingTalk AI Card streaming. Constraint: WeChat iLink exposes typing and block streaming, but no editable message/card streaming API Constraint: DingTalk streaming depends on the AI Card create/deliver/stream/finalize lifecycle Rejected: Fake DingTalk typing with standalone markdown | it would add chat noise instead of platform state Rejected: Auto-pair WeChat after QR login | it bypasses the shared IM pairing model Confidence: high Scope-risk: moderate Directive: Keep WeChat streaming as block-send unless iLink adds editable messages; keep DingTalk streaming on AI Card APIs Tested: bun run check:adapters; bun run check:server; cd desktop && bun run test src/stores/adapterStore.test.ts; cd desktop && bun run build; bunx tsc -p adapters/tsconfig.json --noEmit; git diff --check Not-tested: Live WeChat and DingTalk platform smoke with real production credentials
158 lines
5.1 KiB
TypeScript
158 lines
5.1 KiB
TypeScript
import { create } from 'zustand'
|
||
import { adaptersApi } from '../api/adapters'
|
||
import type { AdapterFileConfig } from '../types/adapter'
|
||
import type { DingtalkRegistrationBegin, DingtalkRegistrationPoll } from '../api/adapters'
|
||
|
||
/**
|
||
* Tauri command 触发器:让主进程 kill + respawn adapter sidecar,
|
||
* 让 ~/.claude/adapters.json 里的最新凭据被新进程读到,建立飞书 / Telegram / 微信 / 钉钉
|
||
* 的 WebSocket 连接。
|
||
*
|
||
* 在非 Tauri 环境(纯浏览器调试 / 单元测试)这会安静失败 —— 那种场景下
|
||
* 本来也没有 sidecar 可重启。
|
||
*/
|
||
async function notifyTauriRestartAdapters(): Promise<void> {
|
||
try {
|
||
// 用 dynamic import 避开 SSR / non-tauri 测试环境的硬依赖
|
||
const { invoke } = await import('@tauri-apps/api/core')
|
||
await invoke('restart_adapters_sidecar')
|
||
} catch (err) {
|
||
// 不阻塞保存流程 —— 配置文件已经写入,下次启动 App 也会生效
|
||
if (typeof console !== 'undefined') {
|
||
console.warn('[adapterStore] restart_adapters_sidecar failed:', err)
|
||
}
|
||
}
|
||
}
|
||
|
||
const SAFE_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789'
|
||
const CODE_LENGTH = 6
|
||
const CODE_TTL_MS = 60 * 60 * 1000 // 60 minutes
|
||
|
||
function generateCode(): string {
|
||
const maxValid = Math.floor(256 / SAFE_ALPHABET.length) * SAFE_ALPHABET.length
|
||
let code = ''
|
||
while (code.length < CODE_LENGTH) {
|
||
const array = new Uint8Array(1)
|
||
crypto.getRandomValues(array)
|
||
if (array[0]! < maxValid) {
|
||
code += SAFE_ALPHABET[array[0]! % SAFE_ALPHABET.length]
|
||
}
|
||
}
|
||
return code
|
||
}
|
||
|
||
type AdapterStore = {
|
||
config: AdapterFileConfig
|
||
isLoading: boolean
|
||
error: string | null
|
||
|
||
fetchConfig: () => Promise<void>
|
||
updateConfig: (patch: Partial<AdapterFileConfig>) => Promise<void>
|
||
generatePairingCode: () => Promise<string>
|
||
startWechatLogin: () => Promise<{ qrcodeUrl?: string; message: string; sessionKey: string }>
|
||
pollWechatLogin: (sessionKey: string) => Promise<{ connected: boolean; message?: string }>
|
||
removePairedUser: (platform: 'telegram' | 'feishu' | 'wechat' | 'dingtalk', userId: string | number) => Promise<void>
|
||
beginDingtalkRegistration: () => Promise<DingtalkRegistrationBegin>
|
||
pollDingtalkRegistration: (deviceCode: string) => Promise<DingtalkRegistrationPoll>
|
||
unbindWechatAccount: () => Promise<void>
|
||
unbindDingtalkBot: () => Promise<void>
|
||
}
|
||
|
||
export const useAdapterStore = create<AdapterStore>((set, get) => ({
|
||
config: {},
|
||
isLoading: false,
|
||
error: null,
|
||
|
||
fetchConfig: async () => {
|
||
set({ isLoading: true, error: null })
|
||
try {
|
||
const config = await adaptersApi.getConfig()
|
||
set({ config, isLoading: false })
|
||
} catch (err) {
|
||
const message = err instanceof Error ? err.message : 'Failed to load config'
|
||
set({ isLoading: false, error: message })
|
||
}
|
||
},
|
||
|
||
updateConfig: async (patch) => {
|
||
const config = await adaptersApi.updateConfig(patch)
|
||
set({ config })
|
||
// 配置文件已写入磁盘,让 Tauri 主进程 kill + respawn adapter sidecar,
|
||
// 触发飞书 / Telegram WebSocket 用新凭据重连。pairing code / paired users
|
||
// 这种轻量更新也会触发重启 —— 这是个有意为之的简化:保证"任何配置变更
|
||
// 都立刻生效",比起精细判断哪些字段值得重启更可靠。
|
||
void notifyTauriRestartAdapters()
|
||
},
|
||
|
||
generatePairingCode: async () => {
|
||
const code = generateCode()
|
||
const now = Date.now()
|
||
await get().updateConfig({
|
||
pairing: {
|
||
code,
|
||
expiresAt: now + CODE_TTL_MS,
|
||
createdAt: now,
|
||
},
|
||
})
|
||
return code
|
||
},
|
||
|
||
startWechatLogin: async () => {
|
||
return adaptersApi.startWechatLogin()
|
||
},
|
||
|
||
pollWechatLogin: async (sessionKey) => {
|
||
const result = await adaptersApi.pollWechatLogin(sessionKey)
|
||
if ('connected' in result && result.connected === false) {
|
||
return { connected: false, message: result.message }
|
||
}
|
||
if ('wechat' in result || 'telegram' in result || 'feishu' in result || 'dingtalk' in result) {
|
||
set({ config: result })
|
||
void notifyTauriRestartAdapters()
|
||
return { connected: true }
|
||
}
|
||
return { connected: false }
|
||
},
|
||
|
||
beginDingtalkRegistration: () => adaptersApi.beginDingtalkRegistration(),
|
||
|
||
pollDingtalkRegistration: async (deviceCode) => {
|
||
const result = await adaptersApi.pollDingtalkRegistration(deviceCode)
|
||
if (result.config) {
|
||
set({ config: result.config })
|
||
void notifyTauriRestartAdapters()
|
||
}
|
||
return result
|
||
},
|
||
|
||
unbindWechatAccount: async () => {
|
||
const config = await adaptersApi.unbindWechat()
|
||
set({ config })
|
||
void notifyTauriRestartAdapters()
|
||
},
|
||
|
||
unbindDingtalkBot: async () => {
|
||
await get().updateConfig({
|
||
dingtalk: {
|
||
clientId: undefined,
|
||
clientSecret: undefined,
|
||
pairedUsers: [],
|
||
},
|
||
})
|
||
},
|
||
|
||
removePairedUser: async (platform, userId) => {
|
||
const { config } = get()
|
||
const platformConfig = config[platform]
|
||
if (!platformConfig) return
|
||
|
||
const pairedUsers = (platformConfig.pairedUsers ?? []).filter(
|
||
(u) => String(u.userId) !== String(userId),
|
||
)
|
||
|
||
await get().updateConfig({
|
||
[platform]: { ...platformConfig, pairedUsers },
|
||
})
|
||
},
|
||
}))
|