feat(portable): 预装国内渠道+本地模型自动发现+4个新技能

- release.yml 便携包离线预装 钉钉/飞书中国版/企业微信 插件(借鉴 openclaw-china 生态)
- config-server 新增 /api/local-models:自动探测 Ollama/LM Studio 本地模型(借鉴 RealShocky/openclaw-windows)
- Config.html 检测到本地模型自动生成卡片、免 Key 点选即用;新增钉钉渠道卡片
- skills-cn 新增 pdf-toolkit/web-to-markdown/qrcode-maker/image-compress(13→17)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hfshfg
2026-06-17 22:45:59 +08:00
parent 086fd35b29
commit 8bffa5d9d7
7 changed files with 352 additions and 4 deletions

View File

@@ -443,6 +443,40 @@ const server = http.createServer((req, res) => {
return;
}
// API: Discover local models (Ollama / LM Studio)
// 借鉴 RealShocky/openclaw-windows自动探测本机已装的本地模型
// 用户无需手填 baseUrl/模型名,直接点选即可(便携版纯离线推理卖点)。
// 静默失败:探测不到就返回空数组,不影响 Config 页面。
if (req.url === '/api/local-models' && req.method === 'GET') {
(async () => {
const probes = [
{ provider: 'ollama', label: 'Ollama', base: 'http://127.0.0.1:11434/v1', api: 'http://127.0.0.1:11434/api/tags' },
{ provider: 'lmstudio', label: 'LM Studio', base: 'http://127.0.0.1:1234/v1', api: 'http://127.0.0.1:1234/v1/models' },
];
const found = [];
await Promise.all(probes.map(async (p) => {
try {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), 1200);
const r = await fetch(p.api, { signal: ctrl.signal });
clearTimeout(t);
if (!r.ok) return;
const data = await r.json();
// Ollama: { models:[{name}] } | LM Studio (OpenAI-style): { data:[{id}] }
const models = Array.isArray(data.models)
? data.models.map(m => m.name).filter(Boolean)
: Array.isArray(data.data)
? data.data.map(m => m.id).filter(Boolean)
: [];
if (models.length) found.push({ provider: p.provider, label: p.label, base: p.base, models });
} catch { /* 探测失败:该 provider 未运行,跳过 */ }
}));
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ providers: found }));
})();
return;
}
// API: Save config
if (req.url === '/api/config' && req.method === 'POST') {
let body = '';