thatgfsj-code 2.2.9 → 3.0.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/CHANGELOG.md +15 -67
- package/dist/app/index.d.ts +28 -5
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js +69 -10
- package/dist/app/index.js.map +1 -1
- package/dist/cache/fingerprint.d.ts +49 -0
- package/dist/cache/fingerprint.d.ts.map +1 -0
- package/dist/cache/fingerprint.js +100 -0
- package/dist/cache/fingerprint.js.map +1 -0
- package/dist/cache/index.d.ts +15 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +15 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/smartModel.d.ts +40 -0
- package/dist/cache/smartModel.d.ts.map +1 -0
- package/dist/cache/smartModel.js +43 -0
- package/dist/cache/smartModel.js.map +1 -0
- package/dist/cache/stats.d.ts +93 -0
- package/dist/cache/stats.d.ts.map +1 -0
- package/dist/cache/stats.js +155 -0
- package/dist/cache/stats.js.map +1 -0
- package/dist/cache/volatile.d.ts +32 -0
- package/dist/cache/volatile.d.ts.map +1 -0
- package/dist/cache/volatile.js +44 -0
- package/dist/cache/volatile.js.map +1 -0
- package/dist/cmd/index.js +49 -57
- package/dist/cmd/index.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +8 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/types.d.ts +14 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/llm/anthropic.d.ts +55 -5
- package/dist/llm/anthropic.d.ts.map +1 -1
- package/dist/llm/anthropic.js +172 -26
- package/dist/llm/anthropic.js.map +1 -1
- package/dist/llm/index.d.ts +26 -8
- package/dist/llm/index.d.ts.map +1 -1
- package/dist/llm/index.js +83 -15
- package/dist/llm/index.js.map +1 -1
- package/dist/llm/openai.d.ts +28 -8
- package/dist/llm/openai.d.ts.map +1 -1
- package/dist/llm/openai.js +58 -8
- package/dist/llm/openai.js.map +1 -1
- package/dist/llm/provider.d.ts +40 -8
- package/dist/llm/provider.d.ts.map +1 -1
- package/dist/llm/provider.js +8 -1
- package/dist/llm/provider.js.map +1 -1
- package/dist/prompts/index.d.ts +32 -0
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +33 -11
- package/dist/prompts/index.js.map +1 -1
- package/dist/session/index.d.ts +32 -3
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/index.js +51 -36
- package/dist/session/index.js.map +1 -1
- package/dist/skills/brainstorming.js +33 -33
- package/dist/skills/code-review.js +35 -35
- package/dist/skills/executing-plans.js +18 -18
- package/dist/skills/frontend-design.js +25 -25
- package/dist/skills/git-workflow.js +26 -26
- package/dist/skills/improve-architecture.js +28 -28
- package/dist/skills/neuroweave.js +37 -37
- package/dist/skills/playwright.js +62 -62
- package/dist/skills/prototype.js +20 -20
- package/dist/skills/subagent.js +19 -19
- package/dist/skills/supabase.js +34 -34
- package/dist/skills/systematic-debugging.js +34 -34
- package/dist/skills/tdd.js +29 -29
- package/dist/skills/triage.js +25 -25
- package/dist/skills/verification.js +22 -22
- package/dist/skills/writing-plans.js +32 -32
- package/dist/tools/nwt.js +16 -16
- package/dist/tui/app.d.ts.map +1 -1
- package/dist/tui/app.js +9 -2
- package/dist/tui/app.js.map +1 -1
- package/dist/tui/components/Header.d.ts +8 -0
- package/dist/tui/components/Header.d.ts.map +1 -1
- package/dist/tui/components/Header.js +17 -4
- package/dist/tui/components/Header.js.map +1 -1
- package/dist/tui/components/InitWizard.d.ts +12 -1
- package/dist/tui/components/InitWizard.d.ts.map +1 -1
- package/dist/tui/components/InitWizard.js +58 -3
- package/dist/tui/components/InitWizard.js.map +1 -1
- package/dist/tui/components/ToolCall.d.ts.map +1 -1
- package/dist/tui/components/ToolCall.js +1 -6
- package/dist/tui/components/ToolCall.js.map +1 -1
- package/dist/tui/hooks/useChat.d.ts +22 -0
- package/dist/tui/hooks/useChat.d.ts.map +1 -1
- package/dist/tui/hooks/useChat.js +82 -46
- package/dist/tui/hooks/useChat.js.map +1 -1
- package/dist/tui/hooks/useCommands.d.ts.map +1 -1
- package/dist/tui/hooks/useCommands.js +57 -0
- package/dist/tui/hooks/useCommands.js.map +1 -1
- package/dist/tui/welcome.d.ts.map +1 -1
- package/dist/tui/welcome.js +2 -3
- package/dist/tui/welcome.js.map +1 -1
- package/dist/types.d.ts +79 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -1
- package/dist/utils/stableStringify.d.ts +21 -0
- package/dist/utils/stableStringify.d.ts.map +1 -0
- package/dist/utils/stableStringify.js +53 -0
- package/dist/utils/stableStringify.js.map +1 -0
- package/dist/utils/thinking.d.ts.map +1 -1
- package/dist/utils/thinking.js +0 -3
- package/dist/utils/thinking.js.map +1 -1
- package/package.json +6 -6
- package/src/app/index.ts +180 -124
- package/src/cache/fingerprint.ts +101 -0
- package/src/cache/index.ts +15 -0
- package/src/cache/smartModel.ts +52 -0
- package/src/cache/stats.ts +199 -0
- package/src/cache/volatile.ts +47 -0
- package/src/cmd/index.tsx +288 -292
- package/src/config/index.ts +156 -148
- package/src/config/providers.ts +234 -234
- package/src/config/types.ts +67 -53
- package/src/hooks/index.ts +111 -111
- package/src/llm/anthropic.ts +388 -243
- package/src/llm/gemini.ts +169 -169
- package/src/llm/index.ts +265 -200
- package/src/llm/openai.ts +243 -196
- package/src/llm/provider.ts +66 -34
- package/src/prompts/index.ts +260 -220
- package/src/session/compactor.ts +103 -103
- package/src/session/index.ts +181 -168
- package/src/session/message.ts +42 -42
- package/src/skills/brainstorming.ts +43 -43
- package/src/skills/code-review.ts +45 -45
- package/src/skills/executing-plans.ts +27 -27
- package/src/skills/frontend-design.ts +35 -35
- package/src/skills/git-workflow.ts +36 -36
- package/src/skills/improve-architecture.ts +38 -38
- package/src/skills/index.ts +136 -136
- package/src/skills/neuroweave.ts +47 -47
- package/src/skills/playwright.ts +72 -72
- package/src/skills/prototype.ts +30 -30
- package/src/skills/subagent.ts +28 -28
- package/src/skills/supabase.ts +44 -44
- package/src/skills/systematic-debugging.ts +44 -44
- package/src/skills/tdd.ts +39 -39
- package/src/skills/triage.ts +35 -35
- package/src/skills/verification.ts +31 -31
- package/src/skills/writing-plans.ts +42 -42
- package/src/tools/nwt.ts +598 -598
- package/src/tools/types.ts +122 -122
- package/src/tui/app.tsx +199 -186
- package/src/tui/components/ChatList.tsx +41 -41
- package/src/tui/components/ChatMessage.tsx +54 -54
- package/src/tui/components/Header.tsx +56 -29
- package/src/tui/components/InitWizard.tsx +217 -132
- package/src/tui/components/Markdown.tsx +35 -35
- package/src/tui/components/ModelSelector.tsx +87 -87
- package/src/tui/components/StatusBar.tsx +30 -30
- package/src/tui/components/Thinking.tsx +17 -17
- package/src/tui/components/ToolCall.tsx +102 -107
- package/src/tui/components/UserInput.tsx +131 -131
- package/src/tui/hooks/useChat.ts +287 -238
- package/src/tui/hooks/useCommands.ts +234 -176
- package/src/tui/index.ts +6 -6
- package/src/tui/welcome.ts +177 -178
- package/src/types.ts +104 -42
- package/src/utils/diff.ts +71 -71
- package/src/utils/project.ts +99 -99
- package/src/utils/stableStringify.ts +47 -0
- package/src/utils/thinking.ts +118 -121
- package/tests/cache/fingerprint.test.ts +75 -0
- package/tests/cache/stableStringify.test.ts +43 -0
- package/tests/cache/stats.test.ts +146 -0
- package/tests/cache/volatile.test.ts +75 -0
- package/tests/smoke-pollution.mjs +78 -0
- package/tests/smoke-thinking.mjs +110 -0
- package/tests/smoke-tool-stream.mjs +69 -0
- package/tests/smoke-tool.mjs +117 -0
- package/.nwt/meta.json +0 -5
- package/dist/version.d.ts +0 -16
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js +0 -16
- package/dist/version.js.map +0 -1
- package/src/version.ts +0 -16
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
/** @jsxImportSource react */
|
|
2
|
-
import React, { memo } from 'react';
|
|
3
|
-
import { Box, Static } from 'ink';
|
|
4
|
-
import { ChatMessage, type MessageData } from './ChatMessage.js';
|
|
5
|
-
import type { ToolCallData } from './ToolCall.js';
|
|
6
|
-
|
|
7
|
-
interface Props {
|
|
8
|
-
messages: MessageData[];
|
|
9
|
-
streaming?: string;
|
|
10
|
-
streamingToolCalls?: ToolCallData[];
|
|
11
|
-
width?: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const ChatList = memo(function ChatList({ messages, streaming, streamingToolCalls, width }: Props) {
|
|
15
|
-
const hasStreaming = !!(streaming || (streamingToolCalls && streamingToolCalls.length > 0));
|
|
16
|
-
|
|
17
|
-
return (
|
|
18
|
-
<Box flexDirection="column">
|
|
19
|
-
{/* Completed messages - Static prevents re-rendering */}
|
|
20
|
-
{messages.length > 0 && (
|
|
21
|
-
<Static items={messages}>
|
|
22
|
-
{(msg: MessageData, index: number) => (
|
|
23
|
-
<ChatMessage key={`msg-${index}`} message={msg} width={width} />
|
|
24
|
-
)}
|
|
25
|
-
</Static>
|
|
26
|
-
)}
|
|
27
|
-
|
|
28
|
-
{/* Streaming content - only this part re-renders */}
|
|
29
|
-
{hasStreaming && (
|
|
30
|
-
<ChatMessage
|
|
31
|
-
message={{
|
|
32
|
-
role: 'assistant',
|
|
33
|
-
content: streaming || '',
|
|
34
|
-
toolCalls: streamingToolCalls && streamingToolCalls.length > 0 ? streamingToolCalls : undefined,
|
|
35
|
-
}}
|
|
36
|
-
width={width}
|
|
37
|
-
/>
|
|
38
|
-
)}
|
|
39
|
-
</Box>
|
|
40
|
-
);
|
|
41
|
-
});
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
import React, { memo } from 'react';
|
|
3
|
+
import { Box, Static } from 'ink';
|
|
4
|
+
import { ChatMessage, type MessageData } from './ChatMessage.js';
|
|
5
|
+
import type { ToolCallData } from './ToolCall.js';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
messages: MessageData[];
|
|
9
|
+
streaming?: string;
|
|
10
|
+
streamingToolCalls?: ToolCallData[];
|
|
11
|
+
width?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const ChatList = memo(function ChatList({ messages, streaming, streamingToolCalls, width }: Props) {
|
|
15
|
+
const hasStreaming = !!(streaming || (streamingToolCalls && streamingToolCalls.length > 0));
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Box flexDirection="column">
|
|
19
|
+
{/* Completed messages - Static prevents re-rendering */}
|
|
20
|
+
{messages.length > 0 && (
|
|
21
|
+
<Static items={messages}>
|
|
22
|
+
{(msg: MessageData, index: number) => (
|
|
23
|
+
<ChatMessage key={`msg-${index}`} message={msg} width={width} />
|
|
24
|
+
)}
|
|
25
|
+
</Static>
|
|
26
|
+
)}
|
|
27
|
+
|
|
28
|
+
{/* Streaming content - only this part re-renders */}
|
|
29
|
+
{hasStreaming && (
|
|
30
|
+
<ChatMessage
|
|
31
|
+
message={{
|
|
32
|
+
role: 'assistant',
|
|
33
|
+
content: streaming || '',
|
|
34
|
+
toolCalls: streamingToolCalls && streamingToolCalls.length > 0 ? streamingToolCalls : undefined,
|
|
35
|
+
}}
|
|
36
|
+
width={width}
|
|
37
|
+
/>
|
|
38
|
+
)}
|
|
39
|
+
</Box>
|
|
40
|
+
);
|
|
41
|
+
});
|
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
/** @jsxImportSource react */
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { Box, Text } from 'ink';
|
|
4
|
-
import { Markdown } from './Markdown.js';
|
|
5
|
-
import { ToolCall, type ToolCallData } from './ToolCall.js';
|
|
6
|
-
|
|
7
|
-
interface MessageData {
|
|
8
|
-
role: 'user' | 'assistant' | 'tool';
|
|
9
|
-
content: string;
|
|
10
|
-
toolCalls?: ToolCallData[];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface Props {
|
|
14
|
-
message: MessageData;
|
|
15
|
-
width?: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function UserMessage({ content }: { content: string }) {
|
|
19
|
-
return (
|
|
20
|
-
<Box flexDirection="column" marginBottom={1} paddingLeft={1}>
|
|
21
|
-
<Text bold color="#06B6D4">You</Text>
|
|
22
|
-
<Box paddingLeft={2}>
|
|
23
|
-
<Text>{content}</Text>
|
|
24
|
-
</Box>
|
|
25
|
-
</Box>
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function AssistantMessage({ content, toolCalls }: { content: string; toolCalls?: ToolCallData[] }) {
|
|
30
|
-
return (
|
|
31
|
-
<Box flexDirection="column" marginBottom={1}>
|
|
32
|
-
{toolCalls && toolCalls.map((tc, i) => (
|
|
33
|
-
<ToolCall key={i} tool={tc} />
|
|
34
|
-
))}
|
|
35
|
-
{content && (
|
|
36
|
-
<Box flexDirection="column" paddingLeft={1}>
|
|
37
|
-
<Text bold color="#22D3EE">AI</Text>
|
|
38
|
-
<Box paddingLeft={2}>
|
|
39
|
-
<Markdown content={content} />
|
|
40
|
-
</Box>
|
|
41
|
-
</Box>
|
|
42
|
-
)}
|
|
43
|
-
</Box>
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export const ChatMessage = React.memo(function ChatMessage({ message }: Props) {
|
|
48
|
-
if (message.role === 'user') {
|
|
49
|
-
return <UserMessage content={message.content} />;
|
|
50
|
-
}
|
|
51
|
-
return <AssistantMessage content={message.content} toolCalls={message.toolCalls} />;
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
export type { MessageData };
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { Markdown } from './Markdown.js';
|
|
5
|
+
import { ToolCall, type ToolCallData } from './ToolCall.js';
|
|
6
|
+
|
|
7
|
+
interface MessageData {
|
|
8
|
+
role: 'user' | 'assistant' | 'tool';
|
|
9
|
+
content: string;
|
|
10
|
+
toolCalls?: ToolCallData[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
message: MessageData;
|
|
15
|
+
width?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function UserMessage({ content }: { content: string }) {
|
|
19
|
+
return (
|
|
20
|
+
<Box flexDirection="column" marginBottom={1} paddingLeft={1}>
|
|
21
|
+
<Text bold color="#06B6D4">You</Text>
|
|
22
|
+
<Box paddingLeft={2}>
|
|
23
|
+
<Text>{content}</Text>
|
|
24
|
+
</Box>
|
|
25
|
+
</Box>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function AssistantMessage({ content, toolCalls }: { content: string; toolCalls?: ToolCallData[] }) {
|
|
30
|
+
return (
|
|
31
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
32
|
+
{toolCalls && toolCalls.map((tc, i) => (
|
|
33
|
+
<ToolCall key={i} tool={tc} />
|
|
34
|
+
))}
|
|
35
|
+
{content && (
|
|
36
|
+
<Box flexDirection="column" paddingLeft={1}>
|
|
37
|
+
<Text bold color="#22D3EE">AI</Text>
|
|
38
|
+
<Box paddingLeft={2}>
|
|
39
|
+
<Markdown content={content} />
|
|
40
|
+
</Box>
|
|
41
|
+
</Box>
|
|
42
|
+
)}
|
|
43
|
+
</Box>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const ChatMessage = React.memo(function ChatMessage({ message }: Props) {
|
|
48
|
+
if (message.role === 'user') {
|
|
49
|
+
return <UserMessage content={message.content} />;
|
|
50
|
+
}
|
|
51
|
+
return <AssistantMessage content={message.content} toolCalls={message.toolCalls} />;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export type { MessageData };
|
|
@@ -1,29 +1,56 @@
|
|
|
1
|
-
/** @jsxImportSource react */
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { Box, Text } from 'ink';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
provider: string;
|
|
7
|
+
model: string;
|
|
8
|
+
/**
|
|
9
|
+
* v3.0.0: optional cache hit-rate + estimated savings. When provided,
|
|
10
|
+
* the header shows `⚡ 命中率 87% · 节省 ¥0.42` on the right side. When
|
|
11
|
+
* null (no rounds yet, or provider does not surface usage), the right
|
|
12
|
+
* side just shows provider/model.
|
|
13
|
+
*/
|
|
14
|
+
cacheHitRate?: number | null;
|
|
15
|
+
cacheSavingsCNY?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Choose a color for the hit-rate chip based on Reasonix-style thresholds:
|
|
20
|
+
* ≥ 80% green (excellent — most of the prefix is being reused)
|
|
21
|
+
* ≥ 50% yellow (partial hit — system prompt is cached but messages tail is not)
|
|
22
|
+
* < 50% gray (cold — every round re-creates the cache; investigate)
|
|
23
|
+
*/
|
|
24
|
+
function hitRateColor(rate: number): string {
|
|
25
|
+
if (rate >= 0.8) return '#10B981';
|
|
26
|
+
if (rate >= 0.5) return '#F59E0B';
|
|
27
|
+
return '#6B7280';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const Header = React.memo(function Header({ provider, model, cacheHitRate, cacheSavingsCNY }: Props) {
|
|
31
|
+
const showCache = typeof cacheHitRate === 'number' && cacheHitRate > 0;
|
|
32
|
+
return (
|
|
33
|
+
<Box flexDirection="column" marginBottom={0}>
|
|
34
|
+
<Box justifyContent="space-between" width="100%">
|
|
35
|
+
<Box>
|
|
36
|
+
<Text color="#06B6D4" bold> ⚡ </Text>
|
|
37
|
+
<Text color="#22D3EE" bold>THATGFSJ CODE</Text>
|
|
38
|
+
<Text dimColor> v3.0.0</Text>
|
|
39
|
+
</Box>
|
|
40
|
+
<Box>
|
|
41
|
+
{showCache && (
|
|
42
|
+
<>
|
|
43
|
+
<Text color={hitRateColor(cacheHitRate!)} bold> ⚡ {(cacheHitRate! * 100).toFixed(1)}% </Text>
|
|
44
|
+
<Text dimColor> · 节省 ¥{(cacheSavingsCNY ?? 0).toFixed(2)} </Text>
|
|
45
|
+
<Text dimColor> · </Text>
|
|
46
|
+
</>
|
|
47
|
+
)}
|
|
48
|
+
<Text color="#06B6D4" bold> {provider} </Text>
|
|
49
|
+
<Text dimColor>/</Text>
|
|
50
|
+
<Text color="#22D3EE"> {model} </Text>
|
|
51
|
+
</Box>
|
|
52
|
+
</Box>
|
|
53
|
+
<Text color="#374151">{'─'.repeat(80)}</Text>
|
|
54
|
+
</Box>
|
|
55
|
+
);
|
|
56
|
+
});
|
|
@@ -1,132 +1,217 @@
|
|
|
1
|
-
/** @jsxImportSource react */
|
|
2
|
-
import React, { useState, useEffect } from 'react';
|
|
3
|
-
import { Box, Text } from 'ink';
|
|
4
|
-
import SelectInput from 'ink-select-input';
|
|
5
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
6
|
-
import { join } from 'path';
|
|
7
|
-
import { homedir } from 'os';
|
|
8
|
-
import { listProviders, getModelsForProvider, isCustomProvider } from '../../config/providers.js';
|
|
9
|
-
import type { ProviderName } from '../../config/types.js';
|
|
10
|
-
|
|
11
|
-
interface Props {
|
|
12
|
-
onComplete: (provider: ProviderName, model: string, apiKey: string, baseUrl?: string) => void;
|
|
13
|
-
onCancel: () => void;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
type InitStep =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
<Text color="#
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
import React, { useState, useEffect } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import SelectInput from 'ink-select-input';
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { homedir } from 'os';
|
|
8
|
+
import { listProviders, getModelsForProvider, isCustomProvider } from '../../config/providers.js';
|
|
9
|
+
import type { ProviderName } from '../../config/types.js';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
onComplete: (provider: ProviderName, model: string, apiKey: string, baseUrl?: string) => void;
|
|
13
|
+
onCancel: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type InitStep =
|
|
17
|
+
| 'provider'
|
|
18
|
+
| 'api_key'
|
|
19
|
+
| 'model'
|
|
20
|
+
| 'custom_model'
|
|
21
|
+
| 'custom_url'
|
|
22
|
+
| 'cache_strategy'
|
|
23
|
+
| 'cache_ttl';
|
|
24
|
+
|
|
25
|
+
interface CacheChoice {
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
ttl: '5m' | '1h';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* v3.0.0: InitWizard gained two new steps after model selection:
|
|
32
|
+
* - cache_strategy: enable / disable prompt caching
|
|
33
|
+
* - cache_ttl: 5m vs 1h (only if enabled)
|
|
34
|
+
*
|
|
35
|
+
* The defaults mirror ConfigManager.DEFAULT_CONFIG (enabled, 5m). Users on
|
|
36
|
+
* OpenAI / DeepSeek / Gemini can ignore these — those providers do
|
|
37
|
+
* automatic caching and the flag is purely advisory there. The flag
|
|
38
|
+
* matters for Anthropic users who want to opt out of cache_control
|
|
39
|
+
* markers or pick a longer TTL.
|
|
40
|
+
*/
|
|
41
|
+
export function InitWizard({ onComplete, onCancel }: Props) {
|
|
42
|
+
const [step, setStep] = useState<InitStep>('provider');
|
|
43
|
+
const [selectedProvider, setSelectedProvider] = useState<ProviderName>('siliconflow');
|
|
44
|
+
const [apiKey, setApiKey] = useState('');
|
|
45
|
+
const [selectedModel, setSelectedModel] = useState('');
|
|
46
|
+
const [customUrl, setCustomUrl] = useState('');
|
|
47
|
+
const [cacheChoice, setCacheChoice] = useState<CacheChoice>({ enabled: true, ttl: '5m' });
|
|
48
|
+
|
|
49
|
+
const providers = listProviders();
|
|
50
|
+
const models = getModelsForProvider(selectedProvider);
|
|
51
|
+
|
|
52
|
+
const saveConfig = (provider: ProviderName, model: string, key: string, url?: string) => {
|
|
53
|
+
const dir = join(homedir(), '.thatgfsj');
|
|
54
|
+
const configPath = join(dir, 'config.json');
|
|
55
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
56
|
+
|
|
57
|
+
const config: Record<string, any> = {
|
|
58
|
+
provider,
|
|
59
|
+
model,
|
|
60
|
+
apiKey: key,
|
|
61
|
+
temperature: 0.7,
|
|
62
|
+
maxTokens: 4096,
|
|
63
|
+
contextLength: 50,
|
|
64
|
+
// v3.0.0: cache policy from wizard
|
|
65
|
+
cache: {
|
|
66
|
+
enabled: cacheChoice.enabled,
|
|
67
|
+
ttl: cacheChoice.ttl,
|
|
68
|
+
strategy: 'auto',
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
if (url) config.baseUrl = url;
|
|
72
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
73
|
+
|
|
74
|
+
// Save to models history
|
|
75
|
+
const historyPath = join(dir, 'models.json');
|
|
76
|
+
let history: string[] = [];
|
|
77
|
+
if (existsSync(historyPath)) {
|
|
78
|
+
try { history = JSON.parse(readFileSync(historyPath, 'utf-8')); } catch {}
|
|
79
|
+
}
|
|
80
|
+
if (!history.includes(model)) {
|
|
81
|
+
history.push(model);
|
|
82
|
+
writeFileSync(historyPath, JSON.stringify(history, null, 2));
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Step 1: Provider selection
|
|
87
|
+
if (step === 'provider') {
|
|
88
|
+
const items = providers.map(p => ({ label: p.name, value: p.key }));
|
|
89
|
+
return (
|
|
90
|
+
<Box flexDirection="column" paddingLeft={1}>
|
|
91
|
+
<Text color="#06B6D4" bold>选择服务商 (↑↓ 回车):</Text>
|
|
92
|
+
<SelectInput
|
|
93
|
+
items={items}
|
|
94
|
+
onSelect={(item) => {
|
|
95
|
+
setSelectedProvider(item.value as ProviderName);
|
|
96
|
+
if (isCustomProvider(item.value as ProviderName)) {
|
|
97
|
+
setStep('custom_url');
|
|
98
|
+
} else {
|
|
99
|
+
setStep('api_key');
|
|
100
|
+
}
|
|
101
|
+
}}
|
|
102
|
+
/>
|
|
103
|
+
</Box>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Custom URL input
|
|
108
|
+
if (step === 'custom_url') {
|
|
109
|
+
return (
|
|
110
|
+
<Box flexDirection="column" paddingLeft={1}>
|
|
111
|
+
<Text color="#06B6D4" bold>输入中转站 URL:</Text>
|
|
112
|
+
<Text dimColor>例如: https://api.example.com/v1</Text>
|
|
113
|
+
<Text color="#F59E0B">(请直接输入 URL 并回车)</Text>
|
|
114
|
+
</Box>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Step 2: API Key
|
|
119
|
+
if (step === 'api_key') {
|
|
120
|
+
return (
|
|
121
|
+
<Box flexDirection="column" paddingLeft={1}>
|
|
122
|
+
<Text color="#06B6D4" bold>输入 API Key:</Text>
|
|
123
|
+
<Text dimColor>服务商: {providers.find(p => p.key === selectedProvider)?.name}</Text>
|
|
124
|
+
<Text color="#F59E0B">(请直接输入 Key 并回车)</Text>
|
|
125
|
+
</Box>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Step 3: Model selection
|
|
130
|
+
if (step === 'model') {
|
|
131
|
+
const items = [
|
|
132
|
+
...models.map(m => ({ label: `${m.name} - ${m.desc}`, value: m.id })),
|
|
133
|
+
{ label: '+ 输入自定义模型名', value: '__custom__' },
|
|
134
|
+
];
|
|
135
|
+
return (
|
|
136
|
+
<Box flexDirection="column" paddingLeft={1}>
|
|
137
|
+
<Text color="#06B6D4" bold>选择模型 (↑↓ 回车):</Text>
|
|
138
|
+
<SelectInput
|
|
139
|
+
items={items}
|
|
140
|
+
onSelect={(item) => {
|
|
141
|
+
if (item.value === '__custom__') {
|
|
142
|
+
setStep('custom_model');
|
|
143
|
+
} else {
|
|
144
|
+
setSelectedModel(item.value);
|
|
145
|
+
// v3.0.0: prompt the user for cache strategy after model.
|
|
146
|
+
setStep('cache_strategy');
|
|
147
|
+
}
|
|
148
|
+
}}
|
|
149
|
+
/>
|
|
150
|
+
</Box>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Custom model name
|
|
155
|
+
if (step === 'custom_model') {
|
|
156
|
+
return (
|
|
157
|
+
<Box flexDirection="column" paddingLeft={1}>
|
|
158
|
+
<Text color="#06B6D4" bold>输入模型名称:</Text>
|
|
159
|
+
<Text color="#F59E0B">(请直接输入模型名并回车)</Text>
|
|
160
|
+
</Box>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// v3.0.0: prompt-cache strategy step
|
|
165
|
+
if (step === 'cache_strategy') {
|
|
166
|
+
const items = [
|
|
167
|
+
{ label: '✓ 开启(推荐,节省 token 成本)', value: 'on' },
|
|
168
|
+
{ label: '✗ 关闭(每次请求都重新计算)', value: 'off' },
|
|
169
|
+
];
|
|
170
|
+
return (
|
|
171
|
+
<Box flexDirection="column" paddingLeft={1}>
|
|
172
|
+
<Text color="#06B6D4" bold>是否开启 Prompt Caching?</Text>
|
|
173
|
+
<Text dimColor>对 Anthropic / DeepSeek / Gemini 有效,OpenAI 兼容接口默认自动缓存。</Text>
|
|
174
|
+
<SelectInput
|
|
175
|
+
items={items}
|
|
176
|
+
onSelect={(item) => {
|
|
177
|
+
if (item.value === 'on') {
|
|
178
|
+
setCacheChoice(c => ({ ...c, enabled: true }));
|
|
179
|
+
setStep('cache_ttl');
|
|
180
|
+
} else {
|
|
181
|
+
setCacheChoice(c => ({ ...c, enabled: false }));
|
|
182
|
+
saveConfig(selectedProvider, selectedModel, apiKey, customUrl || undefined);
|
|
183
|
+
onComplete(selectedProvider, selectedModel, apiKey, customUrl || undefined);
|
|
184
|
+
}
|
|
185
|
+
}}
|
|
186
|
+
/>
|
|
187
|
+
</Box>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// v3.0.0: TTL choice
|
|
192
|
+
if (step === 'cache_ttl') {
|
|
193
|
+
const items = [
|
|
194
|
+
{ label: '5 分钟(写入成本低,适合短会话)', value: '5m' },
|
|
195
|
+
{ label: '1 小时(写入成本高,适合长会话)', value: '1h' },
|
|
196
|
+
];
|
|
197
|
+
return (
|
|
198
|
+
<Box flexDirection="column" paddingLeft={1}>
|
|
199
|
+
<Text color="#06B6D4" bold>Cache TTL:</Text>
|
|
200
|
+
<SelectInput
|
|
201
|
+
items={items}
|
|
202
|
+
onSelect={(item) => {
|
|
203
|
+
setCacheChoice(c => ({ ...c, ttl: item.value as '5m' | '1h' }));
|
|
204
|
+
saveConfig(selectedProvider, selectedModel, apiKey, customUrl || undefined);
|
|
205
|
+
onComplete(selectedProvider, selectedModel, apiKey, customUrl || undefined);
|
|
206
|
+
}}
|
|
207
|
+
/>
|
|
208
|
+
</Box>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Export steps for external handling
|
|
216
|
+
export type { InitStep };
|
|
217
|
+
export { isCustomProvider };
|