tabby-ai-assistant 1.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 (134) hide show
  1. package/README.md +232 -0
  2. package/dist/components/chat/chat-input.component.d.ts +65 -0
  3. package/dist/components/chat/chat-interface.component.d.ts +71 -0
  4. package/dist/components/chat/chat-message.component.d.ts +53 -0
  5. package/dist/components/chat/chat-settings.component.d.ts +62 -0
  6. package/dist/components/common/error-message.component.d.ts +11 -0
  7. package/dist/components/common/loading-spinner.component.d.ts +4 -0
  8. package/dist/components/security/consent-dialog.component.d.ts +11 -0
  9. package/dist/components/security/password-prompt.component.d.ts +10 -0
  10. package/dist/components/security/risk-confirm-dialog.component.d.ts +36 -0
  11. package/dist/components/settings/ai-settings-tab.component.d.ts +72 -0
  12. package/dist/components/settings/general-settings.component.d.ts +60 -0
  13. package/dist/components/settings/provider-config.component.d.ts +182 -0
  14. package/dist/components/settings/security-settings.component.d.ts +23 -0
  15. package/dist/components/terminal/ai-toolbar-button.component.d.ts +10 -0
  16. package/dist/components/terminal/command-preview.component.d.ts +15 -0
  17. package/dist/components/terminal/command-suggestion.component.d.ts +16 -0
  18. package/dist/index.d.ts +8 -0
  19. package/dist/index.js +2 -0
  20. package/dist/index.js.LICENSE.txt +18 -0
  21. package/dist/main.d.ts +8 -0
  22. package/dist/providers/tabby/ai-config.provider.d.ts +18 -0
  23. package/dist/providers/tabby/ai-hotkey.provider.d.ts +21 -0
  24. package/dist/providers/tabby/ai-settings-tab.provider.d.ts +11 -0
  25. package/dist/providers/tabby/ai-toolbar-button.provider.d.ts +17 -0
  26. package/dist/services/chat/chat-history.service.d.ts +67 -0
  27. package/dist/services/chat/chat-session.service.d.ts +58 -0
  28. package/dist/services/chat/command-generator.service.d.ts +49 -0
  29. package/dist/services/core/ai-assistant.service.d.ts +88 -0
  30. package/dist/services/core/ai-provider-manager.service.d.ts +119 -0
  31. package/dist/services/core/config-provider.service.d.ts +137 -0
  32. package/dist/services/core/logger.service.d.ts +21 -0
  33. package/dist/services/providers/anthropic-provider.service.d.ts +39 -0
  34. package/dist/services/providers/base-provider.service.d.ts +137 -0
  35. package/dist/services/providers/glm-provider.service.d.ts +91 -0
  36. package/dist/services/providers/minimax-provider.service.d.ts +93 -0
  37. package/dist/services/providers/openai-compatible.service.d.ts +39 -0
  38. package/dist/services/providers/openai-provider.service.d.ts +38 -0
  39. package/dist/services/security/consent-manager.service.d.ts +65 -0
  40. package/dist/services/security/password-manager.service.d.ts +67 -0
  41. package/dist/services/security/risk-assessment.service.d.ts +65 -0
  42. package/dist/services/security/security-validator.service.d.ts +36 -0
  43. package/dist/services/terminal/command-analyzer.service.d.ts +20 -0
  44. package/dist/services/terminal/context-menu.service.d.ts +24 -0
  45. package/dist/services/terminal/hotkey.service.d.ts +28 -0
  46. package/dist/services/terminal/terminal-context.service.d.ts +100 -0
  47. package/dist/types/ai.types.d.ts +107 -0
  48. package/dist/types/provider.types.d.ts +105 -0
  49. package/dist/types/security.types.d.ts +85 -0
  50. package/dist/types/terminal.types.d.ts +150 -0
  51. package/dist/utils/encryption.utils.d.ts +83 -0
  52. package/dist/utils/formatting.utils.d.ts +106 -0
  53. package/dist/utils/validation.utils.d.ts +83 -0
  54. package/integration-test-output.txt +50 -0
  55. package/integration-tests/api-integration.test.ts +183 -0
  56. package/jest.config.js +47 -0
  57. package/package.json +73 -0
  58. package/setup-jest.ts +37 -0
  59. package/src/components/chat/chat-input.component.html +61 -0
  60. package/src/components/chat/chat-input.component.scss +183 -0
  61. package/src/components/chat/chat-input.component.ts +149 -0
  62. package/src/components/chat/chat-interface.component.html +119 -0
  63. package/src/components/chat/chat-interface.component.scss +354 -0
  64. package/src/components/chat/chat-interface.component.ts +224 -0
  65. package/src/components/chat/chat-message.component.html +65 -0
  66. package/src/components/chat/chat-message.component.scss +178 -0
  67. package/src/components/chat/chat-message.component.ts +93 -0
  68. package/src/components/chat/chat-settings.component.html +132 -0
  69. package/src/components/chat/chat-settings.component.scss +172 -0
  70. package/src/components/chat/chat-settings.component.ts +168 -0
  71. package/src/components/common/error-message.component.ts +124 -0
  72. package/src/components/common/loading-spinner.component.ts +72 -0
  73. package/src/components/security/consent-dialog.component.ts +77 -0
  74. package/src/components/security/password-prompt.component.ts +79 -0
  75. package/src/components/security/risk-confirm-dialog.component.html +87 -0
  76. package/src/components/security/risk-confirm-dialog.component.scss +360 -0
  77. package/src/components/security/risk-confirm-dialog.component.ts +96 -0
  78. package/src/components/settings/ai-settings-tab.component.html +140 -0
  79. package/src/components/settings/ai-settings-tab.component.scss +371 -0
  80. package/src/components/settings/ai-settings-tab.component.ts +193 -0
  81. package/src/components/settings/general-settings.component.html +103 -0
  82. package/src/components/settings/general-settings.component.scss +285 -0
  83. package/src/components/settings/general-settings.component.ts +123 -0
  84. package/src/components/settings/provider-config.component.html +95 -0
  85. package/src/components/settings/provider-config.component.scss +60 -0
  86. package/src/components/settings/provider-config.component.ts +206 -0
  87. package/src/components/settings/security-settings.component.html +51 -0
  88. package/src/components/settings/security-settings.component.scss +66 -0
  89. package/src/components/settings/security-settings.component.ts +71 -0
  90. package/src/components/terminal/ai-toolbar-button.component.ts +49 -0
  91. package/src/components/terminal/command-preview.component.ts +185 -0
  92. package/src/components/terminal/command-suggestion.component.ts +128 -0
  93. package/src/index.ts +163 -0
  94. package/src/main.ts +16 -0
  95. package/src/providers/tabby/ai-config.provider.ts +70 -0
  96. package/src/providers/tabby/ai-hotkey.provider.ts +55 -0
  97. package/src/providers/tabby/ai-settings-tab.provider.ts +18 -0
  98. package/src/providers/tabby/ai-toolbar-button.provider.ts +49 -0
  99. package/src/services/chat/chat-history.service.ts +239 -0
  100. package/src/services/chat/chat-session.service.spec.ts +249 -0
  101. package/src/services/chat/chat-session.service.ts +180 -0
  102. package/src/services/chat/command-generator.service.ts +301 -0
  103. package/src/services/core/ai-assistant.service.ts +334 -0
  104. package/src/services/core/ai-provider-manager.service.ts +314 -0
  105. package/src/services/core/config-provider.service.ts +347 -0
  106. package/src/services/core/logger.service.ts +104 -0
  107. package/src/services/providers/anthropic-provider.service.ts +373 -0
  108. package/src/services/providers/base-provider.service.ts +369 -0
  109. package/src/services/providers/glm-provider.service.ts +467 -0
  110. package/src/services/providers/minimax-provider.service.ts +427 -0
  111. package/src/services/providers/openai-compatible.service.ts +394 -0
  112. package/src/services/providers/openai-provider.service.ts +376 -0
  113. package/src/services/security/consent-manager.service.ts +332 -0
  114. package/src/services/security/password-manager.service.ts +188 -0
  115. package/src/services/security/risk-assessment.service.ts +340 -0
  116. package/src/services/security/security-validator.service.ts +143 -0
  117. package/src/services/terminal/command-analyzer.service.ts +43 -0
  118. package/src/services/terminal/context-menu.service.ts +45 -0
  119. package/src/services/terminal/hotkey.service.ts +53 -0
  120. package/src/services/terminal/terminal-context.service.ts +317 -0
  121. package/src/styles/ai-assistant.scss +449 -0
  122. package/src/types/ai.types.ts +133 -0
  123. package/src/types/provider.types.ts +147 -0
  124. package/src/types/security.types.ts +103 -0
  125. package/src/types/terminal.types.ts +186 -0
  126. package/src/utils/encryption.utils.spec.ts +250 -0
  127. package/src/utils/encryption.utils.ts +271 -0
  128. package/src/utils/formatting.utils.ts +359 -0
  129. package/src/utils/validation.utils.spec.ts +225 -0
  130. package/src/utils/validation.utils.ts +314 -0
  131. package/tsconfig.json +45 -0
  132. package/webpack-docker.config.js +42 -0
  133. package/webpack.config.js +59 -0
  134. package/webpack.config.js.backup +57 -0
