Add Kimi K2 provider

This commit is contained in:
BinaryBeastMaster 2025-07-24 20:00:32 -04:00
parent 1cfc01411e
commit 6ec302ea69
6 changed files with 180 additions and 2 deletions

View file

@ -86,12 +86,14 @@ Interacts with Gemini, AI Studio, ChatGPT, and Claude UIs. Injects chat, capture
- [`AIStudioProvider`](extension/providers/aistudio.js)
- [`ChatGptProvider`](extension/providers/chatgpt.js)
- [`ClaudeProvider`](extension/providers/claude.js)
- [`KimiK2Provider`](extension/providers/kimi_k2.js)
**Supported Chat Interfaces:**
- Gemini (`gemini.google.com`)
- AI Studio (`aistudio.google.com`)
- ChatGPT (`chatgpt.com`)
- Claude (`claude.ai`)
- Kimi K2 (`k2.kimi.ai`)
ChatGPT is a trademark of OpenAI. Gemini and AI Studio are trademarks of Google. Claude is a trademark of Anthropic. This project is not affiliated with, endorsed by, or sponsored by OpenAI, Google, or Anthropic.

34
docs/provider-kimi-k2.md Normal file
View file

@ -0,0 +1,34 @@
# Kimi K2 Provider Architecture
This document describes the `KimiK2Provider` used by the extension to automate the Kimi K2 chat interface.
---
## 🧩 Overview
`KimiK2Provider` is modeled after the existing AI Studio and ChatGPT providers. It sends messages and captures responses from `k2.kimi.ai` using DOM based monitoring by default with an optional debugger fallback.
---
## ⚙️ Configurable Options
```js
this.captureMethod = 'dom'; // or 'debugger'
this.debuggerUrlPattern = '*k2.kimi.ai/api/chat*';
this.includeThinkingInMessage = false;
```
---
## 📌 DOM Selectors
- Input field: `textarea, div[contenteditable="true"]`
- Send button: `button[type="submit"], button.send-btn`
- Response blocks: `.message.ai, .chat-message.ai`
- Typing indicator: `.typing, .loading`
---
## ✅ Summary
The provider offers a lightweight integration with Kimi K2 using the same structure as the other providers. It can capture responses via DOM observation or via Chrome debugger when configured.

View file

@ -36,7 +36,7 @@ let lastSuccessfullyProcessedMessageText = null; // Text of the last message suc
const pendingRequestDetails = new Map(); // Stores { text: string } for active requests, keyed by requestId
// Supported domains for chat interfaces
const supportedDomains = ['gemini.google.com', 'aistudio.google.com', 'chatgpt.com', 'claude.ai'];
const supportedDomains = ['gemini.google.com', 'aistudio.google.com', 'chatgpt.com', 'claude.ai', 'k2.kimi.ai'];
// ===== DEBUGGER RELATED GLOBALS =====
const BG_LOG_PREFIX = '[BG DEBUGGER]';

View file

