@miphamai/cli 0.39.0 → 0.40.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/bin/mipham.ts +4 -13
- package/package.json +3 -4
- package/src/core/auto-memory.ts +23 -5
- package/src/core/engine.ts +5 -0
- package/src/core/metrics.ts +148 -72
- package/src/daemon/index.ts +2 -1
- package/src/daemon/logger.ts +84 -0
- package/src/daemon/schedule-manager.ts +2 -1
- package/src/daemon/server.ts +6 -4
- package/src/daemon/worker-pool.ts +2 -1
- package/src/index.tsx +10 -1
- package/src/providers/registry.ts +19 -1
- package/src/shared/constants.ts +472 -4
- package/src/shared/mipham-models.json +50 -0
- package/src/shared/package-info.ts +4 -11
- package/src/shared/types.ts +439 -4
- package/src/ui/commands.ts +51 -7
- package/src/ui/input.tsx +8 -1
package/src/shared/types.ts
CHANGED
|
@@ -1,8 +1,443 @@
|
|
|
1
|
-
// ──
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// ── 类型唯一真源(vendored from packages/shared/src/types.ts)──
|
|
2
|
+
// 为让 npm 包自包含(运行时无需 @mipham/shared workspace 依赖),
|
|
3
|
+
// 此处从 packages/shared/src/types.ts 同步。改动共享类型时两边同步。
|
|
4
|
+
// ── Provider Types ──
|
|
5
|
+
export type ProtocolType = 'openai-compatible' | 'anthropic' | 'custom'
|
|
6
|
+
|
|
7
|
+
export interface ModelInfo {
|
|
8
|
+
id: string
|
|
9
|
+
name: string
|
|
10
|
+
providerId: string
|
|
11
|
+
contextWindow: number
|
|
12
|
+
maxOutput: number
|
|
13
|
+
vision: boolean
|
|
14
|
+
status: 'active' | 'upcoming' | 'deprecated'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ProviderConfig {
|
|
18
|
+
id: string
|
|
19
|
+
name: string
|
|
20
|
+
protocol: ProtocolType
|
|
21
|
+
baseUrl?: string
|
|
22
|
+
apiKey: string
|
|
23
|
+
models: ModelInfo[]
|
|
24
|
+
status?: 'active' | 'upcoming'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── Message Types ──
|
|
28
|
+
export interface TextContent {
|
|
29
|
+
type: 'text'
|
|
30
|
+
text: string
|
|
31
|
+
}
|
|
32
|
+
export interface ImageContent {
|
|
33
|
+
type: 'image_url'
|
|
34
|
+
image_url: { url: string }
|
|
35
|
+
}
|
|
36
|
+
export interface ToolUseContent {
|
|
37
|
+
type: 'tool_use'
|
|
38
|
+
id: string
|
|
39
|
+
name: string
|
|
40
|
+
input: Record<string, unknown>
|
|
41
|
+
}
|
|
42
|
+
export interface ToolResultContent {
|
|
43
|
+
type: 'tool_result'
|
|
44
|
+
tool_use_id: string
|
|
45
|
+
content: string
|
|
46
|
+
}
|
|
47
|
+
export interface ThinkingContent {
|
|
48
|
+
type: 'thinking'
|
|
49
|
+
thinking: string
|
|
50
|
+
}
|
|
51
|
+
export type ContentBlock =
|
|
52
|
+
TextContent | ImageContent | ToolUseContent | ToolResultContent | ThinkingContent
|
|
53
|
+
|
|
54
|
+
export interface Message {
|
|
55
|
+
role: 'system' | 'user' | 'assistant'
|
|
56
|
+
content: string | ContentBlock[]
|
|
57
|
+
/** DeepSeek reasoning tokens — must be passed back to the API in multi-turn conversations. */
|
|
58
|
+
reasoning_content?: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Tool Types ──
|
|
62
|
+
export type ToolPermission = 'auto' | 'ask' | 'bypass'
|
|
63
|
+
export type ToolCategory =
|
|
64
|
+
'file' | 'exec' | 'agent' | 'network' | 'system' | 'artifact' | 'scheduling'
|
|
65
|
+
|
|
66
|
+
// ── Artifact Types ──
|
|
67
|
+
export interface ArtifactEntry {
|
|
68
|
+
name: string
|
|
69
|
+
path: string
|
|
70
|
+
url: string
|
|
71
|
+
size: number
|
|
72
|
+
type: 'html' | 'svg'
|
|
73
|
+
createdAt: string
|
|
74
|
+
sessionId: string
|
|
75
|
+
versions?: string[] // version tags e.g. ['v1', 'v2']
|
|
76
|
+
versionCount?: number
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ArtifactManifest {
|
|
80
|
+
version: 1
|
|
81
|
+
artifacts: ArtifactEntry[]
|
|
82
|
+
port?: number
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ToolResult {
|
|
86
|
+
success: boolean
|
|
87
|
+
content: string
|
|
88
|
+
error?: string
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Task Notification Types ──
|
|
92
|
+
export interface TaskNotification {
|
|
93
|
+
taskId: string
|
|
94
|
+
status: 'started' | 'completed' | 'failed'
|
|
95
|
+
description: string
|
|
96
|
+
content?: string
|
|
97
|
+
error?: string
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Stream Types ──
|
|
101
|
+
export interface StreamChunk {
|
|
102
|
+
type:
|
|
103
|
+
| 'text'
|
|
104
|
+
| 'tool_use'
|
|
105
|
+
| 'tool_result'
|
|
106
|
+
| 'thinking'
|
|
107
|
+
| 'stop'
|
|
108
|
+
| 'error'
|
|
109
|
+
| 'warning'
|
|
110
|
+
| 'task_notification'
|
|
111
|
+
| 'usage'
|
|
112
|
+
content?: string
|
|
113
|
+
toolUse?: ToolUseContent
|
|
114
|
+
tool_use_id?: string
|
|
115
|
+
error?: string
|
|
116
|
+
/** DeepSeek reasoning tokens accumulated during this stream. */
|
|
117
|
+
reasoning_content?: string
|
|
118
|
+
/** Anthropic thinking block content (DeepSeek Anthropic endpoint). */
|
|
119
|
+
thinking?: string
|
|
120
|
+
/** Background task notification payload (type: 'task_notification'). */
|
|
121
|
+
taskNotification?: TaskNotification
|
|
122
|
+
/** API-reported input token count (type: 'usage'). */
|
|
123
|
+
inputTokens?: number
|
|
124
|
+
/** API-reported output token count (type: 'usage'). */
|
|
125
|
+
outputTokens?: number
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Config Types ──
|
|
129
|
+
export interface MiphamConfig {
|
|
130
|
+
version: string
|
|
131
|
+
defaultProvider: string
|
|
132
|
+
defaultModel: string
|
|
133
|
+
permission: ToolPermission
|
|
134
|
+
/** Org-level permission restrictions (forbiddenModes, maxAllowedMode). */
|
|
135
|
+
permissionRestrictions?: PermissionRestrictions
|
|
136
|
+
providers: ProviderConfig[]
|
|
137
|
+
skills?: { paths: string[]; mcpServers: McpServerConfig[] }
|
|
138
|
+
marketplace?: {
|
|
139
|
+
/** If set, only allow installs from matching repos (e.g. ["One-Mipham/*"]) */
|
|
140
|
+
strictKnownMarketplaces?: string[]
|
|
141
|
+
/** Block installs from matching repos (e.g. ["malicious-org/*"]) */
|
|
142
|
+
blockedMarketplaces?: string[]
|
|
143
|
+
}
|
|
144
|
+
/** Phase 9 feature flags. All default to true. */
|
|
145
|
+
features?: Partial<FeatureFlags>
|
|
146
|
+
/** Phase 10 CRSI feature flags. All default to true. */
|
|
147
|
+
crsi?: Partial<CrsiConfig>
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface FeatureFlags {
|
|
151
|
+
mcp: { oauthEnabled: boolean }
|
|
152
|
+
context: { useRealTokenizer: boolean; adaptiveThresholds: boolean }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface CrsiConfig {
|
|
156
|
+
/** Extract and inject experience rules into agent system prompts. */
|
|
157
|
+
ruleInjection: boolean
|
|
158
|
+
/** Intercept tool calls and auto-fix known failure patterns before execution. */
|
|
159
|
+
preToolHook: boolean
|
|
160
|
+
/** Analyze agent outcomes for recurring failure patterns. */
|
|
161
|
+
autoPatternAnalysis: boolean
|
|
162
|
+
/** Auto-degrade/disable low-effectiveness rules based on success-rate tracking. */
|
|
163
|
+
autoRuleManagement: boolean
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface McpServerConfig {
|
|
167
|
+
name: string
|
|
168
|
+
/** stdio: executable to spawn (mutually exclusive with `url`). */
|
|
169
|
+
command?: string
|
|
170
|
+
/** stdio: args passed to `command`. */
|
|
171
|
+
args?: string[]
|
|
172
|
+
/** HTTP: Streamable HTTP endpoint (mutually exclusive with `command`). */
|
|
173
|
+
url?: string
|
|
174
|
+
/** HTTP: extra request headers (e.g. Authorization). */
|
|
175
|
+
headers?: Record<string, string>
|
|
176
|
+
env?: Record<string, string>
|
|
177
|
+
auth?: {
|
|
178
|
+
type: 'oauth'
|
|
179
|
+
authorizationUrl: string
|
|
180
|
+
tokenUrl: string
|
|
181
|
+
clientId: string
|
|
182
|
+
scopes?: string[]
|
|
183
|
+
redirectPort?: number
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Hook Types ──
|
|
188
|
+
export type HookEvent =
|
|
189
|
+
| 'PreToolUse'
|
|
190
|
+
| 'PostToolUse'
|
|
191
|
+
| 'PostToolUseFailure'
|
|
192
|
+
| 'SessionStart'
|
|
193
|
+
| 'SessionEnd'
|
|
194
|
+
| 'Notification'
|
|
195
|
+
| 'Stop'
|
|
196
|
+
| 'UserPromptSubmit'
|
|
197
|
+
| 'PreCompact'
|
|
198
|
+
| 'PostCompact'
|
|
199
|
+
| 'ConfigChange'
|
|
200
|
+
| 'SubagentStart'
|
|
201
|
+
| 'SubagentStop'
|
|
202
|
+
| 'PreInference'
|
|
203
|
+
|
|
204
|
+
export type HookType = 'command' | 'http' | 'code' | 'mcp_tool'
|
|
205
|
+
|
|
206
|
+
export interface HookConfig {
|
|
207
|
+
type: HookType
|
|
208
|
+
command?: string
|
|
209
|
+
args?: string[]
|
|
210
|
+
url?: string
|
|
211
|
+
method?: 'GET' | 'POST'
|
|
212
|
+
headers?: Record<string, string>
|
|
213
|
+
mcpServer?: string
|
|
214
|
+
mcpTool?: string
|
|
215
|
+
continueOnBlock?: boolean
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface HookDefinition {
|
|
219
|
+
event: HookEvent
|
|
220
|
+
toolName?: string
|
|
221
|
+
handler: (context: HookContext) => Promise<HookResult>
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface HookContext {
|
|
225
|
+
event: HookEvent
|
|
226
|
+
toolName?: string
|
|
227
|
+
toolInput?: Record<string, unknown>
|
|
228
|
+
toolResult?: ToolResult
|
|
229
|
+
sessionId: string
|
|
230
|
+
userPrompt?: string
|
|
231
|
+
configKey?: string
|
|
232
|
+
configValue?: unknown
|
|
233
|
+
/** PreInference: full conversation messages for DLP inspection. */
|
|
234
|
+
messages?: Array<{ role: string; content: string }>
|
|
235
|
+
/** PreInference: recent tool calls and their results. */
|
|
236
|
+
toolCalls?: Array<{
|
|
237
|
+
name: string
|
|
238
|
+
input: Record<string, unknown>
|
|
239
|
+
resultPreview: string
|
|
240
|
+
}>
|
|
241
|
+
/** PreInference: current provider ID. */
|
|
242
|
+
provider?: string
|
|
243
|
+
/** PreInference: current model ID. */
|
|
244
|
+
model?: string
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface HookResult {
|
|
248
|
+
allowed: boolean
|
|
249
|
+
reason?: string
|
|
250
|
+
modifiedInput?: Record<string, unknown>
|
|
251
|
+
decision?: 'allow' | 'block'
|
|
252
|
+
permissionDecision?: 'allow' | 'deny' | 'ask' | 'defer'
|
|
253
|
+
additionalContext?: string
|
|
254
|
+
updatedOutput?: string
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── Instruction Types ──
|
|
258
|
+
export interface InstructionFile {
|
|
259
|
+
path: string
|
|
260
|
+
level: 'group' | 'company' | 'project' | 'directory' | 'user'
|
|
261
|
+
privacy: 'public' | 'project' | 'private'
|
|
262
|
+
language: string
|
|
263
|
+
content: string
|
|
264
|
+
frontmatter: Record<string, unknown>
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── Permission Types ──
|
|
268
|
+
/** Six explicit permission modes matching Claude Code's permission architecture */
|
|
269
|
+
export type PermissionMode =
|
|
270
|
+
'default' | 'acceptEdits' | 'plan' | 'auto' | 'dontAsk' | 'bypassPermissions'
|
|
5
271
|
|
|
272
|
+
/** Backward-compatible alias: PermissionMode plus legacy 'ask' and 'bypass' */
|
|
273
|
+
export type PermissionLevel = PermissionMode | 'ask' | 'bypass'
|
|
274
|
+
|
|
275
|
+
/** Org-level restrictions that cap or forbid specific permission modes. */
|
|
276
|
+
export interface PermissionRestrictions {
|
|
277
|
+
/** Modes that may not be entered (cycle skips them). */
|
|
278
|
+
forbiddenModes?: PermissionMode[]
|
|
279
|
+
/** Ceiling — modes ranked higher (more permissive) than this are treated as forbidden. */
|
|
280
|
+
maxAllowedMode?: PermissionMode
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export interface PermissionConfig {
|
|
284
|
+
mode: PermissionMode
|
|
285
|
+
allow: string[]
|
|
286
|
+
deny: string[]
|
|
287
|
+
/** Optional org-level restrictions enforced on every mode transition. */
|
|
288
|
+
restrictions?: PermissionRestrictions
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export interface PermissionRuleEntry {
|
|
292
|
+
pattern: string // e.g., "Bash(git:*)"
|
|
293
|
+
level: 'allow' | 'deny' | 'ask'
|
|
294
|
+
compiled: RegExp
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export interface PermissionRule {
|
|
298
|
+
toolName: string
|
|
299
|
+
level: PermissionLevel
|
|
300
|
+
pattern?: string
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ── Inference Hook (DLP) Types ──
|
|
304
|
+
|
|
305
|
+
/** Configuration for the PreInference DLP hook, loaded from config.yml. */
|
|
306
|
+
export interface InferenceHookConfig {
|
|
307
|
+
/** DLP server endpoint (HTTPS). Empty = feature disabled. */
|
|
308
|
+
endpoint: string
|
|
309
|
+
/** HMAC signing secret (format: mis_<random>). */
|
|
310
|
+
signing_secret: string
|
|
311
|
+
/** Request timeout in milliseconds. Default 5000. */
|
|
312
|
+
timeout: number
|
|
313
|
+
/** Failure posture: 'fail-closed' blocks on error, 'fail-open' allows. */
|
|
314
|
+
on_failure: 'fail-closed' | 'fail-open'
|
|
315
|
+
/** Organization identifier (optional, sent in payload). */
|
|
316
|
+
organization_id: string
|
|
317
|
+
/** Additional custom headers to send with each request. */
|
|
318
|
+
headers: Record<string, string>
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Outgoing request to the DLP server. */
|
|
322
|
+
export interface InferenceCheckRequest {
|
|
323
|
+
type: 'inference_check'
|
|
324
|
+
id: string
|
|
325
|
+
created_at: string
|
|
326
|
+
data: {
|
|
327
|
+
type: 'pre_inference'
|
|
328
|
+
session_id: string
|
|
329
|
+
organization_id?: string
|
|
330
|
+
provider: string
|
|
331
|
+
model: string
|
|
332
|
+
messages: Array<{ role: string; content: string }>
|
|
333
|
+
tool_calls: Array<{
|
|
334
|
+
name: string
|
|
335
|
+
input: Record<string, unknown>
|
|
336
|
+
result_preview: string
|
|
337
|
+
}>
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Response from the DLP server. */
|
|
342
|
+
export interface InferenceCheckResponse {
|
|
343
|
+
verdict: 'allow' | 'deny'
|
|
344
|
+
reason?: string
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ── Credential Masking Types ──
|
|
348
|
+
|
|
349
|
+
/** Full-file masking rule: entire file content replaced with sentinel. */
|
|
350
|
+
export interface CredentialFullMaskRule {
|
|
351
|
+
path: string
|
|
352
|
+
mode: 'full'
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Per-extract-pattern configuration with optional field-based extraction. */
|
|
356
|
+
export interface CredentialExtractPattern {
|
|
357
|
+
/** Regex pattern to match. When `field` is set, applied to field value only. */
|
|
358
|
+
pattern: string
|
|
359
|
+
/** Optional replacement string. Defaults to CREDENTIAL_SENTINEL. */
|
|
360
|
+
replacement?: string
|
|
361
|
+
/** 🆕 JSON key to extract before applying pattern. If unset, pattern runs against full content. */
|
|
362
|
+
field?: string
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Extract-based masking rule: only regex-matched tokens are replaced. */
|
|
366
|
+
export interface CredentialExtractRule {
|
|
367
|
+
path: string
|
|
368
|
+
mode: 'extract'
|
|
369
|
+
extract: CredentialExtractPattern[]
|
|
370
|
+
/** 🆕 Behavior when no extract pattern matches: 'mask' (replace all) or 'passthrough' (keep). Default: 'mask'. */
|
|
371
|
+
onExtractNoMatch?: 'mask' | 'passthrough'
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** 🆕 JWT-aware masking rule: decode payload and mask specified claims. */
|
|
375
|
+
export interface JwtMaskingRule {
|
|
376
|
+
path: string
|
|
377
|
+
type: 'jwt'
|
|
378
|
+
decode: 'jwt'
|
|
379
|
+
/** Claim names to mask in the JWT payload (e.g. ["sub", "email"]). */
|
|
380
|
+
maskClaims: string[]
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** 🆕 AWS credential pair masking rule: detect key pairs and optionally re-sign. */
|
|
384
|
+
export interface AwsMaskingRule {
|
|
385
|
+
path: string
|
|
386
|
+
type: 'aws'
|
|
387
|
+
awsPairs: boolean
|
|
388
|
+
sigv4: boolean
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export type CredentialFileRule =
|
|
392
|
+
CredentialFullMaskRule | CredentialExtractRule | JwtMaskingRule | AwsMaskingRule
|
|
393
|
+
|
|
394
|
+
/** Configuration for credential masking, loaded from config.yml. */
|
|
395
|
+
export interface CredentialMaskingConfig {
|
|
396
|
+
enabled: boolean
|
|
397
|
+
files: CredentialFileRule[]
|
|
398
|
+
output_scrubbing: {
|
|
399
|
+
enabled: boolean
|
|
400
|
+
patterns: string[]
|
|
401
|
+
}
|
|
402
|
+
env_filter: {
|
|
403
|
+
enabled: boolean
|
|
404
|
+
patterns: string[]
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ── Cross-Session Messaging Types ──
|
|
409
|
+
|
|
410
|
+
/** Controls how inbound cross-session messages are handled. */
|
|
411
|
+
export type CrossSessionInbound = 'allow' | 'ask' | 'deny'
|
|
412
|
+
|
|
413
|
+
/** 🆕 Configuration for cross-session messaging. */
|
|
414
|
+
export interface CrossSessionConfig {
|
|
415
|
+
crossSessionInbound: CrossSessionInbound
|
|
416
|
+
dialogExpiry: number // seconds
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Session information exposed via ListAgents. */
|
|
420
|
+
export interface SessionInfo {
|
|
421
|
+
id: string
|
|
422
|
+
name: string
|
|
423
|
+
machine: string
|
|
424
|
+
pid: number
|
|
425
|
+
startedAt: string
|
|
426
|
+
cwd?: string
|
|
427
|
+
provider?: string
|
|
428
|
+
model?: string
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ── Background Agent Types ──
|
|
432
|
+
|
|
433
|
+
export interface BackgroundAgentConfig {
|
|
434
|
+
auto_commit: boolean
|
|
435
|
+
auto_push: boolean
|
|
436
|
+
auto_worktree: boolean
|
|
437
|
+
commit_coauthors: boolean
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ── CLI 内部类型(引用活服务,不可共享)──
|
|
6
441
|
export interface ToolContext {
|
|
7
442
|
cwd: string
|
|
8
443
|
sessionId: string
|
package/src/ui/commands.ts
CHANGED
|
@@ -668,6 +668,13 @@ const crsiRestoreCmd: CommandHandler = (ctx, args) => {
|
|
|
668
668
|
return { content: `Rule \`${ruleId}\` has been re-enabled.` }
|
|
669
669
|
}
|
|
670
670
|
|
|
671
|
+
/** Render a tiny ASCII sparkline from a series of 0-1 values (success rates). */
|
|
672
|
+
function crsiSparkline(values: number[]): string {
|
|
673
|
+
if (values.length === 0) return '—'
|
|
674
|
+
const chars = '▁▂▃▄▅▆▇█'
|
|
675
|
+
return values.map((v) => chars[Math.min(Math.max(Math.round(v * 7), 0), 7)] ?? '▁').join('')
|
|
676
|
+
}
|
|
677
|
+
|
|
671
678
|
const crsiStatsCmd: CommandHandler = async (ctx) => {
|
|
672
679
|
const engine = ctx.engine.getRuleEngine()
|
|
673
680
|
const tracker = ctx.engine.getEffectivenessTracker()
|
|
@@ -682,21 +689,43 @@ const crsiStatsCmd: CommandHandler = async (ctx) => {
|
|
|
682
689
|
lines.push(`Auto-generated: ${rules.filter((r) => r.source === 'pattern-analyzer').length}`)
|
|
683
690
|
lines.push(`Manual: ${rules.filter((r) => r.source === 'manual').length}`)
|
|
684
691
|
|
|
685
|
-
|
|
692
|
+
const effs = tracker?.allRules ?? []
|
|
693
|
+
if (effs.length > 0) {
|
|
686
694
|
let totalInterceptions = 0
|
|
687
695
|
let totalSuccesses = 0
|
|
688
|
-
for (const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
totalInterceptions += eff.appliedCount
|
|
692
|
-
totalSuccesses += eff.successAfterCount
|
|
693
|
-
}
|
|
696
|
+
for (const eff of effs) {
|
|
697
|
+
totalInterceptions += eff.appliedCount
|
|
698
|
+
totalSuccesses += eff.successAfterCount
|
|
694
699
|
}
|
|
695
700
|
lines.push('')
|
|
696
701
|
lines.push(`Total interceptions: ${totalInterceptions}`)
|
|
697
702
|
lines.push(
|
|
698
703
|
`Success rate after rules: ${totalInterceptions > 0 ? Math.round((totalSuccesses / totalInterceptions) * 100) : 0}%`,
|
|
699
704
|
)
|
|
705
|
+
|
|
706
|
+
lines.push('')
|
|
707
|
+
lines.push('### Rule effectiveness')
|
|
708
|
+
lines.push('| Rule | Status | Applied | Success | Trend |')
|
|
709
|
+
lines.push('|------|--------|---------|---------|-------|')
|
|
710
|
+
const statusIcon: Record<string, string> = { active: '🟢', degrading: '🟡', disabled: '⚫' }
|
|
711
|
+
for (const eff of effs) {
|
|
712
|
+
const icon = statusIcon[eff.status] ?? '⚪'
|
|
713
|
+
const rate =
|
|
714
|
+
eff.appliedCount > 0 ? Math.round((eff.successAfterCount / eff.appliedCount) * 100) : 0
|
|
715
|
+
const trend = crsiSparkline(eff.evaluationHistory.map((h) => 1 - h.failureRate))
|
|
716
|
+
lines.push(
|
|
717
|
+
`| ${eff.ruleId} | ${icon} ${eff.status} | ${eff.appliedCount} | ${rate}% | ${trend} |`,
|
|
718
|
+
)
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const disabled = effs.filter((e) => e.status === 'disabled')
|
|
722
|
+
if (disabled.length > 0) {
|
|
723
|
+
lines.push('')
|
|
724
|
+
lines.push('### Disabled rules (retired)')
|
|
725
|
+
for (const d of disabled) {
|
|
726
|
+
lines.push(`- ${d.ruleId} (failure rate ${Math.round(d.postRuleFailureRate * 100)}%)`)
|
|
727
|
+
}
|
|
728
|
+
}
|
|
700
729
|
}
|
|
701
730
|
|
|
702
731
|
return { content: lines.join('\n') }
|
|
@@ -907,6 +936,21 @@ const sisStatsCmd: CommandHandler = (ctx) => {
|
|
|
907
936
|
)
|
|
908
937
|
lines.push(`平均成功率: ${Math.round(stats.avgSuccessRate * 100)}%`)
|
|
909
938
|
lines.push(`总拦截次数: ${stats.totalInterceptions}`)
|
|
939
|
+
|
|
940
|
+
const active = db.getActive()
|
|
941
|
+
if (active.length > 0) {
|
|
942
|
+
lines.push('')
|
|
943
|
+
lines.push('### 签名明细(按拦截次数排序)')
|
|
944
|
+
lines.push('| 签名 | 状态 | 拦截 | 成功率 |')
|
|
945
|
+
lines.push('|------|------|------|--------|')
|
|
946
|
+
for (const s of active) {
|
|
947
|
+
const icon = s.status === 'active' ? '🟢' : '🟡'
|
|
948
|
+
lines.push(
|
|
949
|
+
`| ${s.id} | ${icon} ${s.status} | ${s.occurrences} | ${Math.round(s.successRate * 100)}% |`,
|
|
950
|
+
)
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
910
954
|
return { content: lines.join('\n') }
|
|
911
955
|
}
|
|
912
956
|
|
package/src/ui/input.tsx
CHANGED
|
@@ -231,7 +231,14 @@ export function InputBar({
|
|
|
231
231
|
if (submittedHistory.length === 0) return
|
|
232
232
|
// Save current draft the first time we enter history browsing
|
|
233
233
|
if (historyIndexRef.current === -1) {
|
|
234
|
-
|
|
234
|
+
// Flush any pending throttle so its deferred setValue doesn't overwrite
|
|
235
|
+
// history navigation (paste → immediate ↑ race). Save the latest value
|
|
236
|
+
// from the ref — state `value` is stale inside the throttle window.
|
|
237
|
+
if (onChangeTimerRef.current) {
|
|
238
|
+
clearTimeout(onChangeTimerRef.current)
|
|
239
|
+
onChangeTimerRef.current = null
|
|
240
|
+
}
|
|
241
|
+
savedDraftRef.current = valueRef.current
|
|
235
242
|
}
|
|
236
243
|
const newIndex = Math.min(historyIndexRef.current + 1, submittedHistory.length - 1)
|
|
237
244
|
historyIndexRef.current = newIndex
|