@three333/termbuddy 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +957 -0
- package/dist/cli.js.map +1 -0
- package/package.json +28 -0
- package/src/app/App.tsx +64 -0
- package/src/app/index.ts +2 -0
- package/src/cli.tsx +7 -0
- package/src/components/AiConsole.tsx +77 -0
- package/src/components/AvatarDisplay.tsx +18 -0
- package/src/components/BuddyAvatar.tsx +32 -0
- package/src/components/StatusHeader.tsx +43 -0
- package/src/components/index.ts +4 -0
- package/src/constants.ts +5 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/useActivityMonitor.ts +25 -0
- package/src/hooks/useAiAgent.ts +177 -0
- package/src/hooks/useBroadcaster.ts +52 -0
- package/src/hooks/useCountdown.ts +42 -0
- package/src/hooks/useScanner.ts +60 -0
- package/src/hooks/useTcpSync.ts +153 -0
- package/src/net/broadcast.ts +32 -0
- package/src/net/index.ts +2 -0
- package/src/protocol.ts +18 -0
- package/src/types.ts +8 -0
- package/src/views/MainMenu.tsx +38 -0
- package/src/views/RoomScanner.tsx +47 -0
- package/src/views/Session.tsx +127 -0
- package/src/views/index.ts +4 -0
- package/tsconfig.json +14 -0
- package/tsup.config.ts +15 -0
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/views/MainMenu.tsx","../src/hooks/useActivityMonitor.ts","../src/hooks/useAiAgent.ts","../src/constants.ts","../src/net/broadcast.ts","../src/net/index.ts","../src/hooks/useBroadcaster.ts","../src/hooks/useCountdown.ts","../src/hooks/useScanner.ts","../src/hooks/useTcpSync.ts","../src/hooks/index.ts","../src/views/RoomScanner.tsx","../src/components/AiConsole.tsx","../src/components/AvatarDisplay.tsx","../src/components/BuddyAvatar.tsx","../src/components/StatusHeader.tsx","../src/components/index.ts","../src/views/Session.tsx","../src/views/index.ts","../src/app/App.tsx","../src/app/index.ts","../src/cli.tsx"],"sourcesContent":["import React from 'react';\nimport {Box, Text, useInput} from 'ink';\n\nexport function MainMenu(props: {onHost: () => void; onJoin: () => void; onExit: () => void}) {\n\tuseInput((input, key) => {\n\t\tif (key.escape || input === 'q') props.onExit();\n\t\tif (input === '1') props.onHost();\n\t\tif (input === '2') props.onJoin();\n\t});\n\n\treturn (\n\t\t<Box flexDirection=\"column\" padding={1}>\n\t\t\t<Text>\n\t\t\t\t{String.raw`\n████████╗███████╗██████╗ ███╗ ███╗██████╗ ██╗ ██╗██████╗ ██████╗ ██╗ ██╗\n╚══██╔══╝██╔════╝██╔══██╗████╗ ████║██╔══██╗██║ ██║██╔══██╗██╔══██╗╚██╗ ██╔╝\n ██║ █████╗ ██████╔╝██╔████╔██║██████╔╝██║ ██║██║ ██║██║ ██║ ╚████╔╝ \n ██║ ██╔══╝ ██╔══██╗██║╚██╔╝██║██╔══██╗██║ ██║██║ ██║██║ ██║ ╚██╔╝ \n ██║ ███████╗██║ ██║██║ ╚═╝ ██║██████╔╝╚██████╔╝██████╔╝██████╔╝ ██║ \n ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ \n`}\n\t\t\t</Text>\n\t\t\t<Box flexDirection=\"column\" marginTop={1}>\n\t\t\t\t<Text>Terminal Body Doubling — 极简 / 极客 / 私密</Text>\n\t\t\t\t<Text> </Text>\n\t\t\t\t<Text>\n\t\t\t\t\t<Text color=\"cyan\">[1]</Text> 建房 (Host)\n\t\t\t\t</Text>\n\t\t\t\t<Text>\n\t\t\t\t\t<Text color=\"cyan\">[2]</Text> 加入 (Join)\n\t\t\t\t</Text>\n\t\t\t\t<Text>\n\t\t\t\t\t<Text color=\"cyan\">[q]</Text> 退出\n\t\t\t\t</Text>\n\t\t\t</Box>\n\t\t</Box>\n\t);\n}\n","import {useEffect, useRef, useState} from 'react';\nimport {useInput} from 'ink';\nimport type {ActivityState} from '../protocol.js';\n\nexport function useActivityMonitor(options?: {idleAfterMs?: number}): {state: ActivityState} {\n\tconst idleAfterMs = options?.idleAfterMs ?? 1500;\n\tconst [state, setState] = useState<ActivityState>('IDLE');\n\n\tconst lastActivityRef = useRef<number>(Date.now());\n\n\tuseInput(() => {\n\t\tlastActivityRef.current = Date.now();\n\t\tsetState('TYPING');\n\t});\n\n\tuseEffect(() => {\n\t\tconst id = setInterval(() => {\n\t\t\tconst delta = Date.now() - lastActivityRef.current;\n\t\t\tif (delta >= idleAfterMs) setState('IDLE');\n\t\t}, 200);\n\t\treturn () => clearInterval(id);\n\t}, [idleAfterMs]);\n\n\treturn {state};\n}\n","import {useCallback, useEffect, useRef, useState} from 'react';\nimport {createAgent, initChatModel, tool} from 'langchain';\n\ntype LineKind = 'user' | 'ai' | 'system';\nexport type AiLine = {kind: LineKind; text: string; at: number};\n\nfunction contentToText(content: unknown): string {\n\tif (typeof content === 'string') return content;\n\tif (!content) return '';\n\tif (Array.isArray(content)) {\n\t\treturn content\n\t\t\t.map((part) => {\n\t\t\t\tif (typeof part === 'string') return part;\n\t\t\t\tif (typeof part === 'object' && part && 'text' in part) return String((part as any).text ?? '');\n\t\t\t\treturn '';\n\t\t\t})\n\t\t\t.join('');\n\t}\n\tif (typeof content === 'object' && 'text' in (content as any)) return String((content as any).text ?? '');\n\treturn String(content);\n}\n\nfunction lastAiText(messages: unknown[]): string | null {\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst m: any = messages[i];\n\t\tconst type = typeof m?.getType === 'function' ? m.getType() : typeof m?._getType === 'function' ? m._getType() : m?.type;\n\t\tif (type === 'ai') {\n\t\t\tconst t = contentToText(m?.content);\n\t\t\treturn t || '';\n\t\t}\n\t}\n\treturn null;\n}\n\nfunction createSystemPrompt(context: {localName: string; peerName: string}) {\n\treturn [\n\t\t'你是 TermBuddy 里的“壳中幽灵 (Ghost in the Shell)”。',\n\t\t'默认隐形;被 / 唤醒时出现。风格:极简、干练、少废话。',\n\t\t'你可以使用工具来操控应用功能(例如倒计时)。',\n\t\t'如果用户提到“倒计时/专注/计时/countdown”,优先调用 start_countdown。',\n\t\t`当前上下文:我叫 ${context.localName};同桌叫 ${context.peerName}。`\n\t].join('\\n');\n}\n\nexport function useAiAgent(options: {\n\tlocalName: string;\n\tpeerName: string;\n\tonStartCountdown?: (minutes: number) => void;\n}) {\n\tconst [lines, setLines] = useState<AiLine[]>([]);\n\tconst [busy, setBusy] = useState(false);\n\n\tconst agentRef = useRef<Awaited<ReturnType<typeof createAgent>> | null>(null);\n\tconst agentInitRef = useRef<Promise<Awaited<ReturnType<typeof createAgent>>> | null>(null);\n\tconst stateRef = useRef<{messages: unknown[]}>({messages: []});\n\tconst abortRef = useRef<AbortController | null>(null);\n\n\tconst append = useCallback((line: AiLine) => {\n\t\tsetLines((prev) => [...prev, line]);\n\t}, []);\n\n\tconst updateLine = useCallback((at: number, text: string) => {\n\t\tsetLines((prev) => {\n\t\t\tconst idx = prev.findIndex((l) => l.at === at);\n\t\t\tif (idx === -1) return prev;\n\t\t\tconst next = [...prev];\n\t\t\tnext[idx] = {...next[idx], text};\n\t\t\treturn next;\n\t\t});\n\t}, []);\n\n\tconst ensureAgent = useCallback(async () => {\n\t\tif (agentRef.current) return agentRef.current;\n\t\tagentInitRef.current ??= (async () => {\n\t\t\tconst startCountdown = tool(\n\t\t\t\tasync (input: {minutes: number}) => {\n\t\t\t\t\tconst minutes = Number(input.minutes);\n\t\t\t\t\tif (!Number.isFinite(minutes) || minutes <= 0) return '倒计时分钟数无效。';\n\t\t\t\t\toptions.onStartCountdown?.(minutes);\n\t\t\t\t\treturn `已开始倒计时 ${minutes} 分钟。`;\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tname: 'start_countdown',\n\t\t\t\t\tdescription: '开始一个专注倒计时(分钟)。',\n\t\t\t\t\tschema: {\n\t\t\t\t\t\ttype: 'object',\n\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\tminutes: {type: 'integer', minimum: 1, maximum: 180, description: '倒计时分钟数'}\n\t\t\t\t\t\t},\n\t\t\t\t\t\trequired: ['minutes'],\n\t\t\t\t\t\tadditionalProperties: false\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t);\n\n\t\t\tconst sessionInfo = tool(\n\t\t\t\tasync () => {\n\t\t\t\t\treturn JSON.stringify(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tlocalName: options.localName,\n\t\t\t\t\t\t\tpeerName: options.peerName\n\t\t\t\t\t\t},\n\t\t\t\t\t\tnull,\n\t\t\t\t\t\t2\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tname: 'session_info',\n\t\t\t\t\tdescription: '获取当前会话上下文(本地昵称、同桌昵称)。',\n\t\t\t\t\tschema: {type: 'object', properties: {}, additionalProperties: false}\n\t\t\t\t}\n\t\t\t);\n\n\t\t\tconst modelId = process.env.TERMBUDDY_MODEL ?? 'openai:gpt-4o-mini';\n\t\t\tconst llm = await initChatModel(modelId, {\n\t\t\t\ttemperature: 0.2,\n\t\t\t\tmaxTokens: 800,\n\t\t\t\ttimeout: 30_000\n\t\t\t});\n\n\t\t\treturn createAgent({\n\t\t\t\tllm,\n\t\t\t\ttools: [startCountdown, sessionInfo],\n\t\t\t\tprompt: createSystemPrompt({localName: options.localName, peerName: options.peerName}),\n\t\t\t\tname: 'ghost'\n\t\t\t});\n\t\t})();\n\n\t\tagentRef.current = await agentInitRef.current;\n\t\treturn agentRef.current;\n\t}, [options.localName, options.onStartCountdown, options.peerName]);\n\n\tconst ask = useCallback(\n\t\tasync (text: string) => {\n\t\t\tappend({kind: 'user', text: `> ${text}`, at: Date.now()});\n\n\t\t\tconst aiAt = Date.now() + 1;\n\t\t\tappend({kind: 'ai', text: '…', at: aiAt});\n\n\t\t\tabortRef.current?.abort();\n\t\t\tabortRef.current = new AbortController();\n\n\t\t\tsetBusy(true);\n\t\t\ttry {\n\t\t\t\tconst agent = await ensureAgent();\n\t\t\t\tconst stream = await agent.stream(\n\t\t\t\t\t{\n\t\t\t\t\t\tmessages: [...stateRef.current.messages, {role: 'user', content: text}]\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tstreamMode: 'values',\n\t\t\t\t\t\tsignal: abortRef.current.signal\n\t\t\t\t\t} as any\n\t\t\t\t);\n\n\t\t\t\tfor await (const chunk of stream as any) {\n\t\t\t\t\tconst messages = (chunk?.messages ?? []) as unknown[];\n\t\t\t\t\tif (messages.length > 0) stateRef.current.messages = messages;\n\t\t\t\t\tconst t = lastAiText(messages);\n\t\t\t\t\tif (t !== null) updateLine(aiAt, t);\n\t\t\t\t}\n\t\t\t} catch (e) {\n\t\t\t\tconst msg = e instanceof Error ? e.message : String(e);\n\t\t\t\tupdateLine(aiAt, `(AI 出错)${msg}`);\n\t\t\t} finally {\n\t\t\t\tsetBusy(false);\n\t\t\t}\n\t\t},\n\t\t[append, ensureAgent, updateLine]\n\t);\n\n\tuseEffect(() => {\n\t\treturn () => abortRef.current?.abort();\n\t}, []);\n\n\treturn {lines, ask, busy};\n}\n","export const UDP_PORT = 45888;\nexport const TCP_DEFAULT_PORT = 45999;\n\nexport const DISCOVERY_VERSION = 1;\nexport const APP_NAME = 'TermBuddy';\n","import os from 'node:os';\n\nfunction ipv4ToInt(ip: string) {\n\treturn ip\n\t\t.split('.')\n\t\t.map((n) => Number.parseInt(n, 10))\n\t\t.reduce((acc, n) => ((acc << 8) | (n & 255)) >>> 0, 0);\n}\n\nfunction intToIpv4(n: number) {\n\treturn [24, 16, 8, 0].map((shift) => String((n >>> shift) & 255)).join('.');\n}\n\nexport function getBroadcastTargets(): string[] {\n\tconst out = new Set<string>(['255.255.255.255']);\n\n\tconst ifaces = os.networkInterfaces();\n\tfor (const entries of Object.values(ifaces)) {\n\t\tif (!entries) continue;\n\t\tfor (const e of entries) {\n\t\t\tif (e.family !== 'IPv4') continue;\n\t\t\tif (e.internal) continue;\n\t\t\tif (!e.address || !e.netmask) continue;\n\t\t\tconst ip = ipv4ToInt(e.address);\n\t\t\tconst mask = ipv4ToInt(e.netmask);\n\t\t\tconst broadcast = (ip | (~mask >>> 0)) >>> 0;\n\t\t\tout.add(intToIpv4(broadcast));\n\t\t}\n\t}\n\n\treturn [...out];\n}\n","export {getBroadcastTargets} from './broadcast.js';\n\n","import {useEffect} from 'react';\nimport dgram from 'node:dgram';\nimport {UDP_PORT, DISCOVERY_VERSION} from '../constants.js';\nimport type {DiscoveryPacket} from '../protocol.js';\nimport {getBroadcastTargets} from '../net/index.js';\n\ntype Options =\n\t| {enabled: false}\n\t| {enabled: true; hostName: string; roomName: string; tcpPort?: number | null; intervalMs?: number};\n\nexport function useBroadcaster(options: Options) {\n\tconst depKey = options.enabled\n\t\t? `${options.hostName}|${options.roomName}|${options.tcpPort ?? ''}|${options.intervalMs ?? 1000}`\n\t\t: 'disabled';\n\n\tuseEffect(() => {\n\t\tif (!options.enabled) return;\n\t\tif (!options.tcpPort) return;\n\n\t\tconst socket = dgram.createSocket('udp4');\n\t\tsocket.on('error', () => {});\n\n\t\tsocket.bind(() => {\n\t\t\tsocket.setBroadcast(true);\n\t\t});\n\n\t\tconst targets = getBroadcastTargets();\n\n\t\tconst send = () => {\n\t\t\tconst packet: DiscoveryPacket = {\n\t\t\t\ttype: 'termbuddy_discovery',\n\t\t\t\tversion: DISCOVERY_VERSION,\n\t\t\t\thostName: options.hostName,\n\t\t\t\troomName: options.roomName,\n\t\t\t\ttcpPort: options.tcpPort!,\n\t\t\t\tsentAt: Date.now()\n\t\t\t};\n\t\t\tconst msg = Buffer.from(JSON.stringify(packet));\n\t\t\tfor (const address of targets) {\n\t\t\t\tsocket.send(msg, UDP_PORT, address);\n\t\t\t}\n\t\t};\n\n\t\tsend();\n\t\tconst id = setInterval(send, options.intervalMs ?? 1000);\n\n\t\treturn () => {\n\t\t\tclearInterval(id);\n\t\t\tsocket.close();\n\t\t};\n\t}, [depKey]);\n}\n","import {useCallback, useEffect, useRef, useState} from 'react';\n\nfunction formatMMSS(totalSeconds: number) {\n\tconst m = Math.floor(totalSeconds / 60);\n\tconst s = totalSeconds % 60;\n\treturn `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;\n}\n\nexport function useCountdown() {\n\tconst [remainingSeconds, setRemainingSeconds] = useState<number | null>(null);\n\tconst timerRef = useRef<NodeJS.Timeout | null>(null);\n\n\tconst start = useCallback((minutes: number) => {\n\t\tconst seconds = Math.max(1, Math.floor(minutes * 60));\n\t\tsetRemainingSeconds(seconds);\n\t\tif (timerRef.current) clearInterval(timerRef.current);\n\t\ttimerRef.current = setInterval(() => {\n\t\t\tsetRemainingSeconds((prev) => {\n\t\t\t\tif (prev === null) return null;\n\t\t\t\tif (prev <= 1) return null;\n\t\t\t\treturn prev - 1;\n\t\t\t});\n\t\t}, 1000);\n\t}, []);\n\n\tuseEffect(() => {\n\t\tif (remainingSeconds !== null) return;\n\t\tif (timerRef.current) clearInterval(timerRef.current);\n\t\ttimerRef.current = null;\n\t}, [remainingSeconds]);\n\n\tuseEffect(() => {\n\t\treturn () => {\n\t\t\tif (timerRef.current) clearInterval(timerRef.current);\n\t\t};\n\t}, []);\n\n\treturn {\n\t\tstart,\n\t\tlabel: remainingSeconds === null ? null : formatMMSS(remainingSeconds)\n\t};\n}\n","import {useEffect, useState} from 'react';\nimport dgram from 'node:dgram';\nimport {UDP_PORT, DISCOVERY_VERSION} from '../constants.js';\nimport type {DiscoveryPacket} from '../protocol.js';\nimport type {DiscoveredRoom} from '../types.js';\n\nfunction safeParse(msg: Buffer): DiscoveryPacket | null {\n\ttry {\n\t\tconst parsed = JSON.parse(msg.toString('utf8')) as DiscoveryPacket;\n\t\tif (parsed?.type !== 'termbuddy_discovery') return null;\n\t\tif (parsed?.version !== DISCOVERY_VERSION) return null;\n\t\tif (!parsed.hostName || !parsed.roomName || !parsed.tcpPort) return null;\n\t\treturn parsed;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nexport function useScanner(options?: {staleAfterMs?: number}): DiscoveredRoom[] {\n\tconst staleAfterMs = options?.staleAfterMs ?? 3500;\n\tconst [rooms, setRooms] = useState<DiscoveredRoom[]>([]);\n\n\tuseEffect(() => {\n\t\tconst socket = dgram.createSocket('udp4');\n\t\tsocket.on('error', () => {});\n\n\t\tsocket.on('message', (msg, rinfo) => {\n\t\t\tconst packet = safeParse(msg);\n\t\t\tif (!packet) return;\n\n\t\t\tconst now = Date.now();\n\t\t\tsetRooms((prev) => {\n\t\t\t\tconst key = `${rinfo.address}:${packet.tcpPort}`;\n\t\t\t\tconst next = prev.filter((r) => `${r.ip}:${r.tcpPort}` !== key);\n\t\t\t\tnext.push({\n\t\t\t\t\tip: rinfo.address,\n\t\t\t\t\thostName: packet.hostName,\n\t\t\t\t\troomName: packet.roomName,\n\t\t\t\t\ttcpPort: packet.tcpPort,\n\t\t\t\t\tlastSeenAt: now\n\t\t\t\t});\n\t\t\t\treturn next;\n\t\t\t});\n\t\t});\n\n\t\tsocket.bind(UDP_PORT, () => {});\n\n\t\tconst prune = setInterval(() => {\n\t\t\tconst now = Date.now();\n\t\t\tsetRooms((prev) => prev.filter((r) => now - r.lastSeenAt <= staleAfterMs));\n\t\t}, 500);\n\n\t\treturn () => {\n\t\t\tclearInterval(prune);\n\t\t\tsocket.close();\n\t\t};\n\t}, [staleAfterMs]);\n\n\treturn rooms;\n}\n","import {useCallback, useEffect, useRef, useState} from 'react';\nimport net from 'node:net';\nimport type {ActivityState, ConnectionStatus, TcpPacket} from '../protocol.js';\nimport {TCP_DEFAULT_PORT} from '../constants.js';\n\ntype HostOptions = {role: 'host'; localName: string; port?: number};\ntype ClientOptions = {role: 'client'; localName: string; hostIp: string; tcpPort: number; hostName?: string};\ntype Options = HostOptions | ClientOptions;\n\nfunction writePacket(socket: net.Socket, packet: TcpPacket) {\n\tsocket.write(`${JSON.stringify(packet)}\\n`, 'utf8');\n}\n\nexport function useTcpSync(options: Options): {\n\tstatus: ConnectionStatus;\n\tlistenPort?: number;\n\tpeerName?: string;\n\tremoteState?: ActivityState;\n\tsendStatus: (state: ActivityState) => void;\n} {\n\tconst [status, setStatus] = useState<ConnectionStatus>(options.role === 'host' ? 'waiting' : 'connecting');\n\tconst [listenPort, setListenPort] = useState<number | undefined>(undefined);\n\tconst [peerName, setPeerName] = useState<string | undefined>(undefined);\n\tconst [remoteState, setRemoteState] = useState<ActivityState | undefined>(undefined);\n\n\tconst socketRef = useRef<net.Socket | null>(null);\n\tconst lastSeenRef = useRef<number>(Date.now());\n\tconst heartbeatRef = useRef<NodeJS.Timeout | null>(null);\n\n\tconst cleanupSocket = useCallback(() => {\n\t\tif (heartbeatRef.current) clearInterval(heartbeatRef.current);\n\t\theartbeatRef.current = null;\n\n\t\tconst s = socketRef.current;\n\t\tsocketRef.current = null;\n\t\tif (s && !s.destroyed) s.destroy();\n\t}, []);\n\n\tconst attachSocket = useCallback(\n\t\t(s: net.Socket) => {\n\t\t\tcleanupSocket();\n\t\t\tsocketRef.current = s;\n\t\t\tlastSeenRef.current = Date.now();\n\n\t\t\tsetStatus('connected');\n\t\t\tsetRemoteState('IDLE');\n\n\t\t\tlet buf = '';\n\t\t\ts.setNoDelay(true);\n\t\t\ts.setEncoding('utf8');\n\n\t\t\tconst onData = (chunk: string) => {\n\t\t\t\tbuf += chunk;\n\t\t\t\twhile (true) {\n\t\t\t\t\tconst idx = buf.indexOf('\\n');\n\t\t\t\t\tif (idx === -1) break;\n\t\t\t\t\tconst line = buf.slice(0, idx).trim();\n\t\t\t\t\tbuf = buf.slice(idx + 1);\n\t\t\t\t\tif (!line) continue;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst packet = JSON.parse(line) as TcpPacket;\n\t\t\t\t\t\tlastSeenRef.current = Date.now();\n\t\t\t\t\t\tif (packet.type === 'hello') {\n\t\t\t\t\t\t\tif (options.role === 'host') setPeerName(packet.clientName);\n\t\t\t\t\t\t\telse setPeerName(packet.hostName);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (packet.type === 'status') setRemoteState(packet.state);\n\t\t\t\t\t\tif (packet.type === 'ping') writePacket(s, {type: 'pong', sentAt: Date.now()});\n\t\t\t\t\t\tif (packet.type === 'pong') {\n\t\t\t\t\t\t\t// no-op\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// ignore\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\n\t\t\ts.on('data', onData);\n\t\t\ts.on('close', () => {\n\t\t\t\tsetStatus(options.role === 'host' ? 'waiting' : 'disconnected');\n\t\t\t\tsetRemoteState('OFFLINE');\n\t\t\t\tcleanupSocket();\n\t\t\t});\n\t\t\ts.on('error', () => {\n\t\t\t\tsetStatus(options.role === 'host' ? 'waiting' : 'disconnected');\n\t\t\t\tsetRemoteState('OFFLINE');\n\t\t\t});\n\n\t\t\t// Hello handshake.\n\t\t\twritePacket(s, {\n\t\t\t\ttype: 'hello',\n\t\t\t\thostName: options.role === 'host' ? options.localName : options.hostName ?? 'Host',\n\t\t\t\tclientName: options.role === 'client' ? options.localName : 'Client',\n\t\t\t\tsentAt: Date.now()\n\t\t\t});\n\n\t\t\theartbeatRef.current = setInterval(() => {\n\t\t\t\tconst sock = socketRef.current;\n\t\t\t\tif (!sock || sock.destroyed) return;\n\t\t\t\twritePacket(sock, {type: 'ping', sentAt: Date.now()});\n\t\t\t\tconst age = Date.now() - lastSeenRef.current;\n\t\t\t\tif (age > 6000) {\n\t\t\t\t\tsetStatus('disconnected');\n\t\t\t\t\tsetRemoteState('OFFLINE');\n\t\t\t\t\tcleanupSocket();\n\t\t\t\t}\n\t\t\t}, 2000);\n\t\t},\n\t\t[cleanupSocket, options]\n\t);\n\n\tuseEffect(() => {\n\t\tif (options.role === 'host') {\n\t\t\tconst server = net.createServer((socket) => {\n\t\t\t\tattachSocket(socket);\n\t\t\t});\n\n\t\t\tserver.on('error', () => {});\n\n\t\t\tserver.listen(options.port ?? TCP_DEFAULT_PORT, () => {\n\t\t\t\tconst address = server.address();\n\t\t\t\tif (address && typeof address === 'object') setListenPort(address.port);\n\t\t\t});\n\n\t\t\treturn () => {\n\t\t\t\tcleanupSocket();\n\t\t\t\tserver.close();\n\t\t\t};\n\t\t}\n\n\t\tsetStatus('connecting');\n\t\tconst socket = net.createConnection({host: options.hostIp, port: options.tcpPort}, () => {\n\t\t\tattachSocket(socket);\n\t\t});\n\t\tsocket.on('error', () => {\n\t\t\tsetStatus('disconnected');\n\t\t\tsetRemoteState('OFFLINE');\n\t\t});\n\n\t\treturn () => {\n\t\t\tsocket.destroy();\n\t\t\tcleanupSocket();\n\t\t};\n\t}, [attachSocket, cleanupSocket, options]);\n\n\tconst sendStatus = useCallback((state: ActivityState) => {\n\t\tconst socket = socketRef.current;\n\t\tif (!socket || socket.destroyed) return;\n\t\twritePacket(socket, {type: 'status', state, sentAt: Date.now()});\n\t}, []);\n\n\treturn {status, listenPort, peerName, remoteState, sendStatus};\n}\n","export {useActivityMonitor} from './useActivityMonitor.js';\nexport {useAiAgent} from './useAiAgent.js';\nexport {useBroadcaster} from './useBroadcaster.js';\nexport {useCountdown} from './useCountdown.js';\nexport {useScanner} from './useScanner.js';\nexport {useTcpSync} from './useTcpSync.js';\n\n","import React, {useMemo} from 'react';\nimport {Box, Text, useInput} from 'ink';\nimport {useScanner} from '../hooks/index.js';\nimport type {DiscoveredRoom} from '../types.js';\n\nexport function RoomScanner(props: {\n\tonSelectRoom: (room: DiscoveredRoom) => void;\n\tonBack: () => void;\n\tonExit: () => void;\n}) {\n\tconst rooms = useScanner();\n\n\tconst sortedRooms = useMemo(() => {\n\t\treturn [...rooms].sort((a, b) => b.lastSeenAt - a.lastSeenAt);\n\t}, [rooms]);\n\n\tuseInput((input, key) => {\n\t\tif (key.escape || input === 'b') props.onBack();\n\t\tif (input === 'q') props.onExit();\n\n\t\tconst index = Number.parseInt(input, 10);\n\t\tif (Number.isNaN(index)) return;\n\t\tconst room = sortedRooms[index - 1];\n\t\tif (!room) return;\n\t\tprops.onSelectRoom(room);\n\t});\n\n\treturn (\n\t\t<Box flexDirection=\"column\" padding={1}>\n\t\t\t<Text>\n\t\t\t\t<Text color=\"yellow\">正在扫描局域网...</Text> (按 <Text color=\"cyan\">b</Text> 返回,{' '}\n\t\t\t\t<Text color=\"cyan\">q</Text> 退出)\n\t\t\t</Text>\n\t\t\t<Box flexDirection=\"column\" marginTop={1}>\n\t\t\t\t{sortedRooms.length === 0 ? (\n\t\t\t\t\t<Text color=\"gray\">暂无房间广播。</Text>\n\t\t\t\t) : (\n\t\t\t\t\tsortedRooms.map((room, i) => (\n\t\t\t\t\t\t<Text key={`${room.ip}:${room.tcpPort}`}>\n\t\t\t\t\t\t\t<Text color=\"cyan\">[{i + 1}]</Text> {room.roomName} — {room.hostName} @ {room.ip}:{room.tcpPort}\n\t\t\t\t\t\t</Text>\n\t\t\t\t\t))\n\t\t\t\t)}\n\t\t\t</Box>\n\t\t</Box>\n\t);\n}\n","import React, {useMemo, useState} from 'react';\nimport {Box, Text, useInput} from 'ink';\nimport {useAiAgent} from '../hooks/index.js';\n\nexport function AiConsole(props: {\n\tonClose: () => void;\n\tonStartCountdown: (minutes: number) => void;\n\tlocalName: string;\n\tpeerName: string;\n}) {\n\tconst [input, setInput] = useState('');\n\tconst agent = useAiAgent({\n\t\tlocalName: props.localName,\n\t\tpeerName: props.peerName,\n\t\tonStartCountdown: props.onStartCountdown\n\t});\n\n\tconst helpLine = useMemo(\n\t\t() => '示例:倒计时20分钟 / countdown 20 / 问个技术问题',\n\t\t[]\n\t);\n\n\tuseInput(\n\t\t(ch, key) => {\n\t\t\tif (key.escape) {\n\t\t\t\tprops.onClose();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (key.return) {\n\t\t\t\tconst line = input.trim();\n\t\t\t\tsetInput('');\n\t\t\t\tif (!line) return;\n\t\t\t\tvoid agent.ask(line);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (key.backspace || key.delete) {\n\t\t\t\tsetInput((s) => s.slice(0, -1));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (key.ctrl || key.meta) return;\n\t\t\tif (ch) setInput((s) => s + ch);\n\t\t},\n\t\t{isActive: true}\n\t);\n\n\tconst lines = agent.lines.slice(-12);\n\n\treturn (\n\t\t<Box flexDirection=\"column\" borderStyle=\"round\" paddingX={1} paddingY={0}>\n\t\t\t<Box justifyContent=\"space-between\">\n\t\t\t\t<Text color=\"cyan\">AI Console</Text>\n\t\t\t\t<Text color=\"gray\">{agent.busy ? 'Thinking…' : 'Esc 关闭'}</Text>\n\t\t\t</Box>\n\n\t\t\t<Box flexDirection=\"column\" marginTop={1}>\n\t\t\t\t<Text color=\"gray\">{helpLine}</Text>\n\t\t\t</Box>\n\n\t\t\t<Box flexDirection=\"column\" marginTop={1}>\n\t\t\t\t{lines.length === 0 ? <Text color=\"gray\">(幽灵还在壳里…)</Text> : null}\n\t\t\t\t{lines.map((l, i) => (\n\t\t\t\t\t<Text key={`${l.kind}:${l.at}:${i}`} color={l.kind === 'user' ? 'yellow' : 'white'}>\n\t\t\t\t\t\t{l.text}\n\t\t\t\t\t</Text>\n\t\t\t\t))}\n\t\t\t</Box>\n\n\t\t\t<Box marginTop={1}>\n\t\t\t\t<Text color=\"green\">{'>'} </Text>\n\t\t\t\t<Text>{input}</Text>\n\t\t\t</Box>\n\t\t</Box>\n\t);\n}\n","import React from 'react';\nimport {Box, Text} from 'ink';\nimport type {ActivityState} from '../protocol.js';\n\nconst AVATAR: Record<ActivityState, {text: string; color?: string}> = {\n\tTYPING: {text: '( >_<)===3', color: 'green'},\n\tIDLE: {text: '( -.-)Zzz', color: 'yellow'},\n\tOFFLINE: {text: '( x_x)', color: 'gray'}\n};\n\nexport function AvatarDisplay(props: {state: ActivityState}) {\n\tconst avatar = AVATAR[props.state];\n\treturn (\n\t\t<Box marginTop={1}>\n\t\t\t<Text color={avatar.color}>{avatar.text}</Text>\n\t\t</Box>\n\t);\n}\n","import React from 'react';\nimport {Box, Text} from 'ink';\nimport type {ActivityState} from '../protocol.js';\n\nconst FRAMES: Record<ActivityState, {color?: string; lines: string[]}> = {\n\tTYPING: {\n\t\tcolor: 'green',\n\t\tlines: [' /\\\\_/\\\\ ', '( >_<) ', ' /|_|\\\\\\\\ ', ' / \\\\\\\\ ']\n\t},\n\tIDLE: {\n\t\tcolor: 'yellow',\n\t\tlines: [' /\\\\_/\\\\ ', '( -.-) ', ' /|_|\\\\\\\\ ', ' / \\\\\\\\ ']\n\t},\n\tOFFLINE: {\n\t\tcolor: 'gray',\n\t\tlines: [' /\\\\_/\\\\ ', '( x_x) ', ' /|_|\\\\\\\\ ', ' / \\\\\\\\ ']\n\t}\n};\n\nexport function BuddyAvatar(props: {state: ActivityState}) {\n\tconst frame = FRAMES[props.state];\n\treturn (\n\t\t<Box flexDirection=\"column\" marginTop={1}>\n\t\t\t{frame.lines.map((line, i) => (\n\t\t\t\t<Text key={`${props.state}:${i}`} color={frame.color}>\n\t\t\t\t\t{line}\n\t\t\t\t</Text>\n\t\t\t))}\n\t\t</Box>\n\t);\n}\n\n","import React from 'react';\nimport {Box, Text} from 'ink';\nimport type {ConnectionStatus} from '../protocol.js';\n\nfunction statusText(status: ConnectionStatus) {\n\tswitch (status) {\n\t\tcase 'waiting':\n\t\t\treturn {label: 'Waiting', color: 'yellow'};\n\t\tcase 'connecting':\n\t\t\treturn {label: 'Connecting', color: 'yellow'};\n\t\tcase 'connected':\n\t\t\treturn {label: 'Connected via TCP', color: 'green'};\n\t\tcase 'disconnected':\n\t\t\treturn {label: 'Disconnected', color: 'red'};\n\t}\n}\n\nexport function StatusHeader(props: {\n\trole: 'host' | 'client';\n\tstatus: ConnectionStatus;\n\thostIp?: string;\n\ttcpPort?: number;\n\tcountdownLabel?: string | null;\n}) {\n\tconst st = statusText(props.status);\n\treturn (\n\t\t<Box justifyContent=\"space-between\">\n\t\t\t<Box>\n\t\t\t\t<Text color={st.color}>{st.label}</Text>\n\t\t\t\t{props.role === 'host' ? (\n\t\t\t\t\t<Text color=\"gray\">{props.tcpPort ? ` — TCP :${props.tcpPort}` : ''}</Text>\n\t\t\t\t) : (\n\t\t\t\t\t<Text color=\"gray\">\n\t\t\t\t\t\t{props.hostIp && props.tcpPort ? ` — ${props.hostIp}:${props.tcpPort}` : ''}\n\t\t\t\t\t</Text>\n\t\t\t\t)}\n\t\t\t</Box>\n\t\t\t<Box>\n\t\t\t\t{props.countdownLabel ? <Text color=\"cyan\">Focus {props.countdownLabel}</Text> : <Text> </Text>}\n\t\t\t</Box>\n\t\t</Box>\n\t);\n}\n","export {AiConsole} from './AiConsole.js';\nexport {AvatarDisplay} from './AvatarDisplay.js';\nexport {BuddyAvatar} from './BuddyAvatar.js';\nexport {StatusHeader} from './StatusHeader.js';\n","import React, {useCallback, useEffect, useMemo, useState} from 'react';\nimport {Box, Text, useInput} from 'ink';\nimport type {ActivityState} from '../protocol.js';\nimport {AiConsole, BuddyAvatar, StatusHeader} from '../components/index.js';\nimport {useActivityMonitor, useBroadcaster, useCountdown, useTcpSync} from '../hooks/index.js';\n\nexport function Session(\n\tprops:\n\t\t| {role: 'host'; localName: string; onExit: () => void}\n\t\t| {\n\t\t\t\trole: 'client';\n\t\t\t\tlocalName: string;\n\t\t\t\tonExit: () => void;\n\t\t\t\thostIp: string;\n\t\t\t\ttcpPort: number;\n\t\t\t\troomName: string;\n\t\t\t\thostName: string;\n\t\t }\n) {\n\tconst roomName = useMemo(() => `${props.localName}'s Room`, [props.localName]);\n\n\tconst [showAi, setShowAi] = useState(false);\n\tconst countdown = useCountdown();\n\n\tconst tcpOptions = useMemo(() => {\n\t\treturn props.role === 'host'\n\t\t\t? ({role: 'host', localName: props.localName} as const)\n\t\t\t: ({\n\t\t\t\t\trole: 'client',\n\t\t\t\t\tlocalName: props.localName,\n\t\t\t\t\thostIp: props.hostIp,\n\t\t\t\t\ttcpPort: props.tcpPort,\n\t\t\t\t\thostName: props.hostName\n\t\t\t } as const);\n\t}, [\n\t\tprops.role,\n\t\tprops.localName,\n\t\tprops.role === 'client' ? props.hostIp : '',\n\t\tprops.role === 'client' ? props.tcpPort : 0,\n\t\tprops.role === 'client' ? props.hostName : ''\n\t]);\n\n\tconst tcp = useTcpSync(tcpOptions);\n\n\tconst broadcasterOptions = useMemo(() => {\n\t\treturn props.role === 'host'\n\t\t\t? ({\n\t\t\t\t\tenabled: true,\n\t\t\t\t\thostName: props.localName,\n\t\t\t\t\troomName,\n\t\t\t\t\ttcpPort: tcp.listenPort\n\t\t\t } as const)\n\t\t\t: ({enabled: false} as const);\n\t}, [props.role, props.localName, roomName, tcp.listenPort]);\n\n\tuseBroadcaster(broadcasterOptions);\n\n\tconst localActivity = useActivityMonitor();\n\n\tconst remoteActivity: ActivityState = tcp.remoteState ?? 'OFFLINE';\n\n\tconst onToggleAi = useCallback(() => setShowAi((v) => !v), []);\n\tconst onCloseAi = useCallback(() => setShowAi(false), []);\n\n\tuseInput(\n\t\t(input, key) => {\n\t\t\tif (input === 'q') props.onExit();\n\t\t\tif (input === '/' && !key.ctrl && !key.meta) onToggleAi();\n\t\t},\n\t\t{isActive: !showAi}\n\t);\n\n\tconst buddyName =\n\t\tprops.role === 'host'\n\t\t\t? tcp.peerName ?? 'Waiting...'\n\t\t\t: `${props.hostName ?? 'Host'} (${props.roomName ?? 'Room'})`;\n\n\tconst localState = localActivity.state;\n\tconst localLabel = props.role === 'host' ? `${props.localName} (Host)` : `${props.localName} (Client)`;\n\n\t// Sync local activity state to peer.\n\tuseEffect(() => {\n\t\tif (tcp.status !== 'connected') return;\n\t\ttcp.sendStatus(localState);\n\t}, [localState, tcp.status, tcp.sendStatus]);\n\n\treturn (\n\t\t<Box flexDirection=\"column\" padding={1}>\n\t\t\t<StatusHeader\n\t\t\t\trole={props.role}\n\t\t\t\tstatus={tcp.status}\n\t\t\t\thostIp={props.role === 'client' ? props.hostIp : undefined}\n\t\t\t\ttcpPort={props.role === 'client' ? props.tcpPort : tcp.listenPort}\n\t\t\t\tcountdownLabel={countdown.label}\n\t\t\t/>\n\n\t\t\t<Box flexDirection=\"row\" gap={4} marginTop={1}>\n\t\t\t\t<Box flexDirection=\"column\" width=\"50%\">\n\t\t\t\t\t<Text color=\"cyan\">{localLabel}</Text>\n\t\t\t\t\t<BuddyAvatar state={localState} />\n\t\t\t\t</Box>\n\n\t\t\t\t<Box flexDirection=\"column\" width=\"50%\">\n\t\t\t\t\t<Text color=\"magenta\">{buddyName}</Text>\n\t\t\t\t\t<BuddyAvatar state={remoteActivity} />\n\t\t\t\t</Box>\n\t\t\t</Box>\n\n\t\t\t<Box marginTop={1}>\n\t\t\t\t<Text color=\"gray\">\n\t\t\t\t\t按 <Text color=\"cyan\">/</Text> 召唤 AI Console,按 <Text color=\"cyan\">q</Text> 返回菜单。\n\t\t\t\t</Text>\n\t\t\t</Box>\n\n\t\t\t{showAi ? (\n\t\t\t\t<Box marginTop={1}>\n\t\t\t\t\t<AiConsole\n\t\t\t\t\t\tonClose={onCloseAi}\n\t\t\t\t\t\tonStartCountdown={countdown.start}\n\t\t\t\t\t\tlocalName={props.localName}\n\t\t\t\t\t\tpeerName={tcp.peerName ?? (props.role === 'client' ? props.hostName : undefined) ?? 'Buddy'}\n\t\t\t\t\t/>\n\t\t\t\t</Box>\n\t\t\t) : null}\n\t\t</Box>\n\t);\n}\n","export {MainMenu} from './MainMenu.js';\nexport {RoomScanner} from './RoomScanner.js';\nexport {Session} from './Session.js';\n\n","import React, {useCallback, useMemo, useState} from 'react';\nimport os from 'node:os';\nimport {useApp} from 'ink';\nimport {MainMenu, RoomScanner, Session} from '../views/index.js';\n\ntype View =\n\t| {name: 'MENU'}\n\t| {name: 'SCANNING'}\n\t| {name: 'SESSION'; role: 'host'}\n\t| {name: 'SESSION'; role: 'client'; hostIp: string; tcpPort: number; roomName: string; hostName: string};\n\nexport function App() {\n\tconst {exit} = useApp();\n\tconst [view, setView] = useState<View>({name: 'MENU'});\n\n\tconst localName = useMemo(() => os.hostname(), []);\n\n\tconst goMenu = useCallback(() => setView({name: 'MENU'}), []);\n\n\tif (view.name === 'MENU') {\n\t\treturn (\n\t\t\t<MainMenu\n\t\t\t\tonHost={() => setView({name: 'SESSION', role: 'host'})}\n\t\t\t\tonJoin={() => setView({name: 'SCANNING'})}\n\t\t\t\tonExit={() => exit()}\n\t\t\t/>\n\t\t);\n\t}\n\n\tif (view.name === 'SCANNING') {\n\t\treturn (\n\t\t\t<RoomScanner\n\t\t\t\tonBack={goMenu}\n\t\t\t\tonExit={() => exit()}\n\t\t\t\tonSelectRoom={(room) =>\n\t\t\t\t\tsetView({\n\t\t\t\t\t\tname: 'SESSION',\n\t\t\t\t\t\trole: 'client',\n\t\t\t\t\t\thostIp: room.ip,\n\t\t\t\t\t\ttcpPort: room.tcpPort,\n\t\t\t\t\t\troomName: room.roomName,\n\t\t\t\t\t\thostName: room.hostName\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t/>\n\t\t);\n\t}\n\n\tif (view.name === 'SESSION' && view.role === 'host') {\n\t\treturn <Session localName={localName} role=\"host\" onExit={goMenu} />;\n\t}\n\n\treturn (\n\t\t<Session\n\t\t\tlocalName={localName}\n\t\t\trole=\"client\"\n\t\t\tonExit={goMenu}\n\t\t\thostIp={view.hostIp}\n\t\t\ttcpPort={view.tcpPort}\n\t\t\troomName={view.roomName}\n\t\t\thostName={view.hostName}\n\t\t/>\n\t);\n}\n","export {App} from './App.js';\n\n","process.env.NODE_ENV ??= 'production';\n\nconst React = await import('react');\nconst {render} = await import('ink');\nconst {App} = await import('./app/index.js');\n\nrender(React.createElement(App));\n"],"mappings":"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","names":["useInput","useEffect","useRef","useState","useEffect","useCallback","useEffect","useRef","useState","useEffect","useState","dgram","useCallback","useEffect","useRef","useState","socket","Box","Text","useInput","jsx","jsxs","useMemo","useState","Box","Text","useInput","jsx","jsxs","Box","Text","jsx","Box","Text","jsx","Box","Text","jsx","jsxs","useCallback","useEffect","useMemo","useState","Box","Text","useInput","jsx","jsxs","useCallback","useMemo","useState","os","jsx","React","App"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@three333/termbuddy",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"termbuddy": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "tsup --watch",
|
|
11
|
+
"build": "tsup",
|
|
12
|
+
"start": "node dist/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@langchain/core": "^1.1.8",
|
|
16
|
+
"@langchain/openai": "^1.2.0",
|
|
17
|
+
"ink": "^6.6.0",
|
|
18
|
+
"langchain": "^1.2.3",
|
|
19
|
+
"react": "^19.2.3"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^25.0.3",
|
|
23
|
+
"@types/react": "^19.2.7",
|
|
24
|
+
"ts-node": "^10.9.2",
|
|
25
|
+
"tsup": "^8.5.1",
|
|
26
|
+
"typescript": "^5.9.3"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/app/App.tsx
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React, {useCallback, useMemo, useState} from 'react';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import {useApp} from 'ink';
|
|
4
|
+
import {MainMenu, RoomScanner, Session} from '../views/index.js';
|
|
5
|
+
|
|
6
|
+
type View =
|
|
7
|
+
| {name: 'MENU'}
|
|
8
|
+
| {name: 'SCANNING'}
|
|
9
|
+
| {name: 'SESSION'; role: 'host'}
|
|
10
|
+
| {name: 'SESSION'; role: 'client'; hostIp: string; tcpPort: number; roomName: string; hostName: string};
|
|
11
|
+
|
|
12
|
+
export function App() {
|
|
13
|
+
const {exit} = useApp();
|
|
14
|
+
const [view, setView] = useState<View>({name: 'MENU'});
|
|
15
|
+
|
|
16
|
+
const localName = useMemo(() => os.hostname(), []);
|
|
17
|
+
|
|
18
|
+
const goMenu = useCallback(() => setView({name: 'MENU'}), []);
|
|
19
|
+
|
|
20
|
+
if (view.name === 'MENU') {
|
|
21
|
+
return (
|
|
22
|
+
<MainMenu
|
|
23
|
+
onHost={() => setView({name: 'SESSION', role: 'host'})}
|
|
24
|
+
onJoin={() => setView({name: 'SCANNING'})}
|
|
25
|
+
onExit={() => exit()}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (view.name === 'SCANNING') {
|
|
31
|
+
return (
|
|
32
|
+
<RoomScanner
|
|
33
|
+
onBack={goMenu}
|
|
34
|
+
onExit={() => exit()}
|
|
35
|
+
onSelectRoom={(room) =>
|
|
36
|
+
setView({
|
|
37
|
+
name: 'SESSION',
|
|
38
|
+
role: 'client',
|
|
39
|
+
hostIp: room.ip,
|
|
40
|
+
tcpPort: room.tcpPort,
|
|
41
|
+
roomName: room.roomName,
|
|
42
|
+
hostName: room.hostName
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (view.name === 'SESSION' && view.role === 'host') {
|
|
50
|
+
return <Session localName={localName} role="host" onExit={goMenu} />;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Session
|
|
55
|
+
localName={localName}
|
|
56
|
+
role="client"
|
|
57
|
+
onExit={goMenu}
|
|
58
|
+
hostIp={view.hostIp}
|
|
59
|
+
tcpPort={view.tcpPort}
|
|
60
|
+
roomName={view.roomName}
|
|
61
|
+
hostName={view.hostName}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
}
|
package/src/app/index.ts
ADDED
package/src/cli.tsx
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React, {useMemo, useState} from 'react';
|
|
2
|
+
import {Box, Text, useInput} from 'ink';
|
|
3
|
+
import {useAiAgent} from '../hooks/index.js';
|
|
4
|
+
|
|
5
|
+
export function AiConsole(props: {
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
onStartCountdown: (minutes: number) => void;
|
|
8
|
+
localName: string;
|
|
9
|
+
peerName: string;
|
|
10
|
+
}) {
|
|
11
|
+
const [input, setInput] = useState('');
|
|
12
|
+
const agent = useAiAgent({
|
|
13
|
+
localName: props.localName,
|
|
14
|
+
peerName: props.peerName,
|
|
15
|
+
onStartCountdown: props.onStartCountdown
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const helpLine = useMemo(
|
|
19
|
+
() => '示例:倒计时20分钟 / countdown 20 / 问个技术问题',
|
|
20
|
+
[]
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
useInput(
|
|
24
|
+
(ch, key) => {
|
|
25
|
+
if (key.escape) {
|
|
26
|
+
props.onClose();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (key.return) {
|
|
31
|
+
const line = input.trim();
|
|
32
|
+
setInput('');
|
|
33
|
+
if (!line) return;
|
|
34
|
+
void agent.ask(line);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (key.backspace || key.delete) {
|
|
39
|
+
setInput((s) => s.slice(0, -1));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (key.ctrl || key.meta) return;
|
|
44
|
+
if (ch) setInput((s) => s + ch);
|
|
45
|
+
},
|
|
46
|
+
{isActive: true}
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const lines = agent.lines.slice(-12);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Box flexDirection="column" borderStyle="round" paddingX={1} paddingY={0}>
|
|
53
|
+
<Box justifyContent="space-between">
|
|
54
|
+
<Text color="cyan">AI Console</Text>
|
|
55
|
+
<Text color="gray">{agent.busy ? 'Thinking…' : 'Esc 关闭'}</Text>
|
|
56
|
+
</Box>
|
|
57
|
+
|
|
58
|
+
<Box flexDirection="column" marginTop={1}>
|
|
59
|
+
<Text color="gray">{helpLine}</Text>
|
|
60
|
+
</Box>
|
|
61
|
+
|
|
62
|
+
<Box flexDirection="column" marginTop={1}>
|
|
63
|
+
{lines.length === 0 ? <Text color="gray">(幽灵还在壳里…)</Text> : null}
|
|
64
|
+
{lines.map((l, i) => (
|
|
65
|
+
<Text key={`${l.kind}:${l.at}:${i}`} color={l.kind === 'user' ? 'yellow' : 'white'}>
|
|
66
|
+
{l.text}
|
|
67
|
+
</Text>
|
|
68
|
+
))}
|
|
69
|
+
</Box>
|
|
70
|
+
|
|
71
|
+
<Box marginTop={1}>
|
|
72
|
+
<Text color="green">{'>'} </Text>
|
|
73
|
+
<Text>{input}</Text>
|
|
74
|
+
</Box>
|
|
75
|
+
</Box>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {Box, Text} from 'ink';
|
|
3
|
+
import type {ActivityState} from '../protocol.js';
|
|
4
|
+
|
|
5
|
+
const AVATAR: Record<ActivityState, {text: string; color?: string}> = {
|
|
6
|
+
TYPING: {text: '( >_<)===3', color: 'green'},
|
|
7
|
+
IDLE: {text: '( -.-)Zzz', color: 'yellow'},
|
|
8
|
+
OFFLINE: {text: '( x_x)', color: 'gray'}
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function AvatarDisplay(props: {state: ActivityState}) {
|
|
12
|
+
const avatar = AVATAR[props.state];
|
|
13
|
+
return (
|
|
14
|
+
<Box marginTop={1}>
|
|
15
|
+
<Text color={avatar.color}>{avatar.text}</Text>
|
|
16
|
+
</Box>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {Box, Text} from 'ink';
|
|
3
|
+
import type {ActivityState} from '../protocol.js';
|
|
4
|
+
|
|
5
|
+
const FRAMES: Record<ActivityState, {color?: string; lines: string[]}> = {
|
|
6
|
+
TYPING: {
|
|
7
|
+
color: 'green',
|
|
8
|
+
lines: [' /\\_/\\ ', '( >_<) ', ' /|_|\\\\ ', ' / \\\\ ']
|
|
9
|
+
},
|
|
10
|
+
IDLE: {
|
|
11
|
+
color: 'yellow',
|
|
12
|
+
lines: [' /\\_/\\ ', '( -.-) ', ' /|_|\\\\ ', ' / \\\\ ']
|
|
13
|
+
},
|
|
14
|
+
OFFLINE: {
|
|
15
|
+
color: 'gray',
|
|
16
|
+
lines: [' /\\_/\\ ', '( x_x) ', ' /|_|\\\\ ', ' / \\\\ ']
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function BuddyAvatar(props: {state: ActivityState}) {
|
|
21
|
+
const frame = FRAMES[props.state];
|
|
22
|
+
return (
|
|
23
|
+
<Box flexDirection="column" marginTop={1}>
|
|
24
|
+
{frame.lines.map((line, i) => (
|
|
25
|
+
<Text key={`${props.state}:${i}`} color={frame.color}>
|
|
26
|
+
{line}
|
|
27
|
+
</Text>
|
|
28
|
+
))}
|
|
29
|
+
</Box>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {Box, Text} from 'ink';
|
|
3
|
+
import type {ConnectionStatus} from '../protocol.js';
|
|
4
|
+
|
|
5
|
+
function statusText(status: ConnectionStatus) {
|
|
6
|
+
switch (status) {
|
|
7
|
+
case 'waiting':
|
|
8
|
+
return {label: 'Waiting', color: 'yellow'};
|
|
9
|
+
case 'connecting':
|
|
10
|
+
return {label: 'Connecting', color: 'yellow'};
|
|
11
|
+
case 'connected':
|
|
12
|
+
return {label: 'Connected via TCP', color: 'green'};
|
|
13
|
+
case 'disconnected':
|
|
14
|
+
return {label: 'Disconnected', color: 'red'};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function StatusHeader(props: {
|
|
19
|
+
role: 'host' | 'client';
|
|
20
|
+
status: ConnectionStatus;
|
|
21
|
+
hostIp?: string;
|
|
22
|
+
tcpPort?: number;
|
|
23
|
+
countdownLabel?: string | null;
|
|
24
|
+
}) {
|
|
25
|
+
const st = statusText(props.status);
|
|
26
|
+
return (
|
|
27
|
+
<Box justifyContent="space-between">
|
|
28
|
+
<Box>
|
|
29
|
+
<Text color={st.color}>{st.label}</Text>
|
|
30
|
+
{props.role === 'host' ? (
|
|
31
|
+
<Text color="gray">{props.tcpPort ? ` — TCP :${props.tcpPort}` : ''}</Text>
|
|
32
|
+
) : (
|
|
33
|
+
<Text color="gray">
|
|
34
|
+
{props.hostIp && props.tcpPort ? ` — ${props.hostIp}:${props.tcpPort}` : ''}
|
|
35
|
+
</Text>
|
|
36
|
+
)}
|
|
37
|
+
</Box>
|
|
38
|
+
<Box>
|
|
39
|
+
{props.countdownLabel ? <Text color="cyan">Focus {props.countdownLabel}</Text> : <Text> </Text>}
|
|
40
|
+
</Box>
|
|
41
|
+
</Box>
|
|
42
|
+
);
|
|
43
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export {useActivityMonitor} from './useActivityMonitor.js';
|
|
2
|
+
export {useAiAgent} from './useAiAgent.js';
|
|
3
|
+
export {useBroadcaster} from './useBroadcaster.js';
|
|
4
|
+
export {useCountdown} from './useCountdown.js';
|
|
5
|
+
export {useScanner} from './useScanner.js';
|
|
6
|
+
export {useTcpSync} from './useTcpSync.js';
|
|
7
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {useEffect, useRef, useState} from 'react';
|
|
2
|
+
import {useInput} from 'ink';
|
|
3
|
+
import type {ActivityState} from '../protocol.js';
|
|
4
|
+
|
|
5
|
+
export function useActivityMonitor(options?: {idleAfterMs?: number}): {state: ActivityState} {
|
|
6
|
+
const idleAfterMs = options?.idleAfterMs ?? 1500;
|
|
7
|
+
const [state, setState] = useState<ActivityState>('IDLE');
|
|
8
|
+
|
|
9
|
+
const lastActivityRef = useRef<number>(Date.now());
|
|
10
|
+
|
|
11
|
+
useInput(() => {
|
|
12
|
+
lastActivityRef.current = Date.now();
|
|
13
|
+
setState('TYPING');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const id = setInterval(() => {
|
|
18
|
+
const delta = Date.now() - lastActivityRef.current;
|
|
19
|
+
if (delta >= idleAfterMs) setState('IDLE');
|
|
20
|
+
}, 200);
|
|
21
|
+
return () => clearInterval(id);
|
|
22
|
+
}, [idleAfterMs]);
|
|
23
|
+
|
|
24
|
+
return {state};
|
|
25
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import {useCallback, useEffect, useRef, useState} from 'react';
|
|
2
|
+
import {createAgent, initChatModel, tool} from 'langchain';
|
|
3
|
+
|
|
4
|
+
type LineKind = 'user' | 'ai' | 'system';
|
|
5
|
+
export type AiLine = {kind: LineKind; text: string; at: number};
|
|
6
|
+
|
|
7
|
+
function contentToText(content: unknown): string {
|
|
8
|
+
if (typeof content === 'string') return content;
|
|
9
|
+
if (!content) return '';
|
|
10
|
+
if (Array.isArray(content)) {
|
|
11
|
+
return content
|
|
12
|
+
.map((part) => {
|
|
13
|
+
if (typeof part === 'string') return part;
|
|
14
|
+
if (typeof part === 'object' && part && 'text' in part) return String((part as any).text ?? '');
|
|
15
|
+
return '';
|
|
16
|
+
})
|
|
17
|
+
.join('');
|
|
18
|
+
}
|
|
19
|
+
if (typeof content === 'object' && 'text' in (content as any)) return String((content as any).text ?? '');
|
|
20
|
+
return String(content);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function lastAiText(messages: unknown[]): string | null {
|
|
24
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
25
|
+
const m: any = messages[i];
|
|
26
|
+
const type = typeof m?.getType === 'function' ? m.getType() : typeof m?._getType === 'function' ? m._getType() : m?.type;
|
|
27
|
+
if (type === 'ai') {
|
|
28
|
+
const t = contentToText(m?.content);
|
|
29
|
+
return t || '';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createSystemPrompt(context: {localName: string; peerName: string}) {
|
|
36
|
+
return [
|
|
37
|
+
'你是 TermBuddy 里的“壳中幽灵 (Ghost in the Shell)”。',
|
|
38
|
+
'默认隐形;被 / 唤醒时出现。风格:极简、干练、少废话。',
|
|
39
|
+
'你可以使用工具来操控应用功能(例如倒计时)。',
|
|
40
|
+
'如果用户提到“倒计时/专注/计时/countdown”,优先调用 start_countdown。',
|
|
41
|
+
`当前上下文:我叫 ${context.localName};同桌叫 ${context.peerName}。`
|
|
42
|
+
].join('\n');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function useAiAgent(options: {
|
|
46
|
+
localName: string;
|
|
47
|
+
peerName: string;
|
|
48
|
+
onStartCountdown?: (minutes: number) => void;
|
|
49
|
+
}) {
|
|
50
|
+
const [lines, setLines] = useState<AiLine[]>([]);
|
|
51
|
+
const [busy, setBusy] = useState(false);
|
|
52
|
+
|
|
53
|
+
const agentRef = useRef<Awaited<ReturnType<typeof createAgent>> | null>(null);
|
|
54
|
+
const agentInitRef = useRef<Promise<Awaited<ReturnType<typeof createAgent>>> | null>(null);
|
|
55
|
+
const stateRef = useRef<{messages: unknown[]}>({messages: []});
|
|
56
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
57
|
+
|
|
58
|
+
const append = useCallback((line: AiLine) => {
|
|
59
|
+
setLines((prev) => [...prev, line]);
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
62
|
+
const updateLine = useCallback((at: number, text: string) => {
|
|
63
|
+
setLines((prev) => {
|
|
64
|
+
const idx = prev.findIndex((l) => l.at === at);
|
|
65
|
+
if (idx === -1) return prev;
|
|
66
|
+
const next = [...prev];
|
|
67
|
+
next[idx] = {...next[idx], text};
|
|
68
|
+
return next;
|
|
69
|
+
});
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
const ensureAgent = useCallback(async () => {
|
|
73
|
+
if (agentRef.current) return agentRef.current;
|
|
74
|
+
agentInitRef.current ??= (async () => {
|
|
75
|
+
const startCountdown = tool(
|
|
76
|
+
async (input: {minutes: number}) => {
|
|
77
|
+
const minutes = Number(input.minutes);
|
|
78
|
+
if (!Number.isFinite(minutes) || minutes <= 0) return '倒计时分钟数无效。';
|
|
79
|
+
options.onStartCountdown?.(minutes);
|
|
80
|
+
return `已开始倒计时 ${minutes} 分钟。`;
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'start_countdown',
|
|
84
|
+
description: '开始一个专注倒计时(分钟)。',
|
|
85
|
+
schema: {
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties: {
|
|
88
|
+
minutes: {type: 'integer', minimum: 1, maximum: 180, description: '倒计时分钟数'}
|
|
89
|
+
},
|
|
90
|
+
required: ['minutes'],
|
|
91
|
+
additionalProperties: false
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const sessionInfo = tool(
|
|
97
|
+
async () => {
|
|
98
|
+
return JSON.stringify(
|
|
99
|
+
{
|
|
100
|
+
localName: options.localName,
|
|
101
|
+
peerName: options.peerName
|
|
102
|
+
},
|
|
103
|
+
null,
|
|
104
|
+
2
|
|
105
|
+
);
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: 'session_info',
|
|
109
|
+
description: '获取当前会话上下文(本地昵称、同桌昵称)。',
|
|
110
|
+
schema: {type: 'object', properties: {}, additionalProperties: false}
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const modelId = process.env.TERMBUDDY_MODEL ?? 'openai:gpt-4o-mini';
|
|
115
|
+
const llm = await initChatModel(modelId, {
|
|
116
|
+
temperature: 0.2,
|
|
117
|
+
maxTokens: 800,
|
|
118
|
+
timeout: 30_000
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return createAgent({
|
|
122
|
+
llm,
|
|
123
|
+
tools: [startCountdown, sessionInfo],
|
|
124
|
+
prompt: createSystemPrompt({localName: options.localName, peerName: options.peerName}),
|
|
125
|
+
name: 'ghost'
|
|
126
|
+
});
|
|
127
|
+
})();
|
|
128
|
+
|
|
129
|
+
agentRef.current = await agentInitRef.current;
|
|
130
|
+
return agentRef.current;
|
|
131
|
+
}, [options.localName, options.onStartCountdown, options.peerName]);
|
|
132
|
+
|
|
133
|
+
const ask = useCallback(
|
|
134
|
+
async (text: string) => {
|
|
135
|
+
append({kind: 'user', text: `> ${text}`, at: Date.now()});
|
|
136
|
+
|
|
137
|
+
const aiAt = Date.now() + 1;
|
|
138
|
+
append({kind: 'ai', text: '…', at: aiAt});
|
|
139
|
+
|
|
140
|
+
abortRef.current?.abort();
|
|
141
|
+
abortRef.current = new AbortController();
|
|
142
|
+
|
|
143
|
+
setBusy(true);
|
|
144
|
+
try {
|
|
145
|
+
const agent = await ensureAgent();
|
|
146
|
+
const stream = await agent.stream(
|
|
147
|
+
{
|
|
148
|
+
messages: [...stateRef.current.messages, {role: 'user', content: text}]
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
streamMode: 'values',
|
|
152
|
+
signal: abortRef.current.signal
|
|
153
|
+
} as any
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
for await (const chunk of stream as any) {
|
|
157
|
+
const messages = (chunk?.messages ?? []) as unknown[];
|
|
158
|
+
if (messages.length > 0) stateRef.current.messages = messages;
|
|
159
|
+
const t = lastAiText(messages);
|
|
160
|
+
if (t !== null) updateLine(aiAt, t);
|
|
161
|
+
}
|
|
162
|
+
} catch (e) {
|
|
163
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
164
|
+
updateLine(aiAt, `(AI 出错)${msg}`);
|
|
165
|
+
} finally {
|
|
166
|
+
setBusy(false);
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
[append, ensureAgent, updateLine]
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
return () => abortRef.current?.abort();
|
|
174
|
+
}, []);
|
|
175
|
+
|
|
176
|
+
return {lines, ask, busy};
|
|
177
|
+
}
|