fix(types): Desktop type safety hardening (TYPE-01)
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

- Unify ConnectionState: kernel-types.ts now canonical source
  with 'handshaking', gateway-types.ts re-exports
- PromptTemplateInfo source/status → union literals
- PromptVariable.type → union literal
- CreateRoleRequest id/permissions → optional
- PropertyPanel: replace 13 as any with typed accessor pattern
- chatStore: window cast via as unknown as Record
This commit is contained in:
iven
2026-04-05 01:30:29 +08:00
parent 36168d6978
commit 3b0ab1a7b7
6 changed files with 32 additions and 26 deletions

View File

@@ -87,8 +87,13 @@ function renderTypeSpecificFields(
data: Partial<WorkflowNodeData>, data: Partial<WorkflowNodeData>,
onChange: (field: string, value: unknown) => void onChange: (field: string, value: unknown) => void
) { ) {
// Type-safe property accessor for union-typed node data // Type-safe property accessors for union-typed node data
const d = data as Record<string, unknown>; const d = data as Record<string, unknown>;
const str = (key: string): string => (d[key] as string) || '';
const num = (key: string): number | string => (d[key] as number) ?? '';
const bool = (key: string): boolean => (d[key] as boolean) || false;
const arr = (key: string): string[] => (d[key] as string[]) || [];
const obj = (key: string): Record<string, unknown> => (d[key] as Record<string, unknown>) || {};
switch (type) { switch (type) {
case 'input': case 'input':
return ( return (
@@ -99,7 +104,7 @@ function renderTypeSpecificFields(
</label> </label>
<input <input
type="text" type="text"
value={d.variableName || ''} value={str('variableName')}
onChange={(e) => onChange('variableName', e.target.value)} onChange={(e) => onChange('variableName', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg font-mono" className="w-full px-3 py-2 border border-gray-300 rounded-lg font-mono"
/> />
@@ -109,7 +114,7 @@ function renderTypeSpecificFields(
Default Value Default Value
</label> </label>
<textarea <textarea
value={d.defaultValue || ''} value={str('defaultValue')}
onChange={(e) => { onChange={(e) => {
try { try {
const parsed = JSON.parse(e.target.value); const parsed = JSON.parse(e.target.value);
@@ -134,7 +139,7 @@ function renderTypeSpecificFields(
Template Template
</label> </label>
<textarea <textarea
value={d.template || ''} value={str('template')}
onChange={(e) => onChange('template', e.target.value)} onChange={(e) => onChange('template', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg font-mono text-sm" className="w-full px-3 py-2 border border-gray-300 rounded-lg font-mono text-sm"
rows={6} rows={6}
@@ -146,7 +151,7 @@ function renderTypeSpecificFields(
</label> </label>
<input <input
type="text" type="text"
value={d.model || ''} value={str('model')}
onChange={(e) => onChange('model', e.target.value)} onChange={(e) => onChange('model', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg" className="w-full px-3 py-2 border border-gray-300 rounded-lg"
placeholder="e.g., gpt-4" placeholder="e.g., gpt-4"
@@ -161,7 +166,7 @@ function renderTypeSpecificFields(
min="0" min="0"
max="2" max="2"
step="0.1" step="0.1"
value={d.temperature ?? ''} value={num('temperature')}
onChange={(e) => onChange('temperature', parseFloat(e.target.value))} onChange={(e) => onChange('temperature', parseFloat(e.target.value))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg" className="w-full px-3 py-2 border border-gray-300 rounded-lg"
/> />
@@ -169,7 +174,7 @@ function renderTypeSpecificFields(
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
type="checkbox" type="checkbox"
checked={d.jsonMode || false} checked={bool('jsonMode')}
onChange={(e) => onChange('jsonMode', e.target.checked)} onChange={(e) => onChange('jsonMode', e.target.checked)}
className="w-4 h-4 text-blue-600 rounded" className="w-4 h-4 text-blue-600 rounded"
/> />
@@ -187,7 +192,7 @@ function renderTypeSpecificFields(
</label> </label>
<input <input
type="text" type="text"
value={d.skillId || ''} value={str('skillId')}
onChange={(e) => onChange('skillId', e.target.value)} onChange={(e) => onChange('skillId', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg font-mono" className="w-full px-3 py-2 border border-gray-300 rounded-lg font-mono"
/> />
@@ -197,7 +202,7 @@ function renderTypeSpecificFields(
Input Mappings (JSON) Input Mappings (JSON)
</label> </label>
<textarea <textarea
value={JSON.stringify(d.inputMappings || {}, null, 2)} value={JSON.stringify(obj('inputMappings'), null, 2)}
onChange={(e) => { onChange={(e) => {
try { try {
const parsed = JSON.parse(e.target.value); const parsed = JSON.parse(e.target.value);
@@ -222,7 +227,7 @@ function renderTypeSpecificFields(
</label> </label>
<input <input
type="text" type="text"
value={d.handId || ''} value={str('handId')}
onChange={(e) => onChange('handId', e.target.value)} onChange={(e) => onChange('handId', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg font-mono" className="w-full px-3 py-2 border border-gray-300 rounded-lg font-mono"
/> />
@@ -233,7 +238,7 @@ function renderTypeSpecificFields(
</label> </label>
<input <input
type="text" type="text"
value={d.action || ''} value={str('action')}
onChange={(e) => onChange('action', e.target.value)} onChange={(e) => onChange('action', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg" className="w-full px-3 py-2 border border-gray-300 rounded-lg"
/> />
@@ -253,13 +258,13 @@ function renderTypeSpecificFields(
<label key={format} className="flex items-center gap-2"> <label key={format} className="flex items-center gap-2">
<input <input
type="checkbox" type="checkbox"
checked={(d.formats || []).includes(format)} checked={arr('formats').includes(format)}
onChange={(e) => { onChange={(e) => {
const formats = d.formats || []; const formats = arr('formats');
if (e.target.checked) { if (e.target.checked) {
onChange('formats', [...formats, format]); onChange('formats', [...formats, format]);
} else { } else {
onChange('formats', formats.filter((f: string) => f !== format)); onChange('formats', formats.filter((f) => f !== format));
} }
}} }}
className="w-4 h-4 text-blue-600 rounded" className="w-4 h-4 text-blue-600 rounded"
@@ -275,7 +280,7 @@ function renderTypeSpecificFields(
</label> </label>
<input <input
type="text" type="text"
value={d.outputDir || ''} value={str('outputDir')}
onChange={(e) => onChange('outputDir', e.target.value)} onChange={(e) => onChange('outputDir', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg" className="w-full px-3 py-2 border border-gray-300 rounded-lg"
placeholder="./output" placeholder="./output"

View File

@@ -90,7 +90,7 @@ export interface ZclawStreamEvent {
} }
// === Connection State === // === Connection State ===
// Re-export from kernel-types to maintain single source of truth
export type ConnectionState = 'disconnected' | 'connecting' | 'handshaking' | 'connected' | 'reconnecting'; export type { ConnectionState } from './kernel-types';
export type EventCallback = (payload: unknown) => void; export type EventCallback = (payload: unknown) => void;

View File

@@ -7,7 +7,7 @@
// === Connection & Status Types === // === Connection & Status Types ===
export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting'; export type ConnectionState = 'disconnected' | 'connecting' | 'handshaking' | 'connected' | 'reconnecting';
export interface KernelStatus { export interface KernelStatus {
initialized: boolean; initialized: boolean;

View File

@@ -188,9 +188,9 @@ export interface PromptTemplateInfo {
name: string; name: string;
category: string; category: string;
description: string | null; description: string | null;
source: string; source: 'builtin' | 'custom';
current_version: number; current_version: number;
status: string; status: 'active' | 'deprecated' | 'archived';
created_at: string; created_at: string;
updated_at: string; updated_at: string;
} }
@@ -211,7 +211,7 @@ export interface PromptVersionInfo {
/** Prompt variable definition */ /** Prompt variable definition */
export interface PromptVariable { export interface PromptVariable {
name: string; name: string;
type: string; type: 'string' | 'number' | 'select' | 'boolean';
default_value?: string; default_value?: string;
description?: string; description?: string;
required?: boolean; required?: boolean;
@@ -432,10 +432,10 @@ export interface RoleInfo {
/** Create role request */ /** Create role request */
export interface CreateRoleRequest { export interface CreateRoleRequest {
id: string; id?: string;
name: string; name: string;
description?: string; description?: string;
permissions: string[]; permissions?: string[];
} }
/** Update role request */ /** Update role request */

View File

@@ -354,7 +354,7 @@ if (import.meta.hot) {
// Dev-only: Expose stores to window for E2E testing // Dev-only: Expose stores to window for E2E testing
if (import.meta.env.DEV && typeof window !== 'undefined') { if (import.meta.env.DEV && typeof window !== 'undefined') {
const w = window as Record<string, unknown>; const w = window as unknown as Record<string, unknown>;
w.__ZCLAW_STORES__ = (w.__ZCLAW_STORES__ as Record<string, unknown>) || {}; w.__ZCLAW_STORES__ = (w.__ZCLAW_STORES__ as Record<string, unknown>) || {};
const stores = w.__ZCLAW_STORES__ as Record<string, unknown>; const stores = w.__ZCLAW_STORES__ as Record<string, unknown>;
stores.chat = useChatStore; stores.chat = useChatStore;

View File

@@ -32,7 +32,7 @@
| DEAD-05 | 39 个未调用 saas-client 方法 | **PARTIALLY_FIXED** | - | 2026-04-04 | 10 个零调用方法已删除 (healthCheck/listDevices/getRelayTask/getUsage×2/listPrompts/getPrompt/listPromptVersions/getPromptVersion/getPlan)29 个保留供 admin-v2 或未来接入 | | DEAD-05 | 39 个未调用 saas-client 方法 | **PARTIALLY_FIXED** | - | 2026-04-04 | 10 个零调用方法已删除 (healthCheck/listDevices/getRelayTask/getUsage×2/listPrompts/getPrompt/listPromptVersions/getPromptVersion/getPlan)29 个保留供 admin-v2 或未来接入 |
| DOC-01 | Tauri 命令数文档 58+ vs 实际 171 | **FIXED** | 06-tauri-backend 文档已更新 175→171 | | DOC-01 | Tauri 命令数文档 58+ vs 实际 171 | **FIXED** | 06-tauri-backend 文档已更新 175→171 |
| DOC-02 | 智能层文档引用已删除模块 | **FALSE_POSITIVE** | 02-intelligence-layer 文档未引用已删除模块, | DOC-02 | 智能层文档引用已删除模块 | **FALSE_POSITIVE** | 02-intelligence-layer 文档未引用已删除模块,
| TYPE-01 | Desktop/Admin 类型不一致 (6 组) | OPEN | - | - | 统一类型定义 | | TYPE-01 | Desktop/Admin 类型不一致 (6 组) | **FIXED** | - | 2026-04-05 | ConnectionState 统一含 handshaking; PromptTemplate source/status + PromptVariable.type 改为 union literal; CreateRoleRequest id/permissions 改可选; PropertyPanel as any 消除; chatStore window cast 修正 |
| G-07 | account_api_keys 被 relay 绕过 | **N/A** | Intentional architecture: provider_keys (Key Pool) 做 upstream key rotation/429/failover; account_api_keys 为 account-level token | | G-07 | account_api_keys 被 relay 绕过 | **N/A** | Intentional architecture: provider_keys (Key Pool) 做 upstream key rotation/429/failover; account_api_keys 为 account-level token |
## P3: 中优先级 ## P3: 中优先级
@@ -135,7 +135,7 @@
| ID | 问题 | 状态 | 验证方法 | | ID | 问题 | 状态 | 验证方法 |
|----|------|------|----------| |----|------|------|----------|
| V11-P4-01 | ContentBlock 4 处定义(不同域) | **FALSE_POSITIVE** | 4 个 crate 各自服务于不同协议域(消息/MCP/Driver/Hand按领域隔离是正确设计 | | V11-P4-01 | ContentBlock 4 处定义(不同域) | **FALSE_POSITIVE** | 4 个 crate 各自服务于不同协议域(消息/MCP/Driver/Hand按领域隔离是正确设计 |
| V11-P4-02 | Desktop ↔ Admin 13+ 类型名称不一致 | **DOCUMENTED** | TYPE-01 已在 V12 Batch 7 部分修复AgentInfo/KernelStatus),剩余为 admin-v2 独立类型系统 | | V11-P4-02 | Desktop ↔ Admin 13+ 类型名称不一致 | **DOCUMENTED** | TYPE-01 完整修复ConnectionState union + PromptTemplate literal + CreateRoleRequest optional),剩余为 admin-v2 独立类型系统有意差异 |
| V11-P4-03 | 文档数字不一致 (Skills 76 vs 66/75/77) | **FALSE_POSITIVE** | find skills -name SKILL.md = 75文档统一为 75 | | V11-P4-03 | 文档数字不一致 (Skills 76 vs 66/75/77) | **FALSE_POSITIVE** | find skills -name SKILL.md = 75文档统一为 75 |
| V11-P4-04 | A2A/WASM feature-gated 未启用 | **FALSE_POSITIVE** | a2a/wasm 在 Cargo.toml 正确 feature-gated默认不启用有意设计 | | V11-P4-04 | A2A/WASM feature-gated 未启用 | **FALSE_POSITIVE** | a2a/wasm 在 Cargo.toml 正确 feature-gated默认不启用有意设计 |
| V11-P4-05 | embedding 生成已禁用 | **FALSE_POSITIVE** | generate_embedding.rs 分块功能已实现,向量生成是 Phase 2 有意延迟,注释已完善 | | V11-P4-05 | embedding 生成已禁用 | **FALSE_POSITIVE** | generate_embedding.rs 分块功能已实现,向量生成是 Phase 2 有意延迟,注释已完善 |
@@ -272,6 +272,7 @@
| 2026-04-05 | SEC2-P3-01 | OPEN → DOCUMENTED | A2A feature-gated 默认不启用 | | 2026-04-05 | SEC2-P3-01 | OPEN → DOCUMENTED | A2A feature-gated 默认不启用 |
| 2026-04-05 | SEC2-P3-02 | OPEN → DOCUMENTED | P4 级差异admin 独立类型系统 | | 2026-04-05 | SEC2-P3-02 | OPEN → DOCUMENTED | P4 级差异admin 独立类型系统 |
| 2026-04-05 | V11-P2-05 | 部分关闭 → FIXED | 完整审计: 160 @connected + 16 @reserved + 1 未注册 = 177 总命令 | | 2026-04-05 | V11-P2-05 | 部分关闭 → FIXED | 完整审计: 160 @connected + 16 @reserved + 1 未注册 = 177 总命令 |
| 2026-04-05 | TYPE-01 | OPEN → FIXED | ConnectionState 统一(含 handshaking); saas-types PromptTemplate/PromptVariable union literal; CreateRoleRequest optional; PropertyPanel as any→类型安全; chatStore window cast |
## V12 模块化端到端审计修复 (2026-04-04) ## V12 模块化端到端审计修复 (2026-04-04)