workplace-pua-cli 0.4.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/.env.example +4 -0
- package/.eslintrc.json +21 -0
- package/.prettierrc.json +9 -0
- package/CHANGELOG.md +107 -0
- package/README.md +240 -0
- package/bin/pua +2 -0
- package/dist/commands/chat.d.ts +15 -0
- package/dist/commands/chat.d.ts.map +1 -0
- package/dist/commands/chat.js +262 -0
- package/dist/commands/chat.js.map +1 -0
- package/dist/commands/config.d.ts +15 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +247 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/prompt.d.ts +14 -0
- package/dist/commands/prompt.d.ts.map +1 -0
- package/dist/commands/prompt.js +126 -0
- package/dist/commands/prompt.js.map +1 -0
- package/dist/config/providers.d.ts +37 -0
- package/dist/config/providers.d.ts.map +1 -0
- package/dist/config/providers.js +96 -0
- package/dist/config/providers.js.map +1 -0
- package/dist/config/session-storage.d.ts +29 -0
- package/dist/config/session-storage.d.ts.map +1 -0
- package/dist/config/session-storage.js +67 -0
- package/dist/config/session-storage.js.map +1 -0
- package/dist/config/settings.d.ts +55 -0
- package/dist/config/settings.d.ts.map +1 -0
- package/dist/config/settings.js +163 -0
- package/dist/config/settings.js.map +1 -0
- package/dist/config/storage.d.ts +69 -0
- package/dist/config/storage.d.ts.map +1 -0
- package/dist/config/storage.js +126 -0
- package/dist/config/storage.js.map +1 -0
- package/dist/history/session.d.ts +52 -0
- package/dist/history/session.d.ts.map +1 -0
- package/dist/history/session.js +122 -0
- package/dist/history/session.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +157 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/base.d.ts +38 -0
- package/dist/llm/base.d.ts.map +1 -0
- package/dist/llm/base.js +22 -0
- package/dist/llm/base.js.map +1 -0
- package/dist/llm/factory.d.ts +12 -0
- package/dist/llm/factory.d.ts.map +1 -0
- package/dist/llm/factory.js +26 -0
- package/dist/llm/factory.js.map +1 -0
- package/dist/llm/openai.d.ts +10 -0
- package/dist/llm/openai.d.ts.map +1 -0
- package/dist/llm/openai.js +97 -0
- package/dist/llm/openai.js.map +1 -0
- package/dist/llm/zhipu.d.ts +10 -0
- package/dist/llm/zhipu.d.ts.map +1 -0
- package/dist/llm/zhipu.js +91 -0
- package/dist/llm/zhipu.js.map +1 -0
- package/dist/prompts/boss.d.ts +6 -0
- package/dist/prompts/boss.d.ts.map +1 -0
- package/dist/prompts/boss.js +41 -0
- package/dist/prompts/boss.js.map +1 -0
- package/dist/prompts/employee.d.ts +6 -0
- package/dist/prompts/employee.d.ts.map +1 -0
- package/dist/prompts/employee.js +41 -0
- package/dist/prompts/employee.js.map +1 -0
- package/dist/prompts/index.d.ts +4 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +9 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/utils/formatter.d.ts +25 -0
- package/dist/utils/formatter.d.ts.map +1 -0
- package/dist/utils/formatter.js +83 -0
- package/dist/utils/formatter.js.map +1 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +31 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/stream.d.ts +36 -0
- package/dist/utils/stream.d.ts.map +1 -0
- package/dist/utils/stream.js +74 -0
- package/dist/utils/stream.js.map +1 -0
- package/docs/OPTIMIZATION.md +772 -0
- package/docs/TECHNICAL_PRINCIPLES.md +663 -0
- package/package.json +52 -0
- package/sample/1.png +0 -0
- package/sample/2.png +0 -0
- package/screenshots/chat-dialogue.png +0 -0
- package/screenshots/chat-mode.png +0 -0
- package/src/__tests__/config/settings.test.ts +48 -0
- package/src/__tests__/prompts/boss.test.ts +35 -0
- package/src/commands/chat.ts +328 -0
- package/src/commands/config.ts +283 -0
- package/src/commands/prompt.ts +154 -0
- package/src/config/providers.ts +109 -0
- package/src/config/session-storage.ts +94 -0
- package/src/config/settings.ts +194 -0
- package/src/config/storage.ts +150 -0
- package/src/history/session.ts +141 -0
- package/src/index.ts +164 -0
- package/src/llm/base.ts +55 -0
- package/src/llm/factory.ts +24 -0
- package/src/llm/openai.ts +113 -0
- package/src/llm/zhipu.ts +101 -0
- package/src/prompts/boss.ts +43 -0
- package/src/prompts/employee.ts +43 -0
- package/src/prompts/index.ts +3 -0
- package/src/utils/formatter.ts +104 -0
- package/src/utils/logger.ts +31 -0
- package/src/utils/stream.ts +76 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +18 -0
package/sample/2.png
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { loadConfig, DEFAULTS, type ProviderType } from '../config/settings';
|
|
3
|
+
|
|
4
|
+
describe('config/settings', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
// 清除环境变量
|
|
7
|
+
delete process.env.ZHIPUAI_API_KEY;
|
|
8
|
+
delete process.env.OPENAI_API_KEY;
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('loadConfig', () => {
|
|
12
|
+
it('should return defaults when no config exists', () => {
|
|
13
|
+
const config = loadConfig({});
|
|
14
|
+
|
|
15
|
+
expect(config.provider).toBe(DEFAULTS.provider);
|
|
16
|
+
expect(config.model).toBe(DEFAULTS.model);
|
|
17
|
+
expect(config.role).toBe(DEFAULTS.role);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should prioritize CLI options over env vars', () => {
|
|
21
|
+
process.env.ZHIPUAI_API_KEY = 'env-key';
|
|
22
|
+
|
|
23
|
+
const config = loadConfig({
|
|
24
|
+
apiKey: 'cli-key',
|
|
25
|
+
provider: 'zhipu' as ProviderType
|
|
26
|
+
} as any);
|
|
27
|
+
|
|
28
|
+
expect(config.apiKey).toBe('cli-key');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should load from environment variables', () => {
|
|
32
|
+
process.env.ZHIPUAI_API_KEY = 'test-key';
|
|
33
|
+
|
|
34
|
+
const config = loadConfig({});
|
|
35
|
+
|
|
36
|
+
expect(config.apiKey).toBe('test-key');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('DEFAULTS', () => {
|
|
41
|
+
it('should have correct default values', () => {
|
|
42
|
+
expect(DEFAULTS.provider).toBe('zhipu');
|
|
43
|
+
expect(DEFAULTS.model).toBe('glm-4.7');
|
|
44
|
+
expect(DEFAULTS.role).toBe('boss');
|
|
45
|
+
expect(DEFAULTS.severity).toBe('medium');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { getBossSystemMessage } from '../prompts/boss';
|
|
3
|
+
|
|
4
|
+
describe('prompts/boss', () => {
|
|
5
|
+
describe('getBossSystemMessage', () => {
|
|
6
|
+
it('should return mild template', () => {
|
|
7
|
+
const prompt = getBossSystemMessage('mild');
|
|
8
|
+
|
|
9
|
+
expect(prompt).toContain('喜欢说教但不严厉');
|
|
10
|
+
expect(prompt).toContain('为你好');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should return medium template', () => {
|
|
14
|
+
const prompt = getBossSystemMessage('medium');
|
|
15
|
+
|
|
16
|
+
expect(prompt).toContain('典型职场 PUA 老板');
|
|
17
|
+
expect(prompt).toContain('要对齐');
|
|
18
|
+
expect(prompt).toContain('闭环');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should return extreme template', () => {
|
|
22
|
+
const prompt = getBossSystemMessage('extreme');
|
|
23
|
+
|
|
24
|
+
expect(prompt).toContain('极度挑剔');
|
|
25
|
+
expect(prompt).toContain('刻薄');
|
|
26
|
+
expect(prompt).toContain('巨婴');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should contain role-specific keywords', () => {
|
|
30
|
+
const prompt = getBossSystemMessage('medium');
|
|
31
|
+
|
|
32
|
+
expect(prompt).toMatch(/年轻人|锻炼|学习|提升/);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import type { Ora } from 'ora';
|
|
5
|
+
import { Message } from '../llm/base';
|
|
6
|
+
import { getBossSystemMessage, getEmployeeSystemMessage } from '../prompts';
|
|
7
|
+
import { createLLM } from '../llm/factory';
|
|
8
|
+
import { getProviderBaseUrl } from '../config/settings';
|
|
9
|
+
import { StreamPrinter } from '../utils/stream';
|
|
10
|
+
import { sessionManager } from '../history/session';
|
|
11
|
+
import { SessionStorage, type SessionData } from '../config/session-storage';
|
|
12
|
+
import { logger } from '../utils/logger';
|
|
13
|
+
import { type ProviderType } from '../config/providers';
|
|
14
|
+
|
|
15
|
+
export interface ChatOptions {
|
|
16
|
+
apiKey: string;
|
|
17
|
+
provider: ProviderType;
|
|
18
|
+
model: string;
|
|
19
|
+
role: 'boss' | 'employee';
|
|
20
|
+
severity: 'mild' | 'medium' | 'extreme';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 全局会话存储实例
|
|
24
|
+
const sessionStorage = new SessionStorage();
|
|
25
|
+
|
|
26
|
+
// 扩展 readline 接口以支持动态属性
|
|
27
|
+
declare module 'readline' {
|
|
28
|
+
interface Interface {
|
|
29
|
+
[key: string]: any;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function chatCommand(options: ChatOptions): Promise<void> {
|
|
34
|
+
// Create session
|
|
35
|
+
const sessionId = `session-${Date.now()}`;
|
|
36
|
+
sessionManager.createSession(sessionId);
|
|
37
|
+
|
|
38
|
+
// Create readline interface first
|
|
39
|
+
const rl = readline.createInterface({
|
|
40
|
+
input: process.stdin,
|
|
41
|
+
output: process.stdout,
|
|
42
|
+
prompt: chalk.green('❯ ')
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// 保存元数据到 rl 以便在命令处理中使用
|
|
46
|
+
rl['role'] = options.role;
|
|
47
|
+
rl['severity'] = options.severity;
|
|
48
|
+
rl['provider'] = options.provider;
|
|
49
|
+
rl['model'] = options.model;
|
|
50
|
+
|
|
51
|
+
// Set up system message
|
|
52
|
+
const systemMessage =
|
|
53
|
+
options.role === 'boss'
|
|
54
|
+
? getBossSystemMessage(options.severity)
|
|
55
|
+
: getEmployeeSystemMessage(options.severity);
|
|
56
|
+
|
|
57
|
+
sessionManager.addMessage({ role: 'system', content: systemMessage });
|
|
58
|
+
|
|
59
|
+
// Create LLM instance
|
|
60
|
+
const llm = createLLM(options.provider, {
|
|
61
|
+
apiKey: options.apiKey,
|
|
62
|
+
model: options.model,
|
|
63
|
+
baseUrl: getProviderBaseUrl(options.provider),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const printer = new StreamPrinter(
|
|
67
|
+
options.role === 'boss' ? chalk.red : chalk.yellow
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Print welcome message
|
|
71
|
+
const roleLabel = options.role === 'boss' ? '老板模式' : '员工模式';
|
|
72
|
+
const roleEmoji = options.role === 'boss' ? '👔' : '👤';
|
|
73
|
+
const severityLabel = {
|
|
74
|
+
mild: '温和',
|
|
75
|
+
medium: '标准',
|
|
76
|
+
extreme: '极端'
|
|
77
|
+
}[options.severity];
|
|
78
|
+
|
|
79
|
+
console.log();
|
|
80
|
+
console.log(chalk.cyan('╔═══════════════════════════════════════════════════════════╗'));
|
|
81
|
+
console.log(chalk.cyan('║') + chalk.bold.white(` ${roleEmoji} PUA CLI - ${roleLabel} `) + chalk.cyan('║'));
|
|
82
|
+
console.log(chalk.cyan('║') + ` Provider: ${chalk.gray(options.provider)} 强度: ${chalk.gray(severityLabel)} ` + chalk.cyan('║'));
|
|
83
|
+
console.log(chalk.cyan('║') + ` 模型: ${chalk.gray(options.model)} ` + chalk.cyan('║'));
|
|
84
|
+
console.log(chalk.cyan('╚═══════════════════════════════════════════════════════════╝'));
|
|
85
|
+
console.log();
|
|
86
|
+
console.log(chalk.gray('输入 /help 查看可用命令,输入 /exit 退出'));
|
|
87
|
+
console.log();
|
|
88
|
+
|
|
89
|
+
rl.prompt();
|
|
90
|
+
|
|
91
|
+
rl.on('line', async (input) => {
|
|
92
|
+
const trimmedInput = input.trim();
|
|
93
|
+
|
|
94
|
+
// Handle commands
|
|
95
|
+
if (trimmedInput.startsWith('/')) {
|
|
96
|
+
await handleCommand(trimmedInput, rl, printer);
|
|
97
|
+
rl.prompt();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!trimmedInput) {
|
|
102
|
+
rl.prompt();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Add user message to session
|
|
107
|
+
sessionManager.addMessage({ role: 'user', content: trimmedInput });
|
|
108
|
+
|
|
109
|
+
// Print user input
|
|
110
|
+
printer.printUserInput(trimmedInput);
|
|
111
|
+
|
|
112
|
+
// Call LLM
|
|
113
|
+
let spinner: any = null;
|
|
114
|
+
let hasStarted = false;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
printer.printResponseHeader(options.role);
|
|
118
|
+
|
|
119
|
+
const messages = sessionManager.getMessages();
|
|
120
|
+
|
|
121
|
+
await llm.chatStream(messages, (chunk) => {
|
|
122
|
+
if (!hasStarted) {
|
|
123
|
+
hasStarted = true;
|
|
124
|
+
if (spinner) {
|
|
125
|
+
spinner.stop();
|
|
126
|
+
spinner = null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
printer.printChunk(chunk);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
printer.printResponseFooter();
|
|
133
|
+
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (spinner) spinner.stop();
|
|
136
|
+
printer.printError(error instanceof Error ? error.message : String(error));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
rl.prompt();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
rl.on('close', () => {
|
|
143
|
+
console.log();
|
|
144
|
+
logger.info('再见!');
|
|
145
|
+
process.exit(0);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function handleCommand(
|
|
150
|
+
command: string,
|
|
151
|
+
rl: readline.Interface,
|
|
152
|
+
printer: StreamPrinter
|
|
153
|
+
): Promise<void> {
|
|
154
|
+
const [cmd, ...args] = command.split(' ');
|
|
155
|
+
|
|
156
|
+
switch (cmd) {
|
|
157
|
+
case '/help':
|
|
158
|
+
printHelp();
|
|
159
|
+
break;
|
|
160
|
+
|
|
161
|
+
case '/exit':
|
|
162
|
+
case '/quit':
|
|
163
|
+
case '/q':
|
|
164
|
+
console.log();
|
|
165
|
+
logger.info('再见!');
|
|
166
|
+
rl.close();
|
|
167
|
+
process.exit(0);
|
|
168
|
+
break;
|
|
169
|
+
|
|
170
|
+
case '/clear':
|
|
171
|
+
sessionManager.clearCurrentSession();
|
|
172
|
+
logger.success('会话历史已清空');
|
|
173
|
+
break;
|
|
174
|
+
|
|
175
|
+
case '/history':
|
|
176
|
+
console.log(sessionManager.getFormattedHistory());
|
|
177
|
+
break;
|
|
178
|
+
|
|
179
|
+
case '/info':
|
|
180
|
+
console.log(sessionManager.getSessionInfo());
|
|
181
|
+
break;
|
|
182
|
+
|
|
183
|
+
case '/save':
|
|
184
|
+
await handleSaveCommand(args, rl);
|
|
185
|
+
break;
|
|
186
|
+
|
|
187
|
+
case '/sessions':
|
|
188
|
+
await handleSessionsCommand(rl);
|
|
189
|
+
break;
|
|
190
|
+
|
|
191
|
+
case '/load':
|
|
192
|
+
await handleLoadCommand(args, rl);
|
|
193
|
+
break;
|
|
194
|
+
|
|
195
|
+
default:
|
|
196
|
+
logger.warning(`未知命令: ${cmd}`);
|
|
197
|
+
console.log(chalk.gray('输入 /help 查看可用命令'));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function printHelp(): void {
|
|
202
|
+
console.log();
|
|
203
|
+
console.log(chalk.bold('可用命令:'));
|
|
204
|
+
console.log(chalk.gray('─').repeat(50));
|
|
205
|
+
console.log(' /help 显示此帮助信息');
|
|
206
|
+
console.log(' /clear 清空当前会话历史');
|
|
207
|
+
console.log(' /history 显示会话历史记录');
|
|
208
|
+
console.log(' /info 显示会话统计信息');
|
|
209
|
+
console.log(' /save [名称] 保存当前会话');
|
|
210
|
+
console.log(' /sessions 列出所有已保存会话');
|
|
211
|
+
console.log(' /load <ID> 加载指定会话');
|
|
212
|
+
console.log(' /exit 退出程序');
|
|
213
|
+
console.log();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function handleSaveCommand(args: string[], rl: readline.Interface): Promise<void> {
|
|
217
|
+
const sessionName = args.join(' ') || '未命名会话';
|
|
218
|
+
const currentMessages = sessionManager.getMessages();
|
|
219
|
+
|
|
220
|
+
const spinner = ora('保存会话中...').start();
|
|
221
|
+
try {
|
|
222
|
+
const savedSession = sessionStorage.saveSession({
|
|
223
|
+
name: sessionName,
|
|
224
|
+
description: `包含 ${currentMessages.length} 条消息`,
|
|
225
|
+
messages: currentMessages,
|
|
226
|
+
metadata: {
|
|
227
|
+
role: rl['role'] || 'boss',
|
|
228
|
+
severity: rl['severity'] || 'medium',
|
|
229
|
+
provider: rl['provider'] || 'zhipu',
|
|
230
|
+
model: rl['model'] || 'glm-4.7'
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
spinner.stop();
|
|
235
|
+
logger.success(`会话已保存: ${savedSession.name} (ID: ${savedSession.id})`);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
spinner.stop();
|
|
238
|
+
logger.error(`保存失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
rl.prompt();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function handleSessionsCommand(rl: readline.Interface): Promise<void> {
|
|
245
|
+
const spinner = ora('加载会话列表...').start();
|
|
246
|
+
try {
|
|
247
|
+
const sessions = sessionStorage.listSessions();
|
|
248
|
+
spinner.stop();
|
|
249
|
+
|
|
250
|
+
if (sessions.length === 0) {
|
|
251
|
+
logger.info('暂无已保存的会话');
|
|
252
|
+
rl.prompt();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
console.log();
|
|
257
|
+
console.log(chalk.bold('已保存的会话:'));
|
|
258
|
+
console.log(chalk.gray('─').repeat(60));
|
|
259
|
+
|
|
260
|
+
for (const session of sessions) {
|
|
261
|
+
const messageCount = session.messages?.length || 0;
|
|
262
|
+
const timeAgo = getTimeAgo(session.updatedAt);
|
|
263
|
+
|
|
264
|
+
console.log(` ${chalk.cyan(session.id.padEnd(12))} ${chalk.white(session.name.padEnd(20))} ${chalk.gray(`(${messageCount} 条消息, ${timeAgo})`)}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
console.log();
|
|
268
|
+
logger.info('使用 /load <ID> 加载会话');
|
|
269
|
+
rl.prompt();
|
|
270
|
+
} catch (error) {
|
|
271
|
+
spinner.stop();
|
|
272
|
+
logger.error(`加载失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
273
|
+
rl.prompt();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function handleLoadCommand(args: string[], rl: readline.Interface): Promise<void> {
|
|
278
|
+
const sessionId = args[0];
|
|
279
|
+
|
|
280
|
+
if (!sessionId) {
|
|
281
|
+
logger.error('请指定会话 ID');
|
|
282
|
+
console.log(chalk.gray('使用 /sessions 查看所有会话'));
|
|
283
|
+
rl.prompt();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const spinner = ora('加载会话中...').start();
|
|
288
|
+
try {
|
|
289
|
+
const session = sessionStorage.loadSession(sessionId);
|
|
290
|
+
|
|
291
|
+
if (!session) {
|
|
292
|
+
spinner.stop();
|
|
293
|
+
logger.error(`未找到会话: ${sessionId}`);
|
|
294
|
+
rl.prompt();
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
spinner.stop();
|
|
299
|
+
|
|
300
|
+
// 加载会话消息
|
|
301
|
+
sessionManager.clearCurrentSession();
|
|
302
|
+
for (const msg of session.messages || []) {
|
|
303
|
+
sessionManager.addMessage({ role: msg.role as any, content: msg.content });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
logger.success(`已加载会话: ${session.name}`);
|
|
307
|
+
console.log();
|
|
308
|
+
console.log(chalk.gray(`会话包含 ${session.messages?.length || 0} 条消息`));
|
|
309
|
+
console.log();
|
|
310
|
+
|
|
311
|
+
rl.prompt();
|
|
312
|
+
} catch (error) {
|
|
313
|
+
spinner.stop();
|
|
314
|
+
logger.error(`加载失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
315
|
+
rl.prompt();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function getTimeAgo(timestamp: string): string {
|
|
320
|
+
const now = Date.now();
|
|
321
|
+
const past = new Date(timestamp).getTime();
|
|
322
|
+
const diff = Math.floor((now - past) / 1000);
|
|
323
|
+
|
|
324
|
+
if (diff < 60) return `${diff} 秒前`;
|
|
325
|
+
if (diff < 3600) return `${Math.floor(diff / 60)} 分钟前`;
|
|
326
|
+
if (diff < 86400) return `${Math.floor(diff / 3600)} 小时前`;
|
|
327
|
+
return `${Math.floor(diff / 86400)} 天前`;
|
|
328
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import input from '@inquirer/input';
|
|
3
|
+
import select from '@inquirer/select';
|
|
4
|
+
import confirm from '@inquirer/confirm';
|
|
5
|
+
import { saveGlobalConfig, loadGlobalConfig, ensureConfigDir, type GlobalConfig } from '../config/storage';
|
|
6
|
+
import { PROVIDERS, getProvider, validateApiKey, validateBaseUrl, type ProviderType } from '../config/providers';
|
|
7
|
+
import { logger } from '../utils/logger';
|
|
8
|
+
|
|
9
|
+
export interface ConfigWizardOptions {
|
|
10
|
+
autoMode?: boolean;
|
|
11
|
+
initialProvider?: ProviderType;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Run the configuration wizard
|
|
16
|
+
*/
|
|
17
|
+
export async function configWizard(options: ConfigWizardOptions = {}): Promise<GlobalConfig> {
|
|
18
|
+
const { autoMode = false, initialProvider } = options;
|
|
19
|
+
|
|
20
|
+
// Show welcome message
|
|
21
|
+
if (!autoMode) {
|
|
22
|
+
console.log();
|
|
23
|
+
console.log(chalk.cyan.bold('╔═══════════════════════════════════════════════════════════╗'));
|
|
24
|
+
console.log(chalk.cyan.bold('║') + ' ' + chalk.white.bold('PUA CLI 配置向导') + ' ' + chalk.cyan.bold('║'));
|
|
25
|
+
console.log(chalk.cyan.bold('╚═══════════════════════════════════════════════════════════╝'));
|
|
26
|
+
console.log();
|
|
27
|
+
console.log(chalk.gray('欢迎使用 PUA CLI!让我们先配置一些基本信息。'));
|
|
28
|
+
console.log();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Load existing config if available
|
|
32
|
+
let existingConfig = loadGlobalConfig();
|
|
33
|
+
|
|
34
|
+
// Step 1: Select Provider
|
|
35
|
+
const providerAnswer = await select({
|
|
36
|
+
message: '选择 AI 服务提供商:',
|
|
37
|
+
choices: [
|
|
38
|
+
{
|
|
39
|
+
name: `${chalk.green('1.')} 智谱 AI ${chalk.gray('(国产,推荐)')}`,
|
|
40
|
+
value: 'zhipu',
|
|
41
|
+
description: '国产大模型,稳定可靠,响应快速',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: `${chalk.blue('2.')} OpenAI ${chalk.gray('(GPT-4o)')}`,
|
|
45
|
+
value: 'openai',
|
|
46
|
+
description: '国际通用,支持 GPT-4o、GPT-4o-mini 等模型',
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
default: existingConfig?.currentProvider || initialProvider || 'zhipu',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const provider = providerAnswer as ProviderType;
|
|
53
|
+
const providerDef = getProvider(provider)!;
|
|
54
|
+
|
|
55
|
+
console.log();
|
|
56
|
+
console.log(chalk.gray(`已选择: ${chalk.white.bold(providerDef.name)}`));
|
|
57
|
+
console.log();
|
|
58
|
+
|
|
59
|
+
// Step 2: Input API Key
|
|
60
|
+
let apiKey = '';
|
|
61
|
+
let apiKeyValid = false;
|
|
62
|
+
|
|
63
|
+
while (!apiKeyValid) {
|
|
64
|
+
apiKey = await input({
|
|
65
|
+
message: `请输入 ${providerDef.name} API Key:`,
|
|
66
|
+
default: existingConfig?.providers[provider]?.apiKey || '',
|
|
67
|
+
validate: (value: string) => {
|
|
68
|
+
if (!value || value.trim().length === 0) {
|
|
69
|
+
return 'API Key 不能为空';
|
|
70
|
+
}
|
|
71
|
+
const validation = validateApiKey(provider, value);
|
|
72
|
+
if (!validation.valid) {
|
|
73
|
+
return validation.error || 'API Key 无效';
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const validation = validateApiKey(provider, apiKey);
|
|
80
|
+
apiKeyValid = validation.valid;
|
|
81
|
+
|
|
82
|
+
if (!apiKeyValid) {
|
|
83
|
+
console.log(chalk.red(validation.error || 'API Key 无效,请重新输入'));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log();
|
|
88
|
+
console.log(chalk.green('✓ API Key 已设置'));
|
|
89
|
+
console.log();
|
|
90
|
+
|
|
91
|
+
// Step 3: Input Base URL (optional)
|
|
92
|
+
const useCustomUrl = await confirm({
|
|
93
|
+
message: '是否使用自定义 Base URL(用于代理)?',
|
|
94
|
+
default: false,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
let baseUrl = providerDef.defaultBaseUrl;
|
|
98
|
+
|
|
99
|
+
if (useCustomUrl) {
|
|
100
|
+
baseUrl = await input({
|
|
101
|
+
message: '请输入 Base URL:',
|
|
102
|
+
default: existingConfig?.providers[provider]?.baseUrl || providerDef.defaultBaseUrl,
|
|
103
|
+
validate: (value: string) => {
|
|
104
|
+
const validation = validateBaseUrl(value);
|
|
105
|
+
if (!validation.valid) {
|
|
106
|
+
return validation.error || 'Base URL 无效';
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log();
|
|
114
|
+
|
|
115
|
+
// Step 4: Select Default Model
|
|
116
|
+
const model = await select({
|
|
117
|
+
message: '选择默认模型:',
|
|
118
|
+
choices: providerDef.defaultModels.map((m) => ({
|
|
119
|
+
name: m,
|
|
120
|
+
value: m,
|
|
121
|
+
})),
|
|
122
|
+
default: existingConfig?.defaults.model || providerDef.defaultModels[0],
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
console.log();
|
|
126
|
+
console.log(chalk.gray(`已选择: ${chalk.white(model)}`));
|
|
127
|
+
console.log();
|
|
128
|
+
|
|
129
|
+
// Step 5: Select Default Role
|
|
130
|
+
const role = await select({
|
|
131
|
+
message: '选择默认角色:',
|
|
132
|
+
choices: [
|
|
133
|
+
{ name: '老板模式 (PUA 别人)', value: 'boss' },
|
|
134
|
+
{ name: '员工模式 (被 PUA)', value: 'employee' },
|
|
135
|
+
],
|
|
136
|
+
default: existingConfig?.defaults.role || 'boss',
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
console.log();
|
|
140
|
+
console.log(chalk.gray(`已选择: ${chalk.white(role === 'boss' ? '老板模式' : '员工模式')}`));
|
|
141
|
+
console.log();
|
|
142
|
+
|
|
143
|
+
// Step 6: Select Default Severity
|
|
144
|
+
const severity = await select({
|
|
145
|
+
message: '选择 PUA 强度:',
|
|
146
|
+
choices: [
|
|
147
|
+
{ name: '温和 (mild)', value: 'mild' },
|
|
148
|
+
{ name: '标准 (medium)', value: 'medium' },
|
|
149
|
+
{ name: '极端 (extreme)', value: 'extreme' },
|
|
150
|
+
],
|
|
151
|
+
default: existingConfig?.defaults.severity || 'medium',
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
console.log();
|
|
155
|
+
console.log(chalk.gray(`已选择: ${chalk.white(severity)}`));
|
|
156
|
+
console.log();
|
|
157
|
+
|
|
158
|
+
// Step 7: Confirm and Save
|
|
159
|
+
const shouldSave = await confirm({
|
|
160
|
+
message: '确认保存配置?',
|
|
161
|
+
default: true,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (!shouldSave) {
|
|
165
|
+
console.log();
|
|
166
|
+
logger.warning('配置已取消');
|
|
167
|
+
throw new Error('配置已取消');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Build config object
|
|
171
|
+
const newConfig: GlobalConfig = {
|
|
172
|
+
currentProvider: provider,
|
|
173
|
+
providers: {
|
|
174
|
+
...existingConfig?.providers,
|
|
175
|
+
[provider]: {
|
|
176
|
+
apiKey,
|
|
177
|
+
baseUrl,
|
|
178
|
+
models: providerDef.defaultModels,
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
defaults: {
|
|
182
|
+
provider,
|
|
183
|
+
model,
|
|
184
|
+
role: role as 'boss' | 'employee',
|
|
185
|
+
severity: severity as 'mild' | 'medium' | 'extreme',
|
|
186
|
+
},
|
|
187
|
+
onboardingCompleted: true,
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Save config
|
|
191
|
+
ensureConfigDir();
|
|
192
|
+
saveGlobalConfig(newConfig);
|
|
193
|
+
|
|
194
|
+
console.log();
|
|
195
|
+
console.log(chalk.green('✓ 配置已保存'));
|
|
196
|
+
console.log();
|
|
197
|
+
console.log(chalk.gray('配置文件位置:'), chalk.white(getConfigPath()));
|
|
198
|
+
console.log();
|
|
199
|
+
|
|
200
|
+
if (!autoMode) {
|
|
201
|
+
console.log(chalk.cyan('─────────────────────────────────────────────────────────────'));
|
|
202
|
+
console.log();
|
|
203
|
+
console.log(chalk.white.bold('配置完成!现在你可以开始使用 PUA CLI 了:'));
|
|
204
|
+
console.log();
|
|
205
|
+
console.log(' ' + chalk.white('pua chat --role boss') + chalk.gray(' # 启动老板模式'));
|
|
206
|
+
console.log(' ' + chalk.white('pua prompt --role boss "你好"') + chalk.gray(' # 单次提示'));
|
|
207
|
+
console.log();
|
|
208
|
+
console.log(chalk.gray('更多选项请运行: pua --help'));
|
|
209
|
+
console.log();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return newConfig;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get the config file path for display
|
|
217
|
+
*/
|
|
218
|
+
function getConfigPath(): string {
|
|
219
|
+
const platform = process.platform;
|
|
220
|
+
|
|
221
|
+
if (platform === 'win32') {
|
|
222
|
+
return '%APPDATA%\\pua-cli\\config.json';
|
|
223
|
+
} else {
|
|
224
|
+
return '~/.config/pua-cli/config.json';
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Show current configuration
|
|
230
|
+
*/
|
|
231
|
+
export async function showConfig(): Promise<void> {
|
|
232
|
+
const config = loadGlobalConfig();
|
|
233
|
+
|
|
234
|
+
if (!config) {
|
|
235
|
+
console.log();
|
|
236
|
+
logger.warning('未找到配置文件');
|
|
237
|
+
console.log(chalk.gray('运行 ') + chalk.white('pua config') + chalk.gray(' 来配置 PUA CLI'));
|
|
238
|
+
console.log();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
console.log();
|
|
243
|
+
console.log(chalk.cyan.bold('当前配置:'));
|
|
244
|
+
console.log(chalk.gray('─').repeat(50));
|
|
245
|
+
console.log();
|
|
246
|
+
|
|
247
|
+
const providerDef = getProvider(config.currentProvider);
|
|
248
|
+
console.log(chalk.bold('当前 Provider:'), chalk.white(providerDef?.name || config.currentProvider));
|
|
249
|
+
console.log();
|
|
250
|
+
|
|
251
|
+
console.log(chalk.bold('已配置的 Providers:'));
|
|
252
|
+
for (const [id, providerConfig] of Object.entries(config.providers)) {
|
|
253
|
+
const p = getProvider(id as any);
|
|
254
|
+
const isCurrent = id === config.currentProvider;
|
|
255
|
+
const prefix = isCurrent ? chalk.green('→') : ' ';
|
|
256
|
+
|
|
257
|
+
console.log(` ${prefix} ${chalk.white(p?.name || id)}`);
|
|
258
|
+
console.log(` API Key: ${chalk.gray(maskApiKey(providerConfig.apiKey))}`);
|
|
259
|
+
if (providerConfig.baseUrl) {
|
|
260
|
+
console.log(` Base URL: ${chalk.gray(providerConfig.baseUrl)}`);
|
|
261
|
+
}
|
|
262
|
+
console.log();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
console.log(chalk.bold('默认设置:'));
|
|
266
|
+
console.log(` 模型: ${chalk.white(config.defaults.model)}`);
|
|
267
|
+
console.log(` 角色: ${chalk.white(config.defaults.role)}`);
|
|
268
|
+
console.log(` 强度: ${chalk.white(config.defaults.severity)}`);
|
|
269
|
+
console.log();
|
|
270
|
+
|
|
271
|
+
console.log(chalk.gray('配置文件:'), chalk.white(getConfigPath()));
|
|
272
|
+
console.log();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Mask API key for display
|
|
277
|
+
*/
|
|
278
|
+
function maskApiKey(apiKey: string): string {
|
|
279
|
+
if (apiKey.length <= 8) {
|
|
280
|
+
return '*'.repeat(apiKey.length);
|
|
281
|
+
}
|
|
282
|
+
return apiKey.slice(0, 4) + '*'.repeat(apiKey.length - 8) + apiKey.slice(-4);
|
|
283
|
+
}
|