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.
Files changed (182) 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 +69 -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/ToolCall.d.ts.map +1 -1
  86. package/dist/tui/components/ToolCall.js +1 -6
  87. package/dist/tui/components/ToolCall.js.map +1 -1
  88. package/dist/tui/hooks/useChat.d.ts +22 -0
  89. package/dist/tui/hooks/useChat.d.ts.map +1 -1
  90. package/dist/tui/hooks/useChat.js +82 -46
  91. package/dist/tui/hooks/useChat.js.map +1 -1
  92. package/dist/tui/hooks/useCommands.d.ts.map +1 -1
  93. package/dist/tui/hooks/useCommands.js +57 -0
  94. package/dist/tui/hooks/useCommands.js.map +1 -1
  95. package/dist/tui/welcome.d.ts.map +1 -1
  96. package/dist/tui/welcome.js +2 -3
  97. package/dist/tui/welcome.js.map +1 -1
  98. package/dist/types.d.ts +79 -6
  99. package/dist/types.d.ts.map +1 -1
  100. package/dist/types.js +7 -0
  101. package/dist/types.js.map +1 -1
  102. package/dist/utils/stableStringify.d.ts +21 -0
  103. package/dist/utils/stableStringify.d.ts.map +1 -0
  104. package/dist/utils/stableStringify.js +53 -0
  105. package/dist/utils/stableStringify.js.map +1 -0
  106. package/dist/utils/thinking.d.ts.map +1 -1
  107. package/dist/utils/thinking.js +0 -3
  108. package/dist/utils/thinking.js.map +1 -1
  109. package/package.json +6 -6
  110. package/src/app/index.ts +180 -124
  111. package/src/cache/fingerprint.ts +101 -0
  112. package/src/cache/index.ts +15 -0
  113. package/src/cache/smartModel.ts +52 -0
  114. package/src/cache/stats.ts +199 -0
  115. package/src/cache/volatile.ts +47 -0
  116. package/src/cmd/index.tsx +288 -292
  117. package/src/config/index.ts +156 -148
  118. package/src/config/providers.ts +234 -234
  119. package/src/config/types.ts +67 -53
  120. package/src/hooks/index.ts +111 -111
  121. package/src/llm/anthropic.ts +388 -243
  122. package/src/llm/gemini.ts +169 -169
  123. package/src/llm/index.ts +265 -200
  124. package/src/llm/openai.ts +243 -196
  125. package/src/llm/provider.ts +66 -34
  126. package/src/prompts/index.ts +260 -220
  127. package/src/session/compactor.ts +103 -103
  128. package/src/session/index.ts +181 -168
  129. package/src/session/message.ts +42 -42
  130. package/src/skills/brainstorming.ts +43 -43
  131. package/src/skills/code-review.ts +45 -45
  132. package/src/skills/executing-plans.ts +27 -27
  133. package/src/skills/frontend-design.ts +35 -35
  134. package/src/skills/git-workflow.ts +36 -36
  135. package/src/skills/improve-architecture.ts +38 -38
  136. package/src/skills/index.ts +136 -136
  137. package/src/skills/neuroweave.ts +47 -47
  138. package/src/skills/playwright.ts +72 -72
  139. package/src/skills/prototype.ts +30 -30
  140. package/src/skills/subagent.ts +28 -28
  141. package/src/skills/supabase.ts +44 -44
  142. package/src/skills/systematic-debugging.ts +44 -44
  143. package/src/skills/tdd.ts +39 -39
  144. package/src/skills/triage.ts +35 -35
  145. package/src/skills/verification.ts +31 -31
  146. package/src/skills/writing-plans.ts +42 -42
  147. package/src/tools/nwt.ts +598 -598
  148. package/src/tools/types.ts +122 -122
  149. package/src/tui/app.tsx +199 -186
  150. package/src/tui/components/ChatList.tsx +41 -41
  151. package/src/tui/components/ChatMessage.tsx +54 -54
  152. package/src/tui/components/Header.tsx +56 -29
  153. package/src/tui/components/InitWizard.tsx +217 -132
  154. package/src/tui/components/Markdown.tsx +35 -35
  155. package/src/tui/components/ModelSelector.tsx +87 -87
  156. package/src/tui/components/StatusBar.tsx +30 -30
  157. package/src/tui/components/Thinking.tsx +17 -17
  158. package/src/tui/components/ToolCall.tsx +102 -107
  159. package/src/tui/components/UserInput.tsx +131 -131
  160. package/src/tui/hooks/useChat.ts +287 -238
  161. package/src/tui/hooks/useCommands.ts +234 -176
  162. package/src/tui/index.ts +6 -6
  163. package/src/tui/welcome.ts +177 -178
  164. package/src/types.ts +104 -42
  165. package/src/utils/diff.ts +71 -71
  166. package/src/utils/project.ts +99 -99
  167. package/src/utils/stableStringify.ts +47 -0
  168. package/src/utils/thinking.ts +118 -121
  169. package/tests/cache/fingerprint.test.ts +75 -0
  170. package/tests/cache/stableStringify.test.ts +43 -0
  171. package/tests/cache/stats.test.ts +146 -0
  172. package/tests/cache/volatile.test.ts +75 -0
  173. package/tests/smoke-pollution.mjs +78 -0
  174. package/tests/smoke-thinking.mjs +110 -0
  175. package/tests/smoke-tool-stream.mjs +69 -0
  176. package/tests/smoke-tool.mjs +117 -0
  177. package/.nwt/meta.json +0 -5
  178. package/dist/version.d.ts +0 -16
  179. package/dist/version.d.ts.map +0 -1
  180. package/dist/version.js +0 -16
  181. package/dist/version.js.map +0 -1
  182. 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 };