feat: 初始化画个标仓库 - AI驱动设计系统(BYOK模式) - 224源/371504图标+25海报源+130技能
This commit is contained in:
+114
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* 画个标 · 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 };
|
||||
})();
|
||||
Reference in New Issue
Block a user