Add 260+ Claude Code skills from skills.sh
Complete collection of AI agent skills including: - Frontend Development (Vue, React, Next.js, Three.js) - Backend Development (NestJS, FastAPI, Node.js) - Mobile Development (React Native, Expo) - Testing (E2E, frontend, webapp) - DevOps (GitHub Actions, CI/CD) - Marketing (SEO, copywriting, analytics) - Security (binary analysis, vulnerability scanning) - And many more... Synchronized from: https://skills.sh/ Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
455
multi-ai-brainstorm/qwen-client.js
Normal file
455
multi-ai-brainstorm/qwen-client.js
Normal file
@@ -0,0 +1,455 @@
|
||||
/**
|
||||
* Qwen API Client for Multi-AI Brainstorm
|
||||
* Integrates with PromptArch's Qwen OAuth service
|
||||
*/
|
||||
|
||||
const DEFAULT_ENDPOINT = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
|
||||
const PROMPTARCH_PROXY = "https://www.rommark.dev/tools/promptarch/api/qwen/chat";
|
||||
const CREDENTIALS_PATH = `${process.env.HOME}/.claude/qwen-credentials.json`;
|
||||
|
||||
// Qwen OAuth Configuration (from Qwen Code source)
|
||||
const QWEN_OAUTH_BASE_URL = 'https://chat.qwen.ai';
|
||||
const QWEN_OAUTH_DEVICE_CODE_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/device/code`;
|
||||
const QWEN_OAUTH_TOKEN_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/token`;
|
||||
const QWEN_OAUTH_CLIENT_ID = 'f0304373b74a44d2b584a3fb70ca9e56';
|
||||
const QWEN_OAUTH_SCOPE = 'openid profile email model.completion';
|
||||
const QWEN_OAUTH_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
|
||||
|
||||
/**
|
||||
* Qwen API Client Class
|
||||
*/
|
||||
class QwenClient {
|
||||
constructor() {
|
||||
this.apiKey = null;
|
||||
this.accessToken = null;
|
||||
this.refreshToken = null;
|
||||
this.tokenExpiresAt = null;
|
||||
this.endpoint = DEFAULT_ENDPOINT;
|
||||
this.model = "coder-model";
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize client with credentials
|
||||
*/
|
||||
async initialize() {
|
||||
try {
|
||||
const fs = require('fs');
|
||||
if (fs.existsSync(CREDENTIALS_PATH)) {
|
||||
const credentials = JSON.parse(fs.readFileSync(CREDENTIALS_PATH, 'utf8'));
|
||||
|
||||
// Handle both API key and OAuth token credentials
|
||||
if (credentials.accessToken) {
|
||||
this.accessToken = credentials.accessToken;
|
||||
this.refreshToken = credentials.refreshToken;
|
||||
this.tokenExpiresAt = credentials.tokenExpiresAt;
|
||||
this.endpoint = credentials.endpoint || DEFAULT_ENDPOINT;
|
||||
|
||||
// Check if token needs refresh
|
||||
if (this.isTokenExpired()) {
|
||||
await this.refreshAccessToken();
|
||||
}
|
||||
return true;
|
||||
} else if (credentials.apiKey) {
|
||||
this.apiKey = credentials.apiKey;
|
||||
this.endpoint = credentials.endpoint || DEFAULT_ENDPOINT;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// No credentials stored yet
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if access token is expired
|
||||
*/
|
||||
isTokenExpired() {
|
||||
if (!this.tokenExpiresAt) return false;
|
||||
// Add 5 minute buffer before expiration
|
||||
return Date.now() >= (this.tokenExpiresAt - 5 * 60 * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt user for authentication method
|
||||
*/
|
||||
async promptForCredentials() {
|
||||
const readline = require('readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
rl.question(
|
||||
'\n🔐 Choose authentication method:\n' +
|
||||
' 1. OAuth (Recommended) - Free 2000 requests/day with qwen.ai account\n' +
|
||||
' 2. API Key - Get it at https://help.aliyun.com/zh/dashscope/\n\n' +
|
||||
'Enter choice (1 or 2): ',
|
||||
async (choice) => {
|
||||
rl.close();
|
||||
|
||||
if (choice === '1') {
|
||||
try {
|
||||
await this.performOAuthFlow();
|
||||
resolve(true);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
} else if (choice === '2') {
|
||||
await this.promptForAPIKey();
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(new Error('Invalid choice. Please enter 1 or 2.'));
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt user for API key only
|
||||
*/
|
||||
async promptForAPIKey() {
|
||||
const readline = require('readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
rl.question('Enter your Qwen API key (get it at https://help.aliyun.com/zh/dashscope/): ', (key) => {
|
||||
if (!key || key.trim().length === 0) {
|
||||
rl.close();
|
||||
reject(new Error('API key is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.apiKey = key.trim();
|
||||
this.saveCredentials();
|
||||
rl.close();
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save credentials to file
|
||||
*/
|
||||
saveCredentials() {
|
||||
try {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const dir = path.dirname(CREDENTIALS_PATH);
|
||||
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
const credentials = {
|
||||
endpoint: this.endpoint
|
||||
};
|
||||
|
||||
// Save OAuth tokens
|
||||
if (this.accessToken) {
|
||||
credentials.accessToken = this.accessToken;
|
||||
credentials.refreshToken = this.refreshToken;
|
||||
credentials.tokenExpiresAt = this.tokenExpiresAt;
|
||||
}
|
||||
// Save API key
|
||||
else if (this.apiKey) {
|
||||
credentials.apiKey = this.apiKey;
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
CREDENTIALS_PATH,
|
||||
JSON.stringify(credentials, null, 2)
|
||||
);
|
||||
console.log(`✓ Credentials saved to ${CREDENTIALS_PATH}`);
|
||||
} catch (error) {
|
||||
console.warn('Could not save credentials:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate PKCE code verifier and challenge pair
|
||||
*/
|
||||
generatePKCEPair() {
|
||||
const crypto = require('crypto');
|
||||
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
||||
const codeChallenge = crypto.createHash('sha256')
|
||||
.update(codeVerifier)
|
||||
.digest('base64url');
|
||||
return { code_verifier: codeVerifier, code_challenge: codeChallenge };
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert object to URL-encoded form data
|
||||
*/
|
||||
objectToUrlEncoded(data) {
|
||||
return Object.keys(data)
|
||||
.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`)
|
||||
.join('&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform OAuth 2.0 Device Code Flow (from Qwen Code implementation)
|
||||
*/
|
||||
async performOAuthFlow() {
|
||||
const { exec } = require('child_process');
|
||||
|
||||
console.log('\n🔐 Starting Qwen OAuth Device Code Flow...\n');
|
||||
|
||||
// Generate PKCE parameters
|
||||
const { code_verifier, code_challenge } = this.generatePKCEPair();
|
||||
|
||||
// Step 1: Request device authorization
|
||||
console.log('Requesting device authorization...');
|
||||
const deviceAuthResponse = await fetch(QWEN_OAUTH_DEVICE_CODE_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: this.objectToUrlEncoded({
|
||||
client_id: QWEN_OAUTH_CLIENT_ID,
|
||||
scope: QWEN_OAUTH_SCOPE,
|
||||
code_challenge: code_challenge,
|
||||
code_challenge_method: 'S256',
|
||||
}),
|
||||
});
|
||||
|
||||
if (!deviceAuthResponse.ok) {
|
||||
const error = await deviceAuthResponse.text();
|
||||
throw new Error(`Device authorization failed: ${deviceAuthResponse.status} - ${error}`);
|
||||
}
|
||||
|
||||
const deviceAuth = await deviceAuthResponse.json();
|
||||
|
||||
if (!deviceAuth.device_code) {
|
||||
throw new Error('Invalid device authorization response');
|
||||
}
|
||||
|
||||
// Step 2: Display authorization instructions
|
||||
console.log('\n=== Qwen OAuth Device Authorization ===\n');
|
||||
console.log('1. Visit this URL in your browser:\n');
|
||||
console.log(` ${deviceAuth.verification_uri_complete}\n`);
|
||||
console.log('2. Sign in to your qwen.ai account and authorize\n');
|
||||
console.log('Waiting for authorization to complete...\n');
|
||||
|
||||
// Try to open browser automatically
|
||||
try {
|
||||
const openCommand = process.platform === 'darwin' ? 'open' :
|
||||
process.platform === 'win32' ? 'start' : 'xdg-open';
|
||||
exec(`${openCommand} "${deviceAuth.verification_uri_complete}"`, (err) => {
|
||||
if (err) {
|
||||
console.debug('Could not open browser automatically');
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.debug('Failed to open browser:', err.message);
|
||||
}
|
||||
|
||||
// Step 3: Poll for token
|
||||
let pollInterval = 2000; // Start with 2 seconds
|
||||
const maxAttempts = Math.ceil(deviceAuth.expires_in / (pollInterval / 1000));
|
||||
let attempt = 0;
|
||||
|
||||
while (attempt < maxAttempts) {
|
||||
attempt++;
|
||||
|
||||
try {
|
||||
console.debug(`Polling for token (attempt ${attempt}/${maxAttempts})...`);
|
||||
|
||||
const tokenResponse = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: this.objectToUrlEncoded({
|
||||
grant_type: QWEN_OAUTH_GRANT_TYPE,
|
||||
client_id: QWEN_OAUTH_CLIENT_ID,
|
||||
device_code: deviceAuth.device_code,
|
||||
code_verifier: code_verifier,
|
||||
}),
|
||||
});
|
||||
|
||||
// Check for pending authorization (standard OAuth RFC 8628 response)
|
||||
if (tokenResponse.status === 400) {
|
||||
const errorData = await tokenResponse.json();
|
||||
|
||||
if (errorData.error === 'authorization_pending') {
|
||||
// User hasn't authorized yet, continue polling
|
||||
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (errorData.error === 'slow_down') {
|
||||
// Polling too frequently, increase interval
|
||||
pollInterval = Math.min(pollInterval * 1.5, 10000);
|
||||
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Other 400 errors (authorization_declined, expired_token, etc.)
|
||||
throw new Error(`Authorization failed: ${errorData.error} - ${errorData.error_description || 'No description'}`);
|
||||
}
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
const error = await tokenResponse.text();
|
||||
throw new Error(`Token request failed: ${tokenResponse.status} - ${error}`);
|
||||
}
|
||||
|
||||
// Success! We have the token
|
||||
const tokenData = await tokenResponse.json();
|
||||
|
||||
if (!tokenData.access_token) {
|
||||
throw new Error('Token response missing access_token');
|
||||
}
|
||||
|
||||
// Save credentials
|
||||
this.accessToken = tokenData.access_token;
|
||||
this.refreshToken = tokenData.refresh_token;
|
||||
this.tokenExpiresAt = tokenData.expires_in ?
|
||||
Date.now() + (tokenData.expires_in * 1000) : null;
|
||||
|
||||
this.saveCredentials();
|
||||
|
||||
console.log('\n✓ OAuth authentication successful!');
|
||||
console.log('✓ Access token obtained and saved.\n');
|
||||
|
||||
return;
|
||||
|
||||
} catch (error) {
|
||||
// Check if this is a fatal error (not pending/slow_down)
|
||||
if (error.message.includes('Authorization failed') ||
|
||||
error.message.includes('Token request failed')) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// For other errors, wait and retry
|
||||
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('OAuth authentication timeout');
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh access token using refresh token
|
||||
*/
|
||||
async refreshAccessToken() {
|
||||
if (!this.refreshToken) {
|
||||
throw new Error('No refresh token available. Please re-authenticate.');
|
||||
}
|
||||
|
||||
console.log('🔄 Refreshing access token...');
|
||||
|
||||
const tokenResponse = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: this.objectToUrlEncoded({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: this.refreshToken,
|
||||
client_id: QWEN_OAUTH_CLIENT_ID,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
const error = await tokenResponse.text();
|
||||
throw new Error(`Token refresh failed: ${tokenResponse.status} - ${error}`);
|
||||
}
|
||||
|
||||
const tokens = await tokenResponse.json();
|
||||
|
||||
this.accessToken = tokens.access_token;
|
||||
if (tokens.refresh_token) {
|
||||
this.refreshToken = tokens.refresh_token;
|
||||
}
|
||||
this.tokenExpiresAt = tokens.expires_in ?
|
||||
Date.now() + (tokens.expires_in * 1000) : null;
|
||||
|
||||
this.saveCredentials();
|
||||
console.log('✓ Token refreshed successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authentication key (prefer OAuth access token, fallback to API key)
|
||||
*/
|
||||
getAuthKey() {
|
||||
return this.accessToken || this.apiKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a chat completion request
|
||||
*/
|
||||
async chatCompletion(messages, options = {}) {
|
||||
const authKey = this.getAuthKey();
|
||||
|
||||
if (!authKey) {
|
||||
throw new Error('Qwen API key not configured. Run /multi-ai-brainstorm first to set up.');
|
||||
}
|
||||
|
||||
// Check if OAuth token needs refresh
|
||||
if (this.accessToken && this.isTokenExpired()) {
|
||||
await this.refreshAccessToken();
|
||||
}
|
||||
|
||||
const {
|
||||
model = this.model,
|
||||
stream = false,
|
||||
temperature = 0.7,
|
||||
maxTokens = 2000
|
||||
} = options;
|
||||
|
||||
const payload = {
|
||||
model,
|
||||
messages,
|
||||
stream,
|
||||
temperature,
|
||||
max_tokens: maxTokens
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(PROMPTARCH_PROXY, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${authKey}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
endpoint: this.endpoint,
|
||||
...payload
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Qwen API error (${response.status}): ${error}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.choices?.[0]?.message?.content || '';
|
||||
} catch (error) {
|
||||
if (error.message.includes('fetch')) {
|
||||
throw new Error('Network error. Please check your internet connection.');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if client is authenticated
|
||||
*/
|
||||
isAuthenticated() {
|
||||
return !!(this.accessToken || this.apiKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
const client = new QwenClient();
|
||||
|
||||
module.exports = client;
|
||||
Reference in New Issue
Block a user