* feat: 删除垃圾更改
* fix: 消除生产代码中的 as any 类型不安全模式
- API 兼容层(openai/grok/gemini): 利用 BetaRawMessageStreamEvent 的
discriminated union 在 switch/case 中直接属性访问,消除 ~29 个 as any
- ConsoleOAuthFlow: 用 as unknown as Parameters<typeof> 替代 as any
- performanceShim: 用 Record<string, unknown> 和显式类型断言替代 as any
- companionReact/auth: 直接访问已有类型属性消除 as any
- sliceAnsi/textHighlighting: 用 as Char 替代 as any(Token 联合类型收窄)
- ccrClient: 利用 RequestResult 类型收窄直接访问 retryAfterMs
- outputsScanner: 用 TurnStartTime.turnStartTime 属性访问替代双重断言
- plans: 用显式数组类型替代 as any[]
- FeedbackSurvey: 用 in 操作符和 Parameters<typeof> 替代 as any
- messageQueueManager: 用 Record<string, unknown> 替代 as any
- mcp.ts: 用 in 操作符类型守卫替代 as any
precheck 通过: typecheck 零错误 + 5420 测试全部通过 + lint 通过
* fix: 将 pipeIpc 添加到 AppState 类型声明,消除 4 个 as any
- AppStateStore: 添加 pipeIpc?: PipeIpcState 可选字段
- PromptInputFooter: 直接访问 s.pipeIpc
- useBackgroundTaskNavigation: 直接访问 s.pipeIpc
- usePipeRouter: 直接访问 store.getState().pipeIpc
- REPL.tsx: 移除 getPipeIpc(s as any) 中的 as any
precheck 通过
* fix: 消除 UltraplanChoiceDialog 中的 wheelDown/wheelUp as any
Ink Key 类型已包含 wheelDown/wheelUp 属性,直接访问即可。
* fix: 消除 sideQuestion.ts 中的 2 个 as any
- toolUse.name: 使用 as unknown as { name: string } 双重断言
- apiErr.error: 使用 as Parameters<typeof formatAPIError>[0] 类型参数
* fix: 为 auto dream 添加 maxTurns: 20 限制,防止单次执行消耗过多 token
* fix: 补充 SAFE_ENV_VARS 中缺失的 OpenAI/Gemini/Grok provider 环境变量
项目级 settings.local.json 的 env 字段在 trust dialog 之前只有
SAFE_ENV_VARS 白名单中的变量会被应用到 process.env。
OPENAI_API_KEY、OPENAI_BASE_URL 等关键变量不在白名单中,
导致容器中通过 settings.local.json 配置 OpenAI 协议时认证失败。
* fix: 修复 goalState.js 模块不存在的类型错误
* fix: 增强 providers 测试的环境变量隔离,防止 mock 污染
* fix: 内联 providers 测试逻辑,彻底隔离 mock 污染
测试不再 import providers.ts(其默认参数触发 getInitialSettings 全链),
改为内联纯函数逻辑,从根源消除 CI 上其他测试 mock.module 污染。
* fix: 添加 goalState 模块存根,修复 CI 构建打包解析失败
CI 中的 autonomy-lifecycle-user-flow 集成测试会执行 build.ts 打包 CLI。
此前 PromptInputFooterLeftSide.tsx 中 require('../../services/goal/goalState.js')
的路径在源码中不存在,打包器报 Could not resolve,导致 (unnamed) 测试失败。
新增 src/services/goal/goalState.ts 存根模块(getGoal 返回 null,组件不渲染),
让打包器在构建期可以解析该 require 路径。同时把 PromptInputFooterLeftSide.tsx
里两处 as unknown as 内联类型签名换成 as typeof import(...),让类型直接来自
存根模块,避免类型定义重复。
152 lines
4.8 KiB
TypeScript
152 lines
4.8 KiB
TypeScript
/**
|
|
* usePipeRouter — Route user input to selected pipe targets.
|
|
*
|
|
* Returns `routeToSelectedPipes(input)` which checks selectedPipes +
|
|
* routeMode and sends the prompt to all connected slave targets.
|
|
* Returns true if routed (caller should skip local execution).
|
|
*/
|
|
import { feature } from 'bun:bundle'
|
|
import { useCallback } from 'react'
|
|
|
|
type StoreApi = { getState: () => any }
|
|
type SetAppState = (updater: (prev: any) => any) => void
|
|
type AddNotification = (opts: {
|
|
key: string
|
|
text: string
|
|
color: string
|
|
priority: string
|
|
timeoutMs: number
|
|
}) => void
|
|
|
|
type Deps = {
|
|
store: StoreApi
|
|
setAppState: SetAppState
|
|
addNotification: AddNotification
|
|
}
|
|
|
|
/**
|
|
* Attempt to route user input to selected pipes.
|
|
* Returns true if routed to at least one pipe (skip local execution).
|
|
*/
|
|
export function usePipeRouter({ store, setAppState, addNotification }: Deps): {
|
|
routeToSelectedPipes: (input: string) => boolean
|
|
} {
|
|
const routeToSelectedPipes = useCallback(
|
|
(input: string): boolean => {
|
|
if (!feature('UDS_INBOX')) return false
|
|
if (!input.trim() || input.trim().startsWith('/')) return false
|
|
|
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
const pipeState = store.getState().pipeIpc
|
|
const selectedPipes: string[] = pipeState?.selectedPipes ?? []
|
|
const routeMode: 'selected' | 'local' = pipeState?.routeMode ?? 'selected'
|
|
|
|
if (selectedPipes.length === 0 || routeMode === 'local') return false
|
|
|
|
const { getConnectedSlaveTargets } =
|
|
require('./useMasterMonitor.js') as typeof import('./useMasterMonitor.js')
|
|
const { getPipeIpc } =
|
|
require('../utils/pipeTransport.js') as typeof import('../utils/pipeTransport.js')
|
|
/* eslint-enable @typescript-eslint/no-require-imports */
|
|
|
|
const targets = getConnectedSlaveTargets(selectedPipes)
|
|
const pipeIpcForDisplay = getPipeIpc(store.getState())
|
|
const discovered: Array<{
|
|
pipeName: string
|
|
role: string
|
|
ip: string
|
|
hostname: string
|
|
}> = pipeIpcForDisplay.discoveredPipes ?? []
|
|
|
|
const sentTargetNames: string[] = []
|
|
const sentTargetLabels: string[] = []
|
|
const failedTargetNames: string[] = []
|
|
|
|
for (const { name, client } of targets) {
|
|
try {
|
|
client.send({ type: 'prompt', data: input.trim() })
|
|
sentTargetNames.push(name)
|
|
// Build display label: [role] hostname/ip for LAN, [role] for local
|
|
const info = discovered.find((d: any) => d.pipeName === name)
|
|
if (info) {
|
|
const isLan = info.ip && info.ip !== pipeIpcForDisplay.localIp
|
|
sentTargetLabels.push(
|
|
isLan
|
|
? `[${info.role}] ${info.hostname}/${info.ip}`
|
|
: `[${info.role}]`,
|
|
)
|
|
} else {
|
|
sentTargetLabels.push(name)
|
|
}
|
|
} catch {
|
|
failedTargetNames.push(name)
|
|
}
|
|
}
|
|
|
|
if (sentTargetNames.length > 0) {
|
|
const promptText = input.trim()
|
|
const promptTimestamp = new Date().toISOString()
|
|
setAppState((prev: any) => {
|
|
const pipeIpc = getPipeIpc(prev)
|
|
const nextSlaves = { ...pipeIpc.slaves }
|
|
for (const name of sentTargetNames) {
|
|
const slave = nextSlaves[name]
|
|
if (!slave) continue
|
|
nextSlaves[name] = {
|
|
...slave,
|
|
status: 'busy',
|
|
lastActivityAt: promptTimestamp,
|
|
lastSummary: `Queued: ${promptText}`,
|
|
lastEventType: 'prompt',
|
|
history: [
|
|
...slave.history,
|
|
{
|
|
type: 'prompt',
|
|
content: promptText,
|
|
from: pipeIpc.serverName ?? 'master',
|
|
timestamp: promptTimestamp,
|
|
},
|
|
],
|
|
}
|
|
}
|
|
return {
|
|
...prev,
|
|
pipeIpc: { ...pipeIpc, slaves: nextSlaves },
|
|
}
|
|
})
|
|
|
|
addNotification({
|
|
key: 'pipe-route-success',
|
|
text: `Routed to ${sentTargetLabels.join(', ')}; main can continue other tasks`,
|
|
color: 'success',
|
|
priority: 'immediate',
|
|
timeoutMs: 3000,
|
|
})
|
|
} else {
|
|
addNotification({
|
|
key: 'pipe-route-fallback',
|
|
text: 'Selected pipes are unavailable; processing locally.',
|
|
color: 'warning',
|
|
priority: 'immediate',
|
|
timeoutMs: 4000,
|
|
})
|
|
}
|
|
|
|
if (failedTargetNames.length > 0) {
|
|
addNotification({
|
|
key: 'pipe-route-partial-failure',
|
|
text: `Failed to send to: ${failedTargetNames.join(', ')}`,
|
|
color: 'warning',
|
|
priority: 'immediate',
|
|
timeoutMs: 4000,
|
|
})
|
|
}
|
|
|
|
return sentTargetNames.length > 0
|
|
},
|
|
[store, setAppState, addNotification],
|
|
)
|
|
|
|
return { routeToSelectedPipes }
|
|
}
|