@ -17,6 +17,7 @@
"*://*.chatgpt.com/*",
"*://*.aistudio.com/*",
"*://*.claude.ai/*",
"*://*.k2.kimi.ai/*",
"ws://localhost:*/"
],
"background": {
@ -58,6 +59,15 @@
"content.js"
],
"run_at": "document_idle"
},
{
"matches": ["*://k2.kimi.ai/*"],
"js": [
"providers/provider-utils.js",
"providers/kimi_k2.js",
"content.js"
],
"run_at": "document_idle"
}
],
"action": {

View file

@ -22,7 +22,8 @@ const providers = {
'gemini': window.geminiProvider,
'aistudio': window.aiStudioProvider,
'chatgpt': window.chatgptProvider,
'claude': window.claudeProvider
'claude': window.claudeProvider,
'kimi_k2': window.kimiK2Provider
};
// Get a provider by ID
@ -40,6 +41,8 @@ function detectProvider(url) {
return providers.chatgpt;
} else if (url.includes('claude.ai')) {
return providers.claude;
} else if (url.includes('k2.kimi.ai')) {
return providers.kimi_k2;
}
// Default to aistudio if we can't detect

View file

@ -0,0 +1,129 @@
/*
* Chat Relay: Relay for AI Chat Interfaces
* Copyright (C) 2025 Jamison Moore
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
// AI Chat Relay - Kimi K2 Provider
class KimiK2Provider {
constructor() {
this.name = 'KimiK2Provider';
this.supportedDomains = ['k2.kimi.ai'];
// --- START OF CONFIGURABLE PROPERTIES ---
this.captureMethod = 'dom'; // DOM capture by default
this.debuggerUrlPattern = '*k2.kimi.ai/api/chat*'; // Placeholder
this.includeThinkingInMessage = false;
// --- END OF CONFIGURABLE PROPERTIES ---
this.inputSelector = 'textarea, div[contenteditable="true"]';
this.sendButtonSelector = 'button[type="submit"], button.send-btn';
this.responseSelector = '.message.ai, .chat-message.ai';
this.thinkingIndicatorSelector = '.typing, .loading';
this.lastSentMessage = '';
this.pendingResponseCallbacks = new Map();
this.requestAccumulators = new Map();
}
async sendChatMessage(text) {
console.log(`[${this.name}] sendChatMessage called:`, text);
const inputElement = document.querySelector(this.inputSelector);
const sendButton = document.querySelector(this.sendButtonSelector);
if (!inputElement || !sendButton) {
console.error(`[${this.name}] Missing input (${this.inputSelector}) or send button (${this.sendButtonSelector})`);
return false;
}
this.lastSentMessage = text;
if (inputElement.tagName.toLowerCase() === 'div' && inputElement.contentEditable === 'true') {
inputElement.focus();
inputElement.innerHTML = '';
inputElement.textContent = text;
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
} else {
inputElement.value = text;
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
inputElement.focus();
}
await new Promise(r => setTimeout(r, 300));
if (!sendButton.disabled && sendButton.getAttribute('aria-disabled') !== 'true') {
sendButton.click();
return true;
}
console.warn(`[${this.name}] Send button disabled.`);
return false;
}
captureResponse(element) {
if (!element) return { found: false, text: '' };
let text = element.textContent.trim();
if (!text || text === this.lastSentMessage) return { found: false, text: '' };
return { found: true, text };
}
initiateResponseCapture(requestId, callback) {
this.pendingResponseCallbacks.set(requestId, callback);
if (this.captureMethod === 'dom') {
console.log(`[${this.name}] DOM capture active for ${requestId}`);
}
}
handleDebuggerData(requestId, rawData, isFinal) {
const cb = this.pendingResponseCallbacks.get(requestId);
if (!cb) return;
const parsed = this.parseDebuggerResponse(rawData);
if (parsed.text || isFinal) {
cb(requestId, parsed.text, isFinal);
}
if (isFinal) {
this.pendingResponseCallbacks.delete(requestId);
}
}
parseDebuggerResponse(raw) {
if (!raw) return { text: '', isFinalResponse: false };
if (raw.includes('[DONE]')) {
const clean = raw.replace('[DONE]', '').trim();
return { text: clean, isFinalResponse: true };
}
return { text: raw.trim(), isFinalResponse: false };
}
getStreamingApiPatterns() {
if (this.captureMethod === 'debugger' && this.debuggerUrlPattern) {
return [{ urlPattern: this.debuggerUrlPattern, requestStage: 'Response' }];
}
return [];
}
shouldSkipResponseMonitoring() {
return this.captureMethod === 'debugger';
}
}
(function() {
if (window.providerUtils) {
const providerInstance = new KimiK2Provider();
window.providerUtils.registerProvider(providerInstance.name, providerInstance.supportedDomains, providerInstance);
} else {
console.error('ProviderUtils not found. KimiK2Provider cannot be registered.');
}
})();