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
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { Message } from '../llm/base';
|
|
4
|
+
import { getBossSystemMessage, getEmployeeSystemMessage } from '../prompts';
|
|
5
|
+
import { createLLM } from '../llm/factory';
|
|
6
|
+
import { getProviderBaseUrl } from '../config/settings';
|
|
7
|
+
import { StreamPrinter } from '../utils/stream';
|
|
8
|
+
import { OutputFormatter, type OutputFormat } from '../utils/formatter';
|
|
9
|
+
import { logger } from '../utils/logger';
|
|
10
|
+
import { type ProviderType } from '../config/providers';
|
|
11
|
+
|
|
12
|
+
export interface PromptOptions {
|
|
13
|
+
apiKey: string;
|
|
14
|
+
provider: ProviderType;
|
|
15
|
+
model: string;
|
|
16
|
+
role: 'boss' | 'employee';
|
|
17
|
+
severity: 'mild' | 'medium' | 'extreme';
|
|
18
|
+
input?: string;
|
|
19
|
+
format?: OutputFormat;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function promptCommand(options: PromptOptions): Promise<void> {
|
|
23
|
+
// Get input from argument or stdin
|
|
24
|
+
let userInput = options.input;
|
|
25
|
+
|
|
26
|
+
if (!userInput) {
|
|
27
|
+
// Read from stdin if available (pipe mode)
|
|
28
|
+
if (!process.stdin.isTTY) {
|
|
29
|
+
userInput = await readFromStdin();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!userInput) {
|
|
34
|
+
logger.error('请提供输入内容');
|
|
35
|
+
console.log(chalk.gray('用法: pua prompt --role boss "你的问题"'));
|
|
36
|
+
console.log(chalk.gray('或者: echo "你的问题" | pua prompt --role boss'));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Set up system message
|
|
41
|
+
const systemMessage =
|
|
42
|
+
options.role === 'boss'
|
|
43
|
+
? getBossSystemMessage(options.severity)
|
|
44
|
+
: getEmployeeSystemMessage(options.severity);
|
|
45
|
+
|
|
46
|
+
const messages: Message[] = [
|
|
47
|
+
{ role: 'system', content: systemMessage },
|
|
48
|
+
{ role: 'user', content: userInput }
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
// Create LLM instance
|
|
52
|
+
const llm = createLLM(options.provider, {
|
|
53
|
+
apiKey: options.apiKey,
|
|
54
|
+
model: options.model,
|
|
55
|
+
baseUrl: getProviderBaseUrl(options.provider),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const printer = new StreamPrinter(
|
|
59
|
+
options.role === 'boss' ? chalk.red : chalk.yellow
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const formatter = new OutputFormatter(options.format);
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const roleLabel = options.role === 'boss' ? '老板' : '员工';
|
|
66
|
+
console.log();
|
|
67
|
+
console.log(chalk.gray(`┌─ ${roleLabel} ─────────────────────────────`));
|
|
68
|
+
|
|
69
|
+
let fullResponse = '';
|
|
70
|
+
|
|
71
|
+
await llm.chatStream(messages, (chunk) => {
|
|
72
|
+
if (chunk.content) {
|
|
73
|
+
fullResponse += chunk.content;
|
|
74
|
+
// 只有在文本格式时才流式输出
|
|
75
|
+
if (!options.format || options.format === 'text') {
|
|
76
|
+
process.stdout.write(chunk.content);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
console.log();
|
|
82
|
+
console.log(chalk.gray('└─────────────────────────────────────'));
|
|
83
|
+
console.log();
|
|
84
|
+
|
|
85
|
+
// 使用格式化器输出
|
|
86
|
+
if (options.format && options.format !== 'text') {
|
|
87
|
+
formatter.print({
|
|
88
|
+
format: options.format,
|
|
89
|
+
content: fullResponse,
|
|
90
|
+
metadata: {
|
|
91
|
+
role: options.role,
|
|
92
|
+
severity: options.severity,
|
|
93
|
+
provider: options.provider,
|
|
94
|
+
model: options.model
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
} else {
|
|
98
|
+
// Print just the response for piping purposes
|
|
99
|
+
console.log(fullResponse);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
} catch (error) {
|
|
103
|
+
printer.printError(error instanceof Error ? error.message : String(error));
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readFromStdin(): Promise<string> {
|
|
109
|
+
return new Promise((resolve) => {
|
|
110
|
+
let data = '';
|
|
111
|
+
|
|
112
|
+
const rl = readline.createInterface({
|
|
113
|
+
input: process.stdin,
|
|
114
|
+
crlfDelay: Infinity
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
rl.on('line', (line) => {
|
|
118
|
+
data += line + '\n';
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
rl.on('close', () => {
|
|
122
|
+
resolve(data.trim());
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function promptBatchCommand(inputs: string[], options: PromptOptions): Promise<void> {
|
|
128
|
+
// Set up system message
|
|
129
|
+
const systemMessage =
|
|
130
|
+
options.role === 'boss'
|
|
131
|
+
? getBossSystemMessage(options.severity)
|
|
132
|
+
: getEmployeeSystemMessage(options.severity);
|
|
133
|
+
|
|
134
|
+
// Create LLM instance
|
|
135
|
+
const llm = createLLM(options.provider, {
|
|
136
|
+
apiKey: options.apiKey,
|
|
137
|
+
model: options.model,
|
|
138
|
+
baseUrl: getProviderBaseUrl(options.provider),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
for (const input of inputs) {
|
|
142
|
+
const messages: Message[] = [
|
|
143
|
+
{ role: 'system', content: systemMessage },
|
|
144
|
+
{ role: 'user', content: input }
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const response = await llm.chat(messages);
|
|
149
|
+
console.log(response);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export type ProviderType = 'zhipu' | 'openai';
|
|
2
|
+
|
|
3
|
+
export interface ProviderDefinition {
|
|
4
|
+
id: ProviderType;
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
defaultBaseUrl: string;
|
|
8
|
+
defaultModels: string[];
|
|
9
|
+
envKeyNames: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const PROVIDERS: Record<ProviderType, ProviderDefinition> = {
|
|
13
|
+
zhipu: {
|
|
14
|
+
id: 'zhipu',
|
|
15
|
+
name: '智谱 AI',
|
|
16
|
+
description: '国产大模型,稳定可靠,响应快速',
|
|
17
|
+
defaultBaseUrl: '',
|
|
18
|
+
defaultModels: ['glm-4.7', 'glm-4.7-flash'],
|
|
19
|
+
envKeyNames: ['ZHIPUAI_API_KEY'],
|
|
20
|
+
},
|
|
21
|
+
openai: {
|
|
22
|
+
id: 'openai',
|
|
23
|
+
name: 'OpenAI',
|
|
24
|
+
description: '国际通用,支持 GPT-4o、GPT-4o-mini 等模型',
|
|
25
|
+
defaultBaseUrl: 'https://api.openai.com/v1',
|
|
26
|
+
defaultModels: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-3.5-turbo'],
|
|
27
|
+
envKeyNames: ['OPENAI_API_KEY'],
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get provider definition by ID
|
|
33
|
+
*/
|
|
34
|
+
export function getProvider(id: string): ProviderDefinition | null {
|
|
35
|
+
return PROVIDERS[id as ProviderType] ?? null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get all provider IDs
|
|
40
|
+
*/
|
|
41
|
+
export function getProviderIds(): ProviderType[] {
|
|
42
|
+
return Object.keys(PROVIDERS) as ProviderType[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Validate API key format (basic validation)
|
|
47
|
+
*/
|
|
48
|
+
export function validateApiKey(provider: ProviderType, apiKey: string): { valid: boolean; error?: string } {
|
|
49
|
+
if (!apiKey || apiKey.trim().length === 0) {
|
|
50
|
+
return { valid: false, error: 'API Key 不能为空' };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Provider-specific validation
|
|
54
|
+
switch (provider) {
|
|
55
|
+
case 'zhipu':
|
|
56
|
+
// 智谱 API key 通常以特定格式开头
|
|
57
|
+
if (apiKey.length < 10) {
|
|
58
|
+
return { valid: false, error: '智谱 API Key 格式不正确(太短)' };
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
|
|
62
|
+
case 'openai':
|
|
63
|
+
// OpenAI API key 通常以 sk- 开头
|
|
64
|
+
if (!apiKey.startsWith('sk-')) {
|
|
65
|
+
return { valid: false, error: 'OpenAI API Key 通常以 sk- 开头' };
|
|
66
|
+
}
|
|
67
|
+
if (apiKey.length < 20) {
|
|
68
|
+
return { valid: false, error: 'OpenAI API Key 格式不正确(太短)' };
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { valid: true };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Validate base URL format
|
|
78
|
+
*/
|
|
79
|
+
export function validateBaseUrl(baseUrl: string): { valid: boolean; error?: string } {
|
|
80
|
+
if (!baseUrl || baseUrl.trim().length === 0) {
|
|
81
|
+
return { valid: true }; // Empty is OK (will use default)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const url = new URL(baseUrl);
|
|
86
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
87
|
+
return { valid: false, error: 'Base URL 必须以 http:// 或 https:// 开头' };
|
|
88
|
+
}
|
|
89
|
+
return { valid: true };
|
|
90
|
+
} catch {
|
|
91
|
+
return { valid: false, error: 'Base URL 格式不正确' };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get API key from environment variables
|
|
97
|
+
*/
|
|
98
|
+
export function getApiKeyFromEnv(provider: ProviderType): string | null {
|
|
99
|
+
const providerDef = PROVIDERS[provider];
|
|
100
|
+
|
|
101
|
+
for (const envKey of providerDef.envKeyNames) {
|
|
102
|
+
const value = process.env[envKey];
|
|
103
|
+
if (value) {
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
export interface SessionData {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
createdAt: string;
|
|
10
|
+
updatedAt: string;
|
|
11
|
+
messages: Array<{ role: string; content: string }>;
|
|
12
|
+
metadata?: {
|
|
13
|
+
role?: string;
|
|
14
|
+
severity?: string;
|
|
15
|
+
provider?: string;
|
|
16
|
+
model?: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class SessionStorage {
|
|
21
|
+
private sessionsDir: string;
|
|
22
|
+
|
|
23
|
+
constructor() {
|
|
24
|
+
this.sessionsDir = path.join(os.homedir(), '.pua-cli', 'sessions');
|
|
25
|
+
this.ensureDirectory();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private ensureDirectory(): void {
|
|
29
|
+
if (!fs.existsSync(this.sessionsDir)) {
|
|
30
|
+
fs.mkdirSync(this.sessionsDir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
listSessions(): SessionData[] {
|
|
35
|
+
const files = fs.readdirSync(this.sessionsDir)
|
|
36
|
+
.filter(f => f.endsWith('.json'));
|
|
37
|
+
|
|
38
|
+
return files.map(file => {
|
|
39
|
+
const filePath = path.join(this.sessionsDir, file);
|
|
40
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
41
|
+
return JSON.parse(content);
|
|
42
|
+
})
|
|
43
|
+
.sort((a, b) =>
|
|
44
|
+
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
loadSession(sessionId: string): SessionData | null {
|
|
49
|
+
const filePath = path.join(this.sessionsDir, `${sessionId}.json`);
|
|
50
|
+
|
|
51
|
+
if (!fs.existsSync(filePath)) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
56
|
+
return JSON.parse(content);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
saveSession(sessionData: Omit<SessionData, 'id' | 'createdAt' | 'updatedAt'>): SessionData {
|
|
60
|
+
const sessionId = this.generateId();
|
|
61
|
+
const now = new Date().toISOString();
|
|
62
|
+
|
|
63
|
+
const fullSession: SessionData = {
|
|
64
|
+
...sessionData,
|
|
65
|
+
id: sessionId,
|
|
66
|
+
createdAt: now,
|
|
67
|
+
updatedAt: now
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const filePath = path.join(this.sessionsDir, `${sessionId}.json`);
|
|
71
|
+
fs.writeFileSync(filePath, JSON.stringify(fullSession, null, 2));
|
|
72
|
+
|
|
73
|
+
return fullSession;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
deleteSession(sessionId: string): boolean {
|
|
77
|
+
const filePath = path.join(this.sessionsDir, `${sessionId}.json`);
|
|
78
|
+
|
|
79
|
+
if (fs.existsSync(filePath)) {
|
|
80
|
+
fs.unlinkSync(filePath);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private generateId(): string {
|
|
88
|
+
return `session-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
getSessionsDir(): string {
|
|
92
|
+
return this.sessionsDir;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import dotenv from 'dotenv';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import {
|
|
4
|
+
loadGlobalConfig,
|
|
5
|
+
loadProjectConfig,
|
|
6
|
+
getProviderApiKey,
|
|
7
|
+
getCurrentProvider,
|
|
8
|
+
hasGlobalConfig,
|
|
9
|
+
type GlobalConfig,
|
|
10
|
+
type ProjectConfig,
|
|
11
|
+
} from './storage';
|
|
12
|
+
import { getProvider, validateApiKey, getApiKeyFromEnv, type ProviderType } from './providers';
|
|
13
|
+
|
|
14
|
+
// Load environment variables
|
|
15
|
+
dotenv.config();
|
|
16
|
+
|
|
17
|
+
export interface RuntimeConfig {
|
|
18
|
+
apiKey: string;
|
|
19
|
+
provider: ProviderType;
|
|
20
|
+
model: string;
|
|
21
|
+
role: 'boss' | 'employee';
|
|
22
|
+
severity: 'mild' | 'medium' | 'extreme';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const DEFAULTS = {
|
|
26
|
+
provider: 'zhipu' as ProviderType,
|
|
27
|
+
model: 'glm-4.7',
|
|
28
|
+
role: 'boss' as 'boss' | 'employee',
|
|
29
|
+
severity: 'medium' as 'mild' | 'medium' | 'extreme',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Load configuration with priority:
|
|
34
|
+
* 1. Command line options (highest)
|
|
35
|
+
* 2. Environment variables
|
|
36
|
+
* 3. Project config (.pua.json)
|
|
37
|
+
* 4. Global config (~/.config/pua-cli/config.json)
|
|
38
|
+
* 5. Defaults (lowest)
|
|
39
|
+
*/
|
|
40
|
+
export function loadConfig(options?: {
|
|
41
|
+
provider?: ProviderType;
|
|
42
|
+
model?: string;
|
|
43
|
+
role?: 'boss' | 'employee';
|
|
44
|
+
severity?: 'mild' | 'medium' | 'extreme';
|
|
45
|
+
apiKey?: string;
|
|
46
|
+
}): RuntimeConfig {
|
|
47
|
+
// Start with defaults
|
|
48
|
+
let provider = DEFAULTS.provider;
|
|
49
|
+
let model = DEFAULTS.model;
|
|
50
|
+
let role = DEFAULTS.role;
|
|
51
|
+
let severity = DEFAULTS.severity;
|
|
52
|
+
let apiKey = '';
|
|
53
|
+
|
|
54
|
+
// Load global config
|
|
55
|
+
const globalConfig = loadGlobalConfig();
|
|
56
|
+
|
|
57
|
+
// Load project config
|
|
58
|
+
const projectConfig = loadProjectConfig();
|
|
59
|
+
|
|
60
|
+
// Apply global config
|
|
61
|
+
if (globalConfig) {
|
|
62
|
+
provider = globalConfig.defaults.provider as ProviderType || provider;
|
|
63
|
+
model = globalConfig.defaults.model || model;
|
|
64
|
+
role = globalConfig.defaults.role as 'boss' | 'employee' || role;
|
|
65
|
+
severity = globalConfig.defaults.severity as 'mild' | 'medium' | 'extreme' || severity;
|
|
66
|
+
apiKey = globalConfig.providers[globalConfig.currentProvider]?.apiKey || '';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Apply project config
|
|
70
|
+
if (projectConfig) {
|
|
71
|
+
if (projectConfig.provider) provider = projectConfig.provider as ProviderType;
|
|
72
|
+
if (projectConfig.model) model = projectConfig.model;
|
|
73
|
+
if (projectConfig.role) role = projectConfig.role as 'boss' | 'employee';
|
|
74
|
+
if (projectConfig.severity) severity = projectConfig.severity as 'mild' | 'medium' | 'extreme';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Apply environment variables
|
|
78
|
+
const envApiKey = getApiKeyFromEnv(provider);
|
|
79
|
+
if (envApiKey) {
|
|
80
|
+
apiKey = envApiKey;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Apply command line options (highest priority)
|
|
84
|
+
if (options) {
|
|
85
|
+
if (options.provider) provider = options.provider;
|
|
86
|
+
if (options.model) model = options.model;
|
|
87
|
+
if (options.role) role = options.role;
|
|
88
|
+
if (options.severity) severity = options.severity;
|
|
89
|
+
if (options.apiKey) apiKey = options.apiKey;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Ensure API key is available
|
|
93
|
+
if (!apiKey) {
|
|
94
|
+
throw new MissingApiKeyError(provider);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
apiKey,
|
|
99
|
+
provider,
|
|
100
|
+
model,
|
|
101
|
+
role,
|
|
102
|
+
severity,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Error thrown when API key is missing
|
|
108
|
+
*/
|
|
109
|
+
export class MissingApiKeyError extends Error {
|
|
110
|
+
constructor(provider: ProviderType) {
|
|
111
|
+
const providerDef = getProvider(provider);
|
|
112
|
+
const envKey = providerDef?.envKeyNames[0] || 'API_KEY';
|
|
113
|
+
|
|
114
|
+
super(
|
|
115
|
+
`未找到 ${providerDef?.name || provider} 的 API Key\n\n` +
|
|
116
|
+
`请使用以下方式之一配置:\n` +
|
|
117
|
+
` 1. 运行: pua config\n` +
|
|
118
|
+
` 2. 设置环境变量: export ${envKey}="your-api-key"\n` +
|
|
119
|
+
` 3. 创建 .env 文件并添加: ${envKey}=your-api-key`
|
|
120
|
+
);
|
|
121
|
+
this.name = 'MissingApiKeyError';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Check if user needs onboarding
|
|
127
|
+
*/
|
|
128
|
+
export function needsOnboarding(): boolean {
|
|
129
|
+
return !hasGlobalConfig();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get provider's base URL
|
|
134
|
+
*/
|
|
135
|
+
export function getProviderBaseUrl(provider: ProviderType): string {
|
|
136
|
+
const globalConfig = loadGlobalConfig();
|
|
137
|
+
const providerConfig = globalConfig?.providers[provider];
|
|
138
|
+
|
|
139
|
+
if (providerConfig?.baseUrl) {
|
|
140
|
+
return providerConfig.baseUrl;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const providerDef = getProvider(provider);
|
|
144
|
+
return providerDef?.defaultBaseUrl || '';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get available models for a provider
|
|
149
|
+
*/
|
|
150
|
+
export function getProviderModels(provider: ProviderType): string[] {
|
|
151
|
+
const globalConfig = loadGlobalConfig();
|
|
152
|
+
const providerConfig = globalConfig?.providers[provider];
|
|
153
|
+
|
|
154
|
+
if (providerConfig?.models && providerConfig.models.length > 0) {
|
|
155
|
+
return providerConfig.models;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const providerDef = getProvider(provider);
|
|
159
|
+
return providerDef?.defaultModels || [];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Validate a config object
|
|
164
|
+
*/
|
|
165
|
+
export function validateConfig(config: RuntimeConfig): { valid: boolean; errors: string[] } {
|
|
166
|
+
const errors: string[] = [];
|
|
167
|
+
|
|
168
|
+
// Validate provider
|
|
169
|
+
const providerDef = getProvider(config.provider);
|
|
170
|
+
if (!providerDef) {
|
|
171
|
+
errors.push(`无效的 provider: ${config.provider}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Validate API key
|
|
175
|
+
if (providerDef) {
|
|
176
|
+
const keyValidation = validateApiKey(config.provider, config.apiKey);
|
|
177
|
+
if (!keyValidation.valid) {
|
|
178
|
+
errors.push(keyValidation.error || 'API Key 无效');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Validate model
|
|
183
|
+
const availableModels = getProviderModels(config.provider);
|
|
184
|
+
if (availableModels.length > 0 && !availableModels.includes(config.model)) {
|
|
185
|
+
errors.push(
|
|
186
|
+
`模型 ${config.model} 在 provider ${config.provider} 中不可用。可用模型: ${availableModels.join(', ')}`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
valid: errors.length === 0,
|
|
192
|
+
errors,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
|
|
5
|
+
export interface ProviderConfig {
|
|
6
|
+
apiKey: string;
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
models: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface GlobalConfig {
|
|
12
|
+
currentProvider: string;
|
|
13
|
+
providers: {
|
|
14
|
+
[key: string]: ProviderConfig;
|
|
15
|
+
};
|
|
16
|
+
defaults: {
|
|
17
|
+
provider: string;
|
|
18
|
+
model: string;
|
|
19
|
+
role: 'boss' | 'employee';
|
|
20
|
+
severity: 'mild' | 'medium' | 'extreme';
|
|
21
|
+
};
|
|
22
|
+
onboardingCompleted: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ProjectConfig {
|
|
26
|
+
provider?: string;
|
|
27
|
+
model?: string;
|
|
28
|
+
role?: 'boss' | 'employee';
|
|
29
|
+
severity?: 'mild' | 'medium' | 'extreme';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get the config directory path following XDG Base Directory Specification
|
|
34
|
+
*/
|
|
35
|
+
export function getConfigDir(): string {
|
|
36
|
+
const platform = process.platform;
|
|
37
|
+
|
|
38
|
+
if (platform === 'win32') {
|
|
39
|
+
// Windows: %APPDATA%\pua-cli
|
|
40
|
+
return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'pua-cli');
|
|
41
|
+
} else {
|
|
42
|
+
// Linux/macOS: ~/.config/pua-cli
|
|
43
|
+
return path.join(os.homedir(), '.config', 'pua-cli');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the global config file path
|
|
49
|
+
*/
|
|
50
|
+
export function getGlobalConfigPath(): string {
|
|
51
|
+
return path.join(getConfigDir(), 'config.json');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get the project config file path
|
|
56
|
+
*/
|
|
57
|
+
export function getProjectConfigPath(): string {
|
|
58
|
+
return path.join(process.cwd(), '.pua.json');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Ensure config directory exists
|
|
63
|
+
*/
|
|
64
|
+
export function ensureConfigDir(): void {
|
|
65
|
+
const configDir = getConfigDir();
|
|
66
|
+
if (!fs.existsSync(configDir)) {
|
|
67
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Load global config from file
|
|
73
|
+
*/
|
|
74
|
+
export function loadGlobalConfig(): GlobalConfig | null {
|
|
75
|
+
const configPath = getGlobalConfigPath();
|
|
76
|
+
|
|
77
|
+
if (!fs.existsSync(configPath)) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
83
|
+
return JSON.parse(content) as GlobalConfig;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
throw new Error(`Failed to load config from ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Save global config to file
|
|
91
|
+
*/
|
|
92
|
+
export function saveGlobalConfig(config: GlobalConfig): void {
|
|
93
|
+
ensureConfigDir();
|
|
94
|
+
const configPath = getGlobalConfigPath();
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
98
|
+
} catch (error) {
|
|
99
|
+
throw new Error(`Failed to save config to ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Load project config from file
|
|
105
|
+
*/
|
|
106
|
+
export function loadProjectConfig(): ProjectConfig | null {
|
|
107
|
+
const configPath = getProjectConfigPath();
|
|
108
|
+
|
|
109
|
+
if (!fs.existsSync(configPath)) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
115
|
+
return JSON.parse(content) as ProjectConfig;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
throw new Error(`Failed to load project config from ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Check if global config exists
|
|
123
|
+
*/
|
|
124
|
+
export function hasGlobalConfig(): boolean {
|
|
125
|
+
return fs.existsSync(getGlobalConfigPath());
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Check if onboarding has been completed
|
|
130
|
+
*/
|
|
131
|
+
export function isOnboardingCompleted(): boolean {
|
|
132
|
+
const config = loadGlobalConfig();
|
|
133
|
+
return config?.onboardingCompleted ?? false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get API key for a provider from config
|
|
138
|
+
*/
|
|
139
|
+
export function getProviderApiKey(provider: string): string | null {
|
|
140
|
+
const config = loadGlobalConfig();
|
|
141
|
+
return config?.providers[provider]?.apiKey ?? null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get current provider
|
|
146
|
+
*/
|
|
147
|
+
export function getCurrentProvider(): string | null {
|
|
148
|
+
const config = loadGlobalConfig();
|
|
149
|
+
return config?.currentProvider ?? null;
|
|
150
|
+
}
|