115 lines
3.8 KiB
JavaScript
115 lines
3.8 KiB
JavaScript
/**
|
|
* 画个标 · AI 客户端:DeepSeek API 接入
|
|
* 职责:API Key 管理(localStorage)、对话调用、错误分类、连接测试
|
|
*/
|
|
const HGB_AI = (function () {
|
|
'use strict';
|
|
|
|
const LS_KEY = 'hgb_ai_key';
|
|
const LS_MODEL = 'hgb_ai_model';
|
|
const ENDPOINT = 'https://api.deepseek.com/chat/completions';
|
|
const MODELS = ['deepseek-v4-flash', 'deepseek-chat', 'deepseek-v4-pro'];
|
|
|
|
/**
|
|
* 不内置任何 Key:Key 仅由使用者在「AI 设置」中填写,存本机 localStorage,只发给 DeepSeek。
|
|
* 因此源码、仓库与部署产物里都不含 Key,公网也无法被他人读取(BYOK 模式)。
|
|
*/
|
|
const DEFAULT_KEY = '';
|
|
|
|
/* ---------- Key 管理 ---------- */
|
|
function getKey() {
|
|
try {
|
|
const v = localStorage.getItem(LS_KEY);
|
|
// 未配置时返回空串,由调用方提示「未配置 API Key」并降级为内置引擎
|
|
if (v) return v;
|
|
return DEFAULT_KEY;
|
|
} catch (e) { return ''; }
|
|
}
|
|
function setKey(key) {
|
|
try { localStorage.setItem(LS_KEY, (key || '').trim()); } catch (e) { /* ignore */ }
|
|
}
|
|
function hasKey() { return !!getKey(); }
|
|
function getModel() {
|
|
try { return localStorage.getItem(LS_MODEL) || 'deepseek-v4-flash'; } catch (e) { return 'deepseek-v4-flash'; }
|
|
}
|
|
function setModel(m) { try { localStorage.setItem(LS_MODEL, m); } catch (e) { /* ignore */ } }
|
|
function clearKey() {
|
|
// 清除本机保存的 Key(已无内置 Key 可回落,清除后即降级为内置引擎)
|
|
try { localStorage.removeItem(LS_KEY); } catch (e) { /* ignore */ }
|
|
}
|
|
|
|
/* ---------- 调用 ---------- */
|
|
/**
|
|
* 调用 DeepSeek 对话补全
|
|
* @param {Array} messages [{role:'system'|'user', content}]
|
|
* @param {object} opts { json?: boolean, maxTokens?: number, temperature?: number }
|
|
* @returns {Promise<string>} 返回内容文本
|
|
*/
|
|
async function chat(messages, opts) {
|
|
const key = getKey();
|
|
if (!key) {
|
|
const err = new Error('未配置 API Key,请先在「AI 设置」中填写');
|
|
err.code = 'NO_KEY';
|
|
throw err;
|
|
}
|
|
opts = opts || {};
|
|
const body = {
|
|
model: getModel(),
|
|
messages,
|
|
max_tokens: opts.maxTokens || 4096,
|
|
temperature: opts.temperature != null ? opts.temperature : 0.5,
|
|
stream: false
|
|
};
|
|
if (opts.json) {
|
|
body.response_format = { type: 'json_object' };
|
|
}
|
|
|
|
let res;
|
|
try {
|
|
res = await fetch(ENDPOINT, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': 'Bearer ' + key
|
|
},
|
|
body: JSON.stringify(body)
|
|
});
|
|
} catch (e) {
|
|
const err = new Error('网络请求失败:' + e.message + '(请检查网络/代理)');
|
|
err.code = 'NETWORK';
|
|
throw err;
|
|
}
|
|
|
|
if (!res.ok) {
|
|
let detail = '';
|
|
try { detail = (await res.json()).error?.message || ''; } catch (e) { /* ignore */ }
|
|
const err = new Error(detail || 'HTTP ' + res.status);
|
|
if (res.status === 401) err.code = 'AUTH';
|
|
else if (res.status === 402) err.code = 'BALANCE';
|
|
else if (res.status === 429) err.code = 'RATE_LIMIT';
|
|
else err.code = 'API';
|
|
err.status = res.status;
|
|
throw err;
|
|
}
|
|
|
|
const data = await res.json();
|
|
const content = data.choices?.[0]?.message?.content;
|
|
if (!content) {
|
|
const err = new Error('AI 返回内容为空');
|
|
err.code = 'EMPTY';
|
|
throw err;
|
|
}
|
|
return content;
|
|
}
|
|
|
|
/** 测试连接:发送最小请求验证 Key 是否有效 */
|
|
async function testConnection() {
|
|
const content = await chat([
|
|
{ role: 'user', content: '回复两个字:正常' }
|
|
], { maxTokens: 16, temperature: 0 });
|
|
return content;
|
|
}
|
|
|
|
return { getKey, setKey, hasKey, getModel, setModel, clearKey, chat, testConnection, MODELS };
|
|
})();
|