thatgfsj-code 2.2.9 → 3.0.1

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.
Files changed (190) hide show
  1. package/CHANGELOG.md +15 -67
  2. package/dist/app/index.d.ts +28 -5
  3. package/dist/app/index.d.ts.map +1 -1
  4. package/dist/app/index.js +76 -10
  5. package/dist/app/index.js.map +1 -1
  6. package/dist/cache/fingerprint.d.ts +49 -0
  7. package/dist/cache/fingerprint.d.ts.map +1 -0
  8. package/dist/cache/fingerprint.js +100 -0
  9. package/dist/cache/fingerprint.js.map +1 -0
  10. package/dist/cache/index.d.ts +15 -0
  11. package/dist/cache/index.d.ts.map +1 -0
  12. package/dist/cache/index.js +15 -0
  13. package/dist/cache/index.js.map +1 -0
  14. package/dist/cache/smartModel.d.ts +40 -0
  15. package/dist/cache/smartModel.d.ts.map +1 -0
  16. package/dist/cache/smartModel.js +43 -0
  17. package/dist/cache/smartModel.js.map +1 -0
  18. package/dist/cache/stats.d.ts +93 -0
  19. package/dist/cache/stats.d.ts.map +1 -0
  20. package/dist/cache/stats.js +155 -0
  21. package/dist/cache/stats.js.map +1 -0
  22. package/dist/cache/volatile.d.ts +32 -0
  23. package/dist/cache/volatile.d.ts.map +1 -0
  24. package/dist/cache/volatile.js +44 -0
  25. package/dist/cache/volatile.js.map +1 -0
  26. package/dist/cmd/index.js +49 -57
  27. package/dist/cmd/index.js.map +1 -1
  28. package/dist/config/index.d.ts.map +1 -1
  29. package/dist/config/index.js +8 -0
  30. package/dist/config/index.js.map +1 -1
  31. package/dist/config/types.d.ts +14 -0
  32. package/dist/config/types.d.ts.map +1 -1
  33. package/dist/llm/anthropic.d.ts +55 -5
  34. package/dist/llm/anthropic.d.ts.map +1 -1
  35. package/dist/llm/anthropic.js +172 -26
  36. package/dist/llm/anthropic.js.map +1 -1
  37. package/dist/llm/index.d.ts +26 -8
  38. package/dist/llm/index.d.ts.map +1 -1
  39. package/dist/llm/index.js +83 -15
  40. package/dist/llm/index.js.map +1 -1
  41. package/dist/llm/openai.d.ts +28 -8
  42. package/dist/llm/openai.d.ts.map +1 -1
  43. package/dist/llm/openai.js +58 -8
  44. package/dist/llm/openai.js.map +1 -1
  45. package/dist/llm/provider.d.ts +40 -8
  46. package/dist/llm/provider.d.ts.map +1 -1
  47. package/dist/llm/provider.js +8 -1
  48. package/dist/llm/provider.js.map +1 -1
  49. package/dist/prompts/index.d.ts +32 -0
  50. package/dist/prompts/index.d.ts.map +1 -1
  51. package/dist/prompts/index.js +33 -11
  52. package/dist/prompts/index.js.map +1 -1
  53. package/dist/session/index.d.ts +32 -3
  54. package/dist/session/index.d.ts.map +1 -1
  55. package/dist/session/index.js +51 -36
  56. package/dist/session/index.js.map +1 -1
  57. package/dist/skills/brainstorming.js +33 -33
  58. package/dist/skills/code-review.js +35 -35
  59. package/dist/skills/executing-plans.js +18 -18
  60. package/dist/skills/frontend-design.js +25 -25
  61. package/dist/skills/git-workflow.js +26 -26
  62. package/dist/skills/improve-architecture.js +28 -28
  63. package/dist/skills/neuroweave.js +37 -37
  64. package/dist/skills/playwright.js +62 -62
  65. package/dist/skills/prototype.js +20 -20
  66. package/dist/skills/subagent.js +19 -19
  67. package/dist/skills/supabase.js +34 -34
  68. package/dist/skills/systematic-debugging.js +34 -34
  69. package/dist/skills/tdd.js +29 -29
  70. package/dist/skills/triage.js +25 -25
  71. package/dist/skills/verification.js +22 -22
  72. package/dist/skills/writing-plans.js +32 -32
  73. package/dist/tools/nwt.js +16 -16
  74. package/dist/tui/app.d.ts.map +1 -1
  75. package/dist/tui/app.js +9 -2
  76. package/dist/tui/app.js.map +1 -1
  77. package/dist/tui/components/Header.d.ts +8 -0
  78. package/dist/tui/components/Header.d.ts.map +1 -1
  79. package/dist/tui/components/Header.js +17 -4
  80. package/dist/tui/components/Header.js.map +1 -1
  81. package/dist/tui/components/InitWizard.d.ts +12 -1
  82. package/dist/tui/components/InitWizard.d.ts.map +1 -1
  83. package/dist/tui/components/InitWizard.js +58 -3
  84. package/dist/tui/components/InitWizard.js.map +1 -1
  85. package/dist/tui/components/ModelSelector.d.ts +3 -1
  86. package/dist/tui/components/ModelSelector.d.ts.map +1 -1
  87. package/dist/tui/components/ModelSelector.js +24 -7
  88. package/dist/tui/components/ModelSelector.js.map +1 -1
  89. package/dist/tui/components/ToolCall.d.ts.map +1 -1
  90. package/dist/tui/components/ToolCall.js +1 -6
  91. package/dist/tui/components/ToolCall.js.map +1 -1
  92. package/dist/tui/components/UserInput.d.ts.map +1 -1
  93. package/dist/tui/components/UserInput.js +20 -0
  94. package/dist/tui/components/UserInput.js.map +1 -1
  95. package/dist/tui/hooks/useChat.d.ts +22 -0
  96. package/dist/tui/hooks/useChat.d.ts.map +1 -1
  97. package/dist/tui/hooks/useChat.js +82 -46
  98. package/dist/tui/hooks/useChat.js.map +1 -1
  99. package/dist/tui/hooks/useCommands.d.ts.map +1 -1
  100. package/dist/tui/hooks/useCommands.js +57 -0
  101. package/dist/tui/hooks/useCommands.js.map +1 -1
  102. package/dist/tui/welcome.d.ts.map +1 -1
  103. package/dist/tui/welcome.js +2 -3
  104. package/dist/tui/welcome.js.map +1 -1
  105. package/dist/types.d.ts +79 -6
  106. package/dist/types.d.ts.map +1 -1
  107. package/dist/types.js +7 -0
  108. package/dist/types.js.map +1 -1
  109. package/dist/utils/stableStringify.d.ts +21 -0
  110. package/dist/utils/stableStringify.d.ts.map +1 -0
  111. package/dist/utils/stableStringify.js +53 -0
  112. package/dist/utils/stableStringify.js.map +1 -0
  113. package/dist/utils/thinking.d.ts.map +1 -1
  114. package/dist/utils/thinking.js +0 -3
  115. package/dist/utils/thinking.js.map +1 -1
  116. package/package.json +7 -6
  117. package/src/app/index.ts +189 -124
  118. package/src/cache/fingerprint.ts +101 -0
  119. package/src/cache/index.ts +15 -0
  120. package/src/cache/smartModel.ts +52 -0
  121. package/src/cache/stats.ts +199 -0
  122. package/src/cache/volatile.ts +47 -0
  123. package/src/cmd/index.tsx +288 -292
  124. package/src/config/index.ts +156 -148
  125. package/src/config/providers.ts +234 -234
  126. package/src/config/types.ts +67 -53
  127. package/src/hooks/index.ts +111 -111
  128. package/src/llm/anthropic.ts +388 -243
  129. package/src/llm/gemini.ts +169 -169
  130. package/src/llm/index.ts +265 -200
  131. package/src/llm/openai.ts +243 -196
  132. package/src/llm/provider.ts +66 -34
  133. package/src/prompts/index.ts +260 -220
  134. package/src/session/compactor.ts +103 -103
  135. package/src/session/index.ts +181 -168
  136. package/src/session/message.ts +42 -42
  137. package/src/skills/brainstorming.ts +43 -43
  138. package/src/skills/code-review.ts +45 -45
  139. package/src/skills/executing-plans.ts +27 -27
  140. package/src/skills/frontend-design.ts +35 -35
  141. package/src/skills/git-workflow.ts +36 -36
  142. package/src/skills/improve-architecture.ts +38 -38
  143. package/src/skills/index.ts +136 -136
  144. package/src/skills/neuroweave.ts +47 -47
  145. package/src/skills/playwright.ts +72 -72
  146. package/src/skills/prototype.ts +30 -30
  147. package/src/skills/subagent.ts +28 -28
  148. package/src/skills/supabase.ts +44 -44
  149. package/src/skills/systematic-debugging.ts +44 -44
  150. package/src/skills/tdd.ts +39 -39
  151. package/src/skills/triage.ts +35 -35
  152. package/src/skills/verification.ts +31 -31
  153. package/src/skills/writing-plans.ts +42 -42
  154. package/src/tools/nwt.ts +598 -598
  155. package/src/tools/types.ts +122 -122
  156. package/src/tui/app.tsx +200 -186
  157. package/src/tui/components/ChatList.tsx +41 -41
  158. package/src/tui/components/ChatMessage.tsx +54 -54
  159. package/src/tui/components/Header.tsx +56 -29
  160. package/src/tui/components/InitWizard.tsx +217 -132
  161. package/src/tui/components/Markdown.tsx +35 -35
  162. package/src/tui/components/ModelSelector.tsx +107 -87
  163. package/src/tui/components/StatusBar.tsx +30 -30
  164. package/src/tui/components/Thinking.tsx +17 -17
  165. package/src/tui/components/ToolCall.tsx +102 -107
  166. package/src/tui/components/UserInput.tsx +151 -131
  167. package/src/tui/hooks/useChat.ts +287 -238
  168. package/src/tui/hooks/useCommands.ts +234 -176
  169. package/src/tui/index.ts +6 -6
  170. package/src/tui/welcome.ts +177 -178
  171. package/src/types.ts +104 -42
  172. package/src/utils/diff.ts +71 -71
  173. package/src/utils/project.ts +99 -99
  174. package/src/utils/stableStringify.ts +47 -0
  175. package/src/utils/thinking.ts +118 -121
  176. package/tests/cache/fingerprint.test.ts +75 -0
  177. package/tests/cache/providerCatalog.test.ts +31 -0
  178. package/tests/cache/stableStringify.test.ts +43 -0
  179. package/tests/cache/stats.test.ts +146 -0
  180. package/tests/cache/volatile.test.ts +75 -0
  181. package/tests/smoke-pollution.mjs +78 -0
  182. package/tests/smoke-thinking.mjs +110 -0
  183. package/tests/smoke-tool-stream.mjs +69 -0
  184. package/tests/smoke-tool.mjs +117 -0
  185. package/.nwt/meta.json +0 -5
  186. package/dist/version.d.ts +0 -16
  187. package/dist/version.d.ts.map +0 -1
  188. package/dist/version.js +0 -16
  189. package/dist/version.js.map +0 -1
  190. 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
