Add support for kimi.moonshot.cn (#656)

* Add support for kimi.moonshot.cn

* some improvements

---------

Co-authored-by: josc146 <josStorer@outlook.com>
This commit is contained in:
xxcdd
2024-03-22 22:50:47 +08:00
committed by GitHub
parent c00b8ff9ab
commit 21b8468dc3
7 changed files with 659 additions and 1 deletions
+10
View File
@@ -23,6 +23,7 @@ import {
chatgptApiModelKeys,
chatgptWebModelKeys,
claudeWebModelKeys,
moonshotWebModelKeys,
customApiModelKeys,
defaultConfig,
getUserConfig,
@@ -46,6 +47,7 @@ import { registerCommands } from './commands.mjs'
import { generateAnswersWithBardWebApi } from '../services/apis/bard-web.mjs'
import { generateAnswersWithClaudeWebApi } from '../services/apis/claude-web.mjs'
import { generateAnswersWithMoonshotCompletionApi } from '../services/apis/moonshot-api.mjs'
import { generateAnswersWithMoonshotWebApi } from '../services/apis/moonshot-web.mjs'
function setPortProxy(port, proxyTabId) {
port.proxy = Browser.tabs.connect(proxyTabId)
@@ -161,6 +163,14 @@ async function executeApi(session, port, config) {
config.moonshotApiKey,
session.modelName,
)
} else if (moonshotWebModelKeys.includes(session.modelName)) {
await generateAnswersWithMoonshotWebApi(
port,
session.question,
session,
config,
session.modelName,
)
}
}
+11
View File
@@ -31,6 +31,7 @@ export const chatgptWebModelKeys = [
export const bingWebModelKeys = ['bingFree4', 'bingFreeSydney']
export const bardWebModelKeys = ['bardWebFree']
export const claudeWebModelKeys = ['claude2WebFree']
export const moonshotWebModelKeys = ['moonshotWebFree']
export const gptApiModelKeys = ['gptApiInstruct', 'gptApiDavinci']
export const chatgptApiModelKeys = [
'chatgptApi35',
@@ -105,6 +106,8 @@ export const Models = {
bingFree4: { value: '', desc: 'Bing (Web, GPT-4)' },
bingFreeSydney: { value: '', desc: 'Bing (Web, GPT-4, Sydney)' },
moonshotWebFree: { value: '', desc: 'Kimi.Moonshot (Web, 100k)' },
bardWebFree: { value: '', desc: 'Gemini (Web)' },
chatglmTurbo: { value: 'chatglm_turbo', desc: 'ChatGLM (ChatGLM-Turbo)' },
@@ -230,6 +233,7 @@ export const defaultConfig = {
'chatgptApi4_8k',
'claude2WebFree',
'bingFree4',
'moonshotWebFree',
'chatglmTurbo',
'customModel',
'azureOpenAi',
@@ -255,6 +259,8 @@ export const defaultConfig = {
chatgptTabId: 0,
chatgptArkoseReqUrl: '',
chatgptArkoseReqForm: '',
kimiMoonShotRefreshToken: '',
kimiMoonShotAccessToken: '',
// unchangeable
@@ -339,6 +345,11 @@ export function isUsingAzureOpenAi(configOrSession) {
export function isUsingClaude2Api(configOrSession) {
return claudeApiModelKeys.includes(configOrSession.modelName)
}
export function isUsingMoonshotWeb(configOrSession) {
return moonshotWebModelKeys.includes(configOrSession.modelName)
}
export function isUsingGithubThirdPartyApi(configOrSession) {
return githubThirdPartyApiModelKeys.includes(configOrSession.modelName)
}
+9 -1
View File
@@ -10,6 +10,7 @@ import {
getPreferredLanguageKey,
getUserConfig,
setAccessToken,
setUserConfig,
} from '../config/index.mjs'
import {
createElementAtPosition,
@@ -289,7 +290,14 @@ async function prepareForStaticCard() {
}
async function overwriteAccessToken() {
if (location.hostname !== 'chat.openai.com') return
if (location.hostname !== 'chat.openai.com') {
if (location.hostname === 'kimi.moonshot.cn') {
setUserConfig({
kimiMoonShotRefreshToken: window.localStorage.refresh_token,
})
}
return
}
let data
if (location.pathname === '/api/auth/session') {
+1
View File
@@ -15,6 +15,7 @@
"https://*.poe.com/*",
"https://*.google.com/*",
"https://claude.ai/*",
"https://*.moonshot.cn/*",
"<all_urls>"
],
"permissions": [
+1
View File
@@ -22,6 +22,7 @@
"https://*.poe.com/",
"https://*.google.com/",
"https://claude.ai/",
"https://*.moonshot.cn/*",
"<all_urls>"
],
"background": {
+624
View File
@@ -0,0 +1,624 @@
import { pushRecord, setAbortController } from './shared.mjs'
import { Models, setUserConfig } from '../../config/index.mjs'
import { fetchSSE } from '../../utils/fetch-sse'
import { isEmpty } from 'lodash-es'
export class MoonshotWeb {
/**
* If the moonshot client has initialized yet (call `init()` if you haven't and this is false)
* @property {boolean}
*/
ready
/**
* A proxy function/string to connect via
* @property {({endpoint: string, options: Object}) => {endpoint: string, options: Object} | string}
*/
proxy
/**
* A fetch function, defaults to globalThis.fetch
* @property {Function}
*/
fetch
/**
* @property {UserConfig}
*/
config
refreshToken
accessToken
/**
* Create a new moonshot API client instance.
* @param {Object} options - Options
* @param {UserConfig} options.config
* @param {function} [options.fetch] - Fetch function
* @example
* const moonshot = new moonshot({
* sessionKey: 'sk-ant-sid01-*****',
* fetch: globalThis.fetch
* })
*
* await moonshot.init();
* moonshot.sendMessage('Hello world').then(console.log)
*/
constructor({ config, fetch }) {
if (fetch) {
this.fetch = fetch
}
this.config = config
this.refreshToken = config.kimiMoonShotRefreshToken
this.accessToken = config.kimiMoonShotAccessToken
}
/**
* Get available models.
* @returns {string[]} Array of model names
*/
models() {
return ['']
}
/**
* Get the default model.
* @returns {string} Default model name
*/
defaultModel() {
return this.models()[0]
}
/**
* todo: mod
* Send a message to a new or existing conversation.
* @param {string} message - Initial message
* @param {SendMessageParams} [params] - Additional parameters
* @param {string} [params.conversation] - Existing conversation ID
* @param {boolean} [params.temporary=true] - Delete after getting response
* @returns {Promise<MessageStreamChunk>} Result message
*/
async sendMessage(message, { conversation = null, temporary = true, ...params }) {
if (!this.ready) {
await this.init()
}
if (!conversation) {
let out
let convo = await this.startConversation(message, {
...params,
done: (a) => {
if (params.done) {
params.done(a)
}
out = a
},
})
if (temporary) {
await convo.delete()
}
return out
} else {
return (await this.getConversation(conversation)).sendMessage(message, {
...params,
})
}
}
/**
* Make an API request.
* @param {string} endpoint - API endpoint
* @param {Object} options - Request options
* @returns {Promise<Response>} Fetch response
* @example
* await a.request('/api/chat/cnor0teaofogidj025b0/completion/stream').then(r => r.json())
*/
request(endpoint, options) {
// Can't figure out a way to test this so I'm just assuming it works
if (!(this.fetch || globalThis.fetch)) {
throw new Error(
`No fetch available in your environment. Use node-18 or later, a modern browser, or add the following code to your project:\n\nimport "isomorphic-fetch";\nconst moonshot = new moonshot({fetch: fetch, sessionKey: "sk-ant-sid01-*****"});`,
)
}
if (!this.proxy) {
this.proxy = ({ endpoint, options }) => ({
endpoint: 'https://kimi.moonshot.cn' + endpoint,
options,
})
}
if (typeof this.proxy === 'string') {
const HOST = this.proxy
this.proxy = ({ endpoint, options }) => ({ endpoint: HOST + endpoint, options })
}
const proxied = this.proxy({ endpoint, options })
return (this.fetch || globalThis.fetch)(proxied.endpoint, proxied.options)
}
/**
* Initialize the client.
* @async
* @returns {Promise<void>} Void
*/
async init() {
const response = this.request('/api/user', {
headers: {
accept: '*/*',
Authorization: `Bearer ${this.accessToken}`,
Origin: 'https://kimi.moonshot.cn',
},
method: 'GET',
})
if ((await response).status === 200) {
this.ready = true
} else {
const { access_token, refresh_token } = await this.request('/api/auth/token/refresh', {
headers: {
accept: '*/*',
Authorization: `Bearer ${this.refreshToken}`,
Origin: 'https://kimi.moonshot.cn',
},
method: 'GET',
})
.then((r) => r.json())
.catch(errorHandle('get kimi.moonshoot.cn access_token'))
this.accessToken = access_token
this.refreshToken = refresh_token
this.config.kimiMoonShotAccessToken = access_token
this.config.kimiMoonShotRefreshToken = refresh_token
await setUserConfig({
kimiMoonShotAccessToken: access_token,
kimiMoonShotRefreshToken: refresh_token,
})
this.ready = true
}
}
/**
* Start a new conversation
* @param {String} message The message to send to start the conversation
* @param {SendMessageParams} [params={}] Message params passed to Conversation.sendMessage
* @returns {Promise<Conversation>}
* @async
* @example
* const conversation = await moonshot.startConversation("Hello! How are you?")
* console.log(await conversation.getInfo());
*/
async startConversation(message, params = {}) {
if (!this.ready) {
await this.init()
}
const { id, name, created_at } = await this.request('/api/chat', {
headers: {
accept: '*/*',
'content-type': 'application/json',
Authorization: `Bearer ${this.accessToken}`,
Origin: 'https://kimi.moonshot.cn',
},
method: 'POST',
signal: params.signal,
body: JSON.stringify({ name: '未命名会话', is_example: false }),
})
.then((r) => r.json())
.catch(errorHandle('startConversation create'))
const convo = new Conversation(this, {
conversationId: id,
name,
created_at,
})
await convo.sendMessage(message, params)
return convo
}
/**
* Get a conversation by its ID
* @param {UUID} id The uuid of the conversation (Conversation.uuid or Conversation.conversationId)
* @async
* @returns {Conversation | null} The conversation
* @example
* const conversation = await moonshot.getConversation("222aa20a-bc79-48d2-8f6d-c819a1b5eaed");
*/
async getConversation(id) {
if (id instanceof Conversation || id.conversationId) {
return new Conversation(this, { conversationId: id.conversationId })
}
return new Conversation(this, { conversationId: id })
}
}
/**
* @typedef SendMessageParams
* @property {Boolean} [retry=false] Whether to retry the most recent message in the conversation instead of sending a new one
* @property {String} [timezone="America/New_York"] The timezone
* @property {Attachment[]} [attachments=[]] Attachments
* @property {doneCallback} [done] Callback when done receiving the message response
* @property {progressCallback} [progress] Callback on message response progress
* @property {string} [model=moonshot.defaultModel()] The model to use
*/
/**
* A moonshot conversation instance.
* @class
* @typedef Conversation
* @classdesc Represents an active moonshot conversation.
*/
export class Conversation {
/**
* The conversation ID
* @property {string}
*/
conversationId
/**
* The conversation name
* @property {string}
*/
name
/**
* The conversation summary (usually empty)
* @property {string}
*/
summary
/**
* The conversation created at
* @property {string}
*/
created_at
/**
* The conversation updated at
* @property {string}
*/
updated_at
/**
* The request function (from parent moonshot instance)
* @property {(url: string, options: object) => Response}
*/
request
/**
* The current model
* @property {string}
*/
model
/**
* If the moonshot client has initialized yet (call `init()` if you haven't and this is false)
* @property {boolean}
*/
ready
/**
* A proxy function/string to connect via
* @property {({endpoint: string, options: Object}) => {endpoint: string, options: Object} | string}
*/
proxy
/**
* A fetch function, defaults to globalThis.fetch
* @property {Function}
*/
fetch
/**
* Create a Conversation instance.
* @param {MoonshotWeb} moonshot - moonshot client instance
* @param {Object} options - Options
* @param {String} options.conversationId - Conversation ID
* @param {String} [options.name] - Conversation name
* @param {String} [options.summary] - Conversation summary
* @param {String} [options.created_at] - Conversation created at
* @param {String} [options.updated_at] - Conversation updated at
* @param {String} [options.model] - moonshot model
*/
constructor(
moonshot,
{ model = 'default', conversationId, name = '', summary = '', created_at, updated_at },
) {
this.moonshot = moonshot
this.conversationId = conversationId
this.request = moonshot.request
if (!this.moonshot) {
throw new Error('moonshot not initialized')
}
if (!this.moonshot.refreshToken) {
throw new Error('moonshot token required, please login at https://kimi.moonshot.cn first')
}
if (!this.conversationId) {
throw new Error('Conversation ID required, are you calling `await moonshot.init()`?')
}
if (model === 'default') {
model = this.moonshot.defaultModel()
}
this.model = model || this.moonshot.defaultModel()
Object.assign(this, {
name,
summary,
created_at: created_at || new Date().toISOString(),
updated_at: updated_at || new Date().toISOString(),
})
}
/**
* Convert the conversation to a JSON object
* @returns {Conversation} The serializable object
*/
toJSON() {
return {
conversationId: this.conversationId,
name: this.name,
summary: this.summary,
created_at: this.created_at,
updated_at: this.updated_at,
model: this.model,
}
}
/**
* Retry the last message in the conversation
* @param {SendMessageParams} [params={}]
* @returns {Promise<MessageStreamChunk>}
*/
async retry(params) {
return this.sendMessage('', { ...params, retry: true })
}
/**
* Send a message to this conversation
* @param {String} message
* @async
* @param {SendMessageParams} params The parameters to send along with the message
* @returns {Promise<MessageStreamChunk>}
*/
async sendMessage(
message,
{
// eslint-disable-next-line no-unused-vars
retry = false,
model = 'default',
done = () => {},
progress = () => {},
// eslint-disable-next-line no-unused-vars
rawResponse = () => {},
signal = null,
} = {},
) {
if (model === 'default') {
model = this.moonshot.defaultModel()
}
// {"messages":[{"role":"user","content":"hello"}],"refs":[],"use_search":true}
const body = { messages: [{ role: 'user', content: message }], refs: [], use_search: true }
let resolve, reject
let returnPromise = new Promise((r, j) => {
resolve = r
reject = j
})
let fullResponse = ''
await fetchSSE(`https://kimi.moonshot.cn/api/chat/${this.conversationId}/completion/stream`, {
method: 'POST',
headers: {
accept: '*/*',
'content-type': 'application/json',
Authorization: `Bearer ${this.moonshot.accessToken}`,
},
signal: signal,
body: JSON.stringify(body),
onMessage(message) {
console.debug('sse message', message)
let parsed
try {
parsed = JSON.parse(message)
} catch (error) {
console.debug('json error', error)
return
}
if (parsed.event === 'cmpl' && parsed.text) fullResponse += parsed.text
const PROGRESS_OBJECT = {
...parsed,
completion: fullResponse,
delta: parsed.text || '',
}
progress(PROGRESS_OBJECT)
if (parsed.event === 'all_done') {
done(PROGRESS_OBJECT)
resolve(PROGRESS_OBJECT)
}
},
async onStart() {},
async onEnd() {
resolve({
completion: fullResponse,
})
},
async onError(resp) {
if (resp instanceof Error) {
reject(resp)
return
}
const error = await resp.json().catch(() => ({}))
reject(
new Error(!isEmpty(error) ? JSON.stringify(error) : `${resp.status} ${resp.statusText}`),
)
},
})
return returnPromise
}
/**
* Delete the conversation
* @async
* @returns Promise<Response>
*/
async delete() {
return await this.request(`/api/chat/chat_conversations/${this.conversationId}`, {
headers: {
accept: '*/*',
Authorization: `Bearer ${this.moonshot.accessToken}`,
Origin: 'https://kimi.moonshot.cn',
},
method: 'DELETE',
}).catch(errorHandle('Delete conversation ' + this.conversationId))
}
/**
* Get all messages in the conversation
* @async
* @returns {Promise<Message[]>}
*/
getMessages() {
return this.getInfo()
.then((a) => a.chat_messages)
.catch(errorHandle('getMessages'))
}
}
/**
* A function that handles errors.
*
* @param {string} msg - The error message.
* @return {function} - A function that logs the error message and exits the process.
*/
function errorHandle(msg) {
return (e) => {
console.error(`Error at: ${msg}`)
console.error(e)
// process.exit(0)
}
}
/**
* @typedef JSONResponse
* @property {'human' | 'assistant'} sender The sender
* @property {string} text The text
* @property {UUID} uuid msg uuid
* @property {string} created_at The message created at
* @property {string} updated_at The message updated at
* @property {string} edited_at When the message was last edited (no editing support via api/web client)
* @property {Attachment[]} attachments The attachments
* @property {string} chat_feedback Feedback
*/
/**
* Message class
* @class
* @classdesc A class representing a message in a Conversation
* @property {Function} request The request function (inherited from moonshot instance)
* @property {JSONResponse} json The JSON representation
* @property {moonshot} moonshot The moonshot instance
* @property {Conversation} conversation The conversation this message belongs to
* @property {UUID} uuid The message uuid
*/
export class Message {
/**
* Create a Message instance.
* @param {Object} params - Params
* @param {Conversation} params.conversation - Conversation instance
* @param {moonshot} params.moonshot - moonshot instance
* @param {Message} message - Message data
*/
constructor(
{ conversation, moonshot },
{ uuid, text, sender, index, updated_at, edited_at, chat_feedback, attachments },
) {
if (!moonshot) {
throw new Error('moonshot not initialized')
}
if (!conversation) {
throw new Error('Conversation not initialized')
}
Object.assign(this, { conversation, moonshot })
this.request = moonshot.request
this.json = { uuid, text, sender, index, updated_at, edited_at, chat_feedback, attachments }
Object.assign(this, this.json)
}
/**
* Convert this message to a JSON representation
* Necessary to prevent circular JSON errors
* @returns {Message}
*/
toJSON() {
return this.json
}
/**
* Returns the value of the "created_at" property as a Date object.
*
* @return {Date} The value of the "created_at" property as a Date object.
*/
get createdAt() {
return new Date(this.json.created_at)
}
/**
* Returns the value of the "updated_at" property as a Date object.
*
* @return {Date} The value of the "updated_at" property as a Date object.
*/
get updatedAt() {
return new Date(this.json.updated_at)
}
/**
* Returns the value of the "edited_at" property as a Date object.
*
* @return {Date} The value of the "edited_at" property as a Date object.
*/
get editedAt() {
return new Date(this.json.edited_at)
}
/**
* Get if message is from the assistant.
* @type {boolean}
*/
get isBot() {
return this.sender === 'assistant'
}
}
/**
* @param {Runtime.Port} port
* @param {string} question
* @param {Session} session
* @param {UserConfig} config
* @param {string} modelName
*/
export async function generateAnswersWithMoonshotWebApi(
port,
question,
session,
config,
modelName,
) {
const bot = new MoonshotWeb({ config })
await bot.init()
const { controller, cleanController } = setAbortController(port)
let answer = ''
const progressFunc = ({ completion }) => {
answer = completion
port.postMessage({ answer: answer, done: false, session: null })
}
const doneFunc = () => {
pushRecord(session, question, answer)
console.debug('conversation history', { content: session.conversationRecords })
port.postMessage({ answer: answer, done: true, session: session })
}
const params = {
progress: progressFunc,
done: doneFunc,
model: Models[modelName].value,
signal: controller.signal,
}
if (!session.moonshot_conversation)
await bot
.startConversation(question, params)
.then((conversation) => {
session.moonshot_conversation = conversation
port.postMessage({ answer: answer, done: true, session: session })
cleanController()
})
.catch((err) => {
cleanController()
throw err
})
else
await bot
.sendMessage(question, {
conversation: session.moonshot_conversation,
...params,
})
.then(cleanController)
.catch((err) => {
cleanController()
throw err
})
}
+3
View File
@@ -26,6 +26,7 @@ import { v4 as uuidv4 } from 'uuid'
* @property {number|null} poe_chatId
* @property {object|null} bard_conversationObj
* @property {object|null} claude_conversation
* @property {object|null} moonshot_conversation
*/
/**
* @param {string|null} question
@@ -82,5 +83,7 @@ export function initSession({
// claude.ai
claude_conversation: null,
// kimi.moonshot.cn
moonshot_conversation: null,
}
}