@@ -0,0 +1,271 @@
1
+ import * as CryptoJS from 'crypto-js';
2
+
3
+ /**
4
+ * 加密工具类
5
+ * 提供安全的加密和解密功能
6
+ */
7
+
8
+ const KEY_SIZE = 256;
9
+ const IV_SIZE = 16;
10
+
11
+ /**
12
+ * 生成随机密钥
13
+ */
14
+ export function generateKey(): string {
15
+ return CryptoJS.lib.WordArray.random(KEY_SIZE / 8).toString();
16
+ }
17
+
18
+ /**
19
+ * 生成随机初始化向量
20
+ */
21
+ export function generateIV(): string {
22
+ return CryptoJS.lib.WordArray.random(IV_SIZE).toString();
23
+ }
24
+
25
+ /**
26
+ * 使用AES加密字符串
27
+ */
28
+ export function encrypt(text: string, key: string): string {
29
+ if (!text || !key) {
30
+ throw new Error('文本和密钥不能为空');
31
+ }
32
+
33
+ try {
34
+ const encrypted = CryptoJS.AES.encrypt(text, key);
35
+ return encrypted.toString();
36
+ } catch (error) {
37
+ throw new Error(`加密失败: ${error instanceof Error ? error.message : String(error)}`);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * 使用AES解密字符串
43
+ */
44
+ export function decrypt(encryptedText: string, key: string): string {
45
+ if (!encryptedText || !key) {
46
+ throw new Error('加密文本和密钥不能为空');
47
+ }
48
+
49
+ try {
50
+ const decrypted = CryptoJS.AES.decrypt(encryptedText, key);
51
+ return decrypted.toString(CryptoJS.enc.Utf8);
52
+ } catch (error) {
53
+ throw new Error(`解密失败: ${error instanceof Error ? error.message : String(error)}`);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * 使用PBKDF2派生密钥
59
+ */
60
+ export function deriveKey(password: string, salt: string, iterations: number = 10000): string {
61
+ if (!password || !salt) {
62
+ throw new Error('密码和盐值不能为空');
63
+ }
64
+
65
+ try {
66
+ const key = CryptoJS.PBKDF2(password, salt, {
67
+ keySize: KEY_SIZE / 32,
68
+ iterations: iterations
69
+ });
70
+ return key.toString();
71
+ } catch (error) {
72
+ throw new Error(`密钥派生失败: ${error instanceof Error ? error.message : String(error)}`);
73
+ }
74
+ }
75
+
76
+ /**
77
+ * 生成加密盐值
78
+ */
79
+ export function generateSalt(): string {
80
+ return CryptoJS.lib.WordArray.random(128 / 8).toString();
81
+ }
82
+
83
+ /**
84
+ * 安全的哈希函数
85
+ */
86
+ export function hash(text: string, salt?: string): string {
87
+ if (!text) {
88
+ throw new Error('文本不能为空');
89
+ }
90
+
91
+ try {
92
+ const data = salt ? text + salt : text;
93
+ return CryptoJS.SHA256(data).toString();
94
+ } catch (error) {
95
+ throw new Error(`哈希失败: ${error instanceof Error ? error.message : String(error)}`);
96
+ }
97
+ }
98
+
99
+ /**
100
+ * 验证哈希值
101
+ */
102
+ export function verifyHash(text: string, hashValue: string, salt?: string): boolean {
103
+ try {
104
+ const computedHash = hash(text, salt);
105
+ return computedHash === hashValue;
106
+ } catch {
107
+ return false;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * 加密对象
113
+ */
114
+ export function encryptObject(obj: any, key: string): string {
115
+ if (!obj || !key) {
116
+ throw new Error('对象和密钥不能为空');
117
+ }
118
+
119
+ try {
120
+ const jsonString = JSON.stringify(obj);
121
+ return encrypt(jsonString, key);
122
+ } catch (error) {
123
+ throw new Error(`对象加密失败: ${error instanceof Error ? error.message : String(error)}`);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * 解密对象
129
+ */
130
+ export function decryptObject<T>(encryptedText: string, key: string): T {
131
+ if (!encryptedText || !key) {
132
+ throw new Error('加密文本和密钥不能为空');
133
+ }
134
+
135
+ try {
136
+ const jsonString = decrypt(encryptedText, key);
137
+ return JSON.parse(jsonString) as T;
138
+ } catch (error) {
139
+ throw new Error(`对象解密失败: ${error instanceof Error ? error.message : String(error)}`);
140
+ }
141
+ }
142
+
143
+ /**
144
+ * 生成令牌
145
+ */
146
+ export function generateToken(length: number = 32): string {
147
+ return CryptoJS.lib.WordArray.random(length).toString();
148
+ }
149
+
150
+ /**
151
+ * Base64编码
152
+ */
153
+ export function base64Encode(text: string): string {
154
+ if (!text) {
155
+ throw new Error('文本不能为空');
156
+ }
157
+
158
+ try {
159
+ return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(text));
160
+ } catch (error) {
161
+ throw new Error(`Base64编码失败: ${error instanceof Error ? error.message : String(error)}`);
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Base64解码
167
+ */
168
+ export function base64Decode(encodedText: string): string {
169
+ if (!encodedText) {
170
+ throw new Error('编码文本不能为空');
171
+ }
172
+
173
+ try {
174
+ return CryptoJS.enc.Base64.parse(encodedText).toString(CryptoJS.enc.Utf8);
175
+ } catch (error) {
176
+ throw new Error(`Base64解码失败: ${error instanceof Error ? error.message : String(error)}`);
177
+ }
178
+ }
179
+
180
+ /**
181
+ * 安全地比较两个字符串(防止时序攻击)
182
+ */
183
+ export function secureCompare(a: string, b: string): boolean {
184
+ if (a.length !== b.length) {
185
+ return false;
186
+ }
187
+
188
+ let result = 0;
189
+ for (let i = 0; i < a.length; i++) {
190
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
191
+ }
192
+
193
+ return result === 0;
194
+ }
195
+
196
+ /**
197
+ * 清除敏感数据
198
+ */
199
+ export function clearSensitiveData(obj: any): void {
200
+ if (!obj) return;
201
+
202
+ const sensitiveKeys = ['password', 'apiKey', 'token', 'secret', 'key', 'credential'];
203
+
204
+ for (const key in obj) {
205
+ if (obj.hasOwnProperty(key)) {
206
+ const lowerKey = key.toLowerCase();
207
+
208
+ if (sensitiveKeys.some(sensitive => lowerKey.includes(sensitive))) {
209
+ obj[key] = '********';
210
+ } else if (typeof obj[key] === 'object') {
211
+ clearSensitiveData(obj[key]);
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ /**
218
+ * 生成加密的配置对象
219
+ */
220
+ export function createSecureConfig(data: any, masterPassword: string): { encrypted: string; salt: string } {
221
+ if (!data || !masterPassword) {
222
+ throw new Error('数据和主密码不能为空');
223
+ }
224
+
225
+ const salt = generateSalt();
226
+ const key = deriveKey(masterPassword, salt);
227
+ const encrypted = encryptObject(data, key);
228
+
229
+ return { encrypted, salt };
230
+ }
231
+
232
+ /**
233
+ * 解密配置对象
234
+ */
235
+ export function parseSecureConfig(encryptedData: string, salt: string, masterPassword: string): any {
236
+ if (!encryptedData || !salt || !masterPassword) {
237
+ throw new Error('加密数据、盐值和主密码不能为空');
238
+ }
239
+
240
+ const key = deriveKey(masterPassword, salt);
241
+ return decryptObject(encryptedData, key);
242
+ }
243
+
244
+ /**
245
+ * 检查密码强度并生成哈希
246
+ */
247
+ export function hashPassword(password: string, salt?: string): { hash: string; salt: string; valid: boolean } {
248
+ if (!password) {
249
+ return { hash: '', salt: '', valid: false };
250
+ }
251
+
252
+ const passwordSalt = salt || generateSalt();
253
+ const passwordHash = hash(password, passwordSalt);
254
+
255
+ return {
256
+ hash: passwordHash,
257
+ salt: passwordSalt,
258
+ valid: password.length >= 8
259
+ };
260
+ }
261
+
262
+ /**
263
+ * 验证密码
264
+ */
265
+ export function verifyPassword(password: string, hash: string, salt: string): boolean {
266
+ if (!password || !hash || !salt) {
267
+ return false;
268
+ }
269
+
270
+ return verifyHash(password, hash, salt);
271
+ }
@@ -0,0 +1,359 @@
1
+ /**
2
+ * 格式化工具类
3
+ * 提供各种数据格式化功能
4
+ */
5
+
6
+ /**
7
+ * 格式化文件大小
8
+ */
9
+ export function formatFileSize(bytes: number, decimals: number = 2): string {
10
+ if (bytes === 0) return '0 Bytes';
11
+
12
+ const k = 1024;
13
+ const dm = decimals < 0 ? 0 : decimals;
14
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
15
+
16
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
17
+
18
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
19
+ }
20
+
21
+ /**
22
+ * 格式化持续时间
23
+ */
24
+ export function formatDuration(milliseconds: number): string {
25
+ if (milliseconds < 1000) {
26
+ return `${milliseconds}ms`;
27
+ }
28
+
29
+ const seconds = Math.floor(milliseconds / 1000);
30
+ const minutes = Math.floor(seconds / 60);
31
+ const hours = Math.floor(minutes / 60);
32
+
33
+ if (hours > 0) {
34
+ return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
35
+ } else if (minutes > 0) {
36
+ return `${minutes}m ${seconds % 60}s`;
37
+ } else {
38
+ return `${seconds}s`;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * 格式化日期
44
+ */
45
+ export function formatDate(date: Date, format: 'short' | 'long' | 'relative' = 'short'): string {
46
+ if (!date) return '';
47
+
48
+ switch (format) {
49
+ case 'short':
50
+ return date.toLocaleDateString();
51
+ case 'long':
52
+ return date.toLocaleString();
53
+ case 'relative':
54
+ return formatRelativeTime(date);
55
+ default:
56
+ return date.toLocaleDateString();
57
+ }
58
+ }
59
+
60
+ /**
61
+ * 格式化相对时间
62
+ */
63
+ function formatRelativeTime(date: Date): string {
64
+ const now = new Date();
65
+ const diff = now.getTime() - date.getTime();
66
+
67
+ const seconds = Math.floor(diff / 1000);
68
+ const minutes = Math.floor(seconds / 60);
69
+ const hours = Math.floor(minutes / 60);
70
+ const days = Math.floor(hours / 24);
71
+
72
+ if (seconds < 60) {
73
+ return '刚刚';
74
+ } else if (minutes < 60) {
75
+ return `${minutes}分钟前`;
76
+ } else if (hours < 24) {
77
+ return `${hours}小时前`;
78
+ } else if (days < 7) {
79
+ return `${days}天前`;
80
+ } else {
81
+ return date.toLocaleDateString();
82
+ }
83
+ }
84
+
85
+ /**
86
+ * 格式化数字
87
+ */
88
+ export function formatNumber(num: number, decimals: number = 0): string {
89
+ if (isNaN(num)) return '0';
90
+
91
+ return num.toLocaleString(undefined, {
92
+ minimumFractionDigits: decimals,
93
+ maximumFractionDigits: decimals
94
+ });
95
+ }
96
+
97
+ /**
98
+ * 格式化百分比
99
+ */
100
+ export function formatPercentage(value: number, total: number, decimals: number = 1): string {
101
+ if (total === 0) return '0%';
102
+
103
+ const percentage = (value / total) * 100;
104
+ return `${percentage.toFixed(decimals)}%`;
105
+ }
106
+
107
+ /**
108
+ * 格式化令牌数量
109
+ */
110
+ export function formatTokens(tokens: number): string {
111
+ if (tokens < 1000) {
112
+ return `${tokens} tokens`;
113
+ } else if (tokens < 1000000) {
114
+ return `${(tokens / 1000).toFixed(1)}K tokens`;
115
+ } else {
116
+ return `${(tokens / 1000000).toFixed(1)}M tokens`;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * 格式化价格
122
+ */
123
+ export function formatPrice(amount: number, currency: string = 'USD'): string {
124
+ return new Intl.NumberFormat(undefined, {
125
+ style: 'currency',
126
+ currency
127
+ }).format(amount);
128
+ }
129
+
130
+ /**
131
+ * 格式化命令输出(添加语法高亮)
132
+ */
133
+ export function formatCommandOutput(output: string): string {
134
+ // 简单的语法高亮
135
+ return output
136
+ .replace(/(\b(?:error|ERROR|Error)\b)/g, '<span class="text-danger">$1</span>')
137
+ .replace(/(\b(?:warning|WARNING|Warning)\b)/g, '<span class="text-warning">$1</span>')
138
+ .replace(/(\b(?:success|SUCCESS|Success)\b)/g, '<span class="text-success">$1</span>')
139
+ .replace(/(\`[^\`]+\`)/g, '<code>$1</code>')
140
+ .replace(/\n/g, '<br>');
141
+ }
142
+
143
+ /**
144
+ * 格式化风险级别
145
+ */
146
+ export function formatRiskLevel(level: string): { text: string; class: string } {
147
+ switch (level.toLowerCase()) {
148
+ case 'low':
149
+ return { text: '低风险', class: 'text-success' };
150
+ case 'medium':
151
+ return { text: '中风险', class: 'text-warning' };
152
+ case 'high':
153
+ return { text: '高风险', class: 'text-danger' };
154
+ case 'critical':
155
+ return { text: '极风险', class: 'text-danger' };
156
+ default:
157
+ return { text: '未知', class: 'text-muted' };
158
+ }
159
+ }
160
+
161
+ /**
162
+ * 格式化置信度
163
+ */
164
+ export function formatConfidence(confidence: number): { text: string; class: string } {
165
+ const percentage = Math.round(confidence * 100);
166
+
167
+ if (percentage >= 90) {
168
+ return { text: `${percentage}% - 很高`, class: 'text-success' };
169
+ } else if (percentage >= 70) {
170
+ return { text: `${percentage}% - 高`, class: 'text-info' };
171
+ } else if (percentage >= 50) {
172
+ return { text: `${percentage}% - 中`, class: 'text-warning' };
173
+ } else {
174
+ return { text: `${percentage}% - 低`, class: 'text-danger' };
175
+ }
176
+ }
177
+
178
+ /**
179
+ * 截断文本
180
+ */
181
+ export function truncateText(text: string, maxLength: number, suffix: string = '...'): string {
182
+ if (!text || text.length <= maxLength) {
183
+ return text || '';
184
+ }
185
+
186
+ return text.substring(0, maxLength - suffix.length) + suffix;
187
+ }
188
+
189
+ /**
190
+ * 格式化错误信息
191
+ */
192
+ export function formatError(error: Error | string): string {
193
+ if (!error) return '未知错误';
194
+
195
+ if (typeof error === 'string') {
196
+ return error;
197
+ }
198
+
199
+ const message = error.message || '未知错误';
200
+ const stack = error.stack;
201
+
202
+ // 如果是API错误,尝试解析
203
+ if (message.includes('API') || message.includes('HTTP')) {
204
+ return `API错误: ${message}`;
205
+ }
206
+
207
+ return stack ? `${message}\n${stack}` : message;
208
+ }
209
+
210
+ /**
211
+ * 格式化字节数组
212
+ */
213
+ export function formatBytes(bytes: number[], separator: string = ' '): string {
214
+ return bytes.map(b => b.toString(16).padStart(2, '0')).join(separator);
215
+ }
216
+
217
+ /**
218
+ * 格式化JSON(美化输出)
219
+ */
220
+ export function formatJson(json: string | object, indent: number = 2): string {
221
+ try {
222
+ const obj = typeof json === 'string' ? JSON.parse(json) : json;
223
+ return JSON.stringify(obj, null, indent);
224
+ } catch {
225
+ return typeof json === 'string' ? json : String(json);
226
+ }
227
+ }
228
+
229
+ /**
230
+ * 格式化速度(bytes per second)
231
+ */
232
+ export function formatSpeed(bytesPerSecond: number): string {
233
+ if (bytesPerSecond < 1024) {
234
+ return `${bytesPerSecond.toFixed(2)} B/s`;
235
+ } else if (bytesPerSecond < 1024 * 1024) {
236
+ return `${(bytesPerSecond / 1024).toFixed(2)} KB/s`;
237
+ } else if (bytesPerSecond < 1024 * 1024 * 1024) {
238
+ return `${(bytesPerSecond / (1024 * 1024)).toFixed(2)} MB/s`;
239
+ } else {
240
+ return `${(bytesPerSecond / (1024 * 1024 * 1024)).toFixed(2)} GB/s`;
241
+ }
242
+ }
243
+
244
+ /**
245
+ * 格式化进度条
246
+ */
247
+ export function formatProgressBar(current: number, total: number, width: number = 20): string {
248
+ if (total === 0) return '[' + ' '.repeat(width) + '] 0%';
249
+
250
+ const percentage = current / total;
251
+ const filled = Math.round(percentage * width);
252
+ const empty = width - filled;
253
+
254
+ const bar = '█'.repeat(filled) + '░'.repeat(empty);
255
+ const percent = Math.round(percentage * 100);
256
+
257
+ return `[${bar}] ${percent}%`;
258
+ }
259
+
260
+ /**
261
+ * 格式化文件路径(显示简化版本)
262
+ */
263
+ export function formatFilePath(path: string, maxLength: number = 50): string {
264
+ if (!path || path.length <= maxLength) {
265
+ return path;
266
+ }
267
+
268
+ const parts = path.split(/[\\/]/);
269
+ if (parts.length <= 2) {
270
+ return '...' + path.substring(path.length - maxLength + 3);
271
+ }
272
+
273
+ const first = parts[0];
274
+ const last = parts[parts.length - 1];
275
+ const second = parts[1];
276
+
277
+ return `${first}/${second}/.../${last}`;
278
+ }
279
+
280
+ /**
281
+ * 格式化列表为文本
282
+ */
283
+ export function formatList(items: string[], delimiter: string = ', '): string {
284
+ if (!items || items.length === 0) {
285
+ return '';
286
+ }
287
+
288
+ if (items.length === 1) {
289
+ return items[0];
290
+ }
291
+
292
+ if (items.length === 2) {
293
+ return `${items[0]} 和 ${items[1]}`;
294
+ }
295
+
296
+ return items.slice(0, -1).join(delimiter) + '、以及 ' + items[items.length - 1];
297
+ }
298
+
299
+ /**
300
+ * 格式化代码块
301
+ */
302
+ export function formatCodeBlock(code: string, language: string = 'text'): string {
303
+ return '```' + language + '\n' + code + '\n```';
304
+ }
305
+
306
+ /**
307
+ * 转义HTML
308
+ */
309
+ export function escapeHtml(text: string): string {
310
+ const map: { [key: string]: string } = {
311
+ '&': '&amp;',
312
+ '<': '&lt;',
313
+ '>': '&gt;',
314
+ '"': '&quot;',
315
+ "'": '&#039;'
316
+ };
317
+
318
+ return text.replace(/[&<>"']/g, m => map[m]);
319
+ }
320
+
321
+ /**
322
+ * 清理文本(移除多余空白)
323
+ */
324
+ export function cleanText(text: string): string {
325
+ return text
326
+ .replace(/\s+/g, ' ')
327
+ .trim();
328
+ }
329
+
330
+ /**
331
+ * 驼峰命名格式化
332
+ */
333
+ export function toCamelCase(str: string): string {
334
+ return str
335
+ .replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => {
336
+ return index === 0 ? word.toLowerCase() : word.toUpperCase();
337
+ })
338
+ .replace(/\s+/g, '');
339
+ }
340
+
341
+ /**
342
+ * 短横线命名格式化
343
+ */
344
+ export function toKebabCase(str: string): string {
345
+ return str
346
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
347
+ .replace(/[\s_]+/g, '-')
348
+ .toLowerCase();
349
+ }
350
+
351
+ /**
352
+ * 下划线命名格式化
353
+ */
354
+ export function toSnakeCase(str: string): string {
355
+ return str
356
+ .replace(/([a-z])([A-Z])/g, '$1_$2')
357
+ .replace(/[\s-]+/g, '_')
358
+ .toLowerCase();
359
+ }