- import { PRODUCT_VERSION_DISPLAY } from '../../version.js';
5
-
6
- interface Props {
7
- provider: string;
8
- model: string;
9
- }
10
-
11
- export const Header = React.memo(function Header({ provider, model }: Props) {
12
- return (
13
- <Box flexDirection="column" marginBottom={0}>
14
- <Box justifyContent="space-between" width="100%">
15
- <Box>
16
- <Text color="#06B6D4" bold> ⚡ </Text>
17
- <Text color="#22D3EE" bold>THATGFSJ CODE</Text>
18
- <Text dimColor> {PRODUCT_VERSION_DISPLAY}</Text>
19
- </Box>
20
- <Box>
21
- <Text color="#06B6D4" bold> {provider} </Text>
22
- <Text dimColor>/</Text>
23
- <Text color="#22D3EE"> {model} </Text>
24
- </Box>
25
- </Box>
26
- <Text color="#374151">{'─'.repeat(80)}</Text>
27
- </Box>
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 = 'provider' | 'api_key' | 'model' | 'custom_model' | 'custom_url';
17
-
18
- export function InitWizard({ onComplete, onCancel }: Props) {
19
- const [step, setStep] = useState<InitStep>('provider');
20
- const [selectedProvider, setSelectedProvider] = useState<ProviderName>('siliconflow');
21
- const [apiKey, setApiKey] = useState('');
22
- const [selectedModel, setSelectedModel] = useState('');
23
- const [customUrl, setCustomUrl] = useState('');
24
-
25
- const providers = listProviders();
26
- const models = getModelsForProvider(selectedProvider);
27
-
28
- const saveConfig = (provider: ProviderName, model: string, key: string, url?: string) => {
29
- const dir = join(homedir(), '.thatgfsj');
30
- const configPath = join(dir, 'config.json');
31
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
32
-
33
- const config: Record<string, any> = { provider, model, apiKey: key, temperature: 0.7, maxTokens: 4096, contextLength: 50 };
34
- if (url) config.baseUrl = url;
35
- writeFileSync(configPath, JSON.stringify(config, null, 2));
36
-
37
- // Save to models history
38
- const historyPath = join(dir, 'models.json');
39
- let history: string[] = [];
40
- if (existsSync(historyPath)) {
41
- try { history = JSON.parse(readFileSync(historyPath, 'utf-8')); } catch {}
42
- }
43
- if (!history.includes(model)) {
44
- history.push(model);
45
- writeFileSync(historyPath, JSON.stringify(history, null, 2));
46
- }
47
- };
48
-
49
- // Step 1: Provider selection
50
- if (step === 'provider') {
51
- const items = providers.map(p => ({ label: p.name, value: p.key }));
52
- return (
53
- <Box flexDirection="column" paddingLeft={1}>
54
- <Text color="#06B6D4" bold>选择服务商 (↑↓ 回车):</Text>
55
- <SelectInput
56
- items={items}
57
- onSelect={(item) => {
58
- setSelectedProvider(item.value as ProviderName);
59
- if (isCustomProvider(item.value as ProviderName)) {
60
- setStep('custom_url');
61
- } else {
62
- setStep('api_key');
63
- }
64
- }}
65
- />
66
- </Box>
67
- );
68
- }
69
-
70
- // Custom URL input
71
- if (step === 'custom_url') {
72
- return (
73
- <Box flexDirection="column" paddingLeft={1}>
74
- <Text color="#06B6D4" bold>输入中转站 URL:</Text>
75
- <Text dimColor>例如: https://api.example.com/v1</Text>
76
- <Text color="#F59E0B">(请直接输入 URL 并回车)</Text>
77
- </Box>
78
- );
79
- }
80
-
81
- // Step 2: API Key
82
- if (step === 'api_key') {
83
- return (
84
- <Box flexDirection="column" paddingLeft={1}>
85
- <Text color="#06B6D4" bold>输入 API Key:</Text>
86
- <Text dimColor>服务商: {providers.find(p => p.key === selectedProvider)?.name}</Text>
87
- <Text color="#F59E0B">(请直接输入 Key 并回车)</Text>
88
- </Box>
89
- );
90
- }
91
-
92
- // Step 3: Model selection
93
- if (step === 'model') {
94
- const items = [
95
- ...models.map(m => ({ label: `${m.name} - ${m.desc}`, value: m.id })),
96
- { label: '+ 输入自定义模型名', value: '__custom__' },
97
- ];
98
- return (
99
- <Box flexDirection="column" paddingLeft={1}>
100
- <Text color="#06B6D4" bold>选择模型 (↑↓ 回车):</Text>
101
- <SelectInput
102
- items={items}
103
- onSelect={(item) => {
104
- if (item.value === '__custom__') {
105
- setStep('custom_model');
106
- } else {
107
- setSelectedModel(item.value);
108
- saveConfig(selectedProvider, item.value, apiKey, customUrl || undefined);
109
- onComplete(selectedProvider, item.value, apiKey, customUrl || undefined);
110
- }
111
- }}
112
- />
113
- </Box>
114
- );
115
- }
116
-
117
- // Custom model name
118
- if (step === 'custom_model') {
119
- return (
120
- <Box flexDirection="column" paddingLeft={1}>
121
- <Text color="#06B6D4" bold>输入模型名称:</Text>
122
- <Text color="#F59E0B">(请直接输入模型名并回车)</Text>
123
- </Box>
124
- );
125
- }
126
-
127
- return null;
128
- }
129
-
130
- // Export steps for external handling
131
- export type { InitStep };
132
- export { isCustomProvider };
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 };