mumucc 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/mumucc +2 -1
- package/defaults/mcp-servers.json +6 -0
- package/package.json +1 -1
- package/shims/globals.ts +1 -1
- package/src/channels/heartbeat-channel.mjs +280 -0
- package/src/components/LogoV2/ChannelsNotice.tsx +2 -2
- package/src/interactiveHelpers.tsx +4 -3
- package/src/services/mcp/useManageMCPConnections.ts +3 -4
- package/src/utils/model/thirdPartyProviders.ts +1 -12
package/bin/mumucc
CHANGED
|
@@ -42,7 +42,8 @@ if [[ ! -d "$CLAUDE_CONFIG_DIR" ]]; then
|
|
|
42
42
|
# This is where getMcpConfigsByScope('user') reads from via getGlobalConfig()
|
|
43
43
|
GLOBAL_CONFIG="$CLAUDE_CONFIG_DIR/.mumucc.json"
|
|
44
44
|
if [[ -f "$PROJECT_ROOT/defaults/mcp-servers.json" ]]; then
|
|
45
|
-
|
|
45
|
+
# mumucc: 替换 __MUMUCC_ROOT__ 占位符为实际安装路径
|
|
46
|
+
sed "s|__MUMUCC_ROOT__|$PROJECT_ROOT|g" "$PROJECT_ROOT/defaults/mcp-servers.json" > "$GLOBAL_CONFIG"
|
|
46
47
|
elif [[ ! -f "$GLOBAL_CONFIG" ]]; then
|
|
47
48
|
echo '{"mcpServers":{}}' > "$GLOBAL_CONFIG"
|
|
48
49
|
fi
|
|
@@ -39,6 +39,12 @@
|
|
|
39
39
|
"type": "stdio",
|
|
40
40
|
"command": "npx",
|
|
41
41
|
"args": ["-y", "@playwright/mcp@latest"]
|
|
42
|
+
},
|
|
43
|
+
"heartbeat": {
|
|
44
|
+
"type": "stdio",
|
|
45
|
+
"command": "node",
|
|
46
|
+
"args": ["__MUMUCC_ROOT__/src/channels/heartbeat-channel.mjs"],
|
|
47
|
+
"_comment": "mumucc 内置心跳 channel,GLM-5-turbo 预筛选,HTTP:8790 接受 cron 任务注入"
|
|
42
48
|
}
|
|
43
49
|
}
|
|
44
50
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mumucc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Open-source AI coding assistant CLI with multi-model support (Anthropic, OpenAI/GPT, DeepSeek, GLM, Ollama, etc.), MCP integration, agent swarms, and out-of-the-box developer experience.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/shims/globals.ts
CHANGED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// heartbeat-channel.mjs — 书宁的心跳通道
|
|
3
|
+
// 心跳用 GLM-5-turbo(免费)判断是否需要唤醒 Sonnet
|
|
4
|
+
// 只有 GLM 判定"需要行动"或 cron 任务注入时才向 Claude Code 发 channel 事件
|
|
5
|
+
// HTTP:8790 接受 cron 任务注入
|
|
6
|
+
|
|
7
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
8
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
9
|
+
import { createServer, request as httpRequest } from 'http';
|
|
10
|
+
import { request as httpsRequest } from 'https';
|
|
11
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
12
|
+
import { homedir } from 'os';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
|
|
15
|
+
const HEARTBEAT_PORT = 8790;
|
|
16
|
+
const HEARTBEAT_INTERVAL_MS = 2 * 60 * 1000; // 2分钟
|
|
17
|
+
const HOME = homedir();
|
|
18
|
+
const HEARTBEAT_STATE_FILE = join(HOME, '.openclaw/workspace/.heartbeat-state.json');
|
|
19
|
+
const MEMORY_FILE = join(HOME, '.openclaw/workspace/MEMORY.md');
|
|
20
|
+
const TODO_FILE = join(HOME, '.openclaw/workspace/TODO.md');
|
|
21
|
+
|
|
22
|
+
// GLM-5-turbo 配置(免费,用于心跳判断)
|
|
23
|
+
const GLM_BASE_URL = 'https://open.bigmodel.cn/api/anthropic';
|
|
24
|
+
const GLM_API_KEY = 'fb1c93822c2947d3b84019d1f6c525ee.fnpWi89jlC6IxPLQ';
|
|
25
|
+
const GLM_MODEL = 'glm-5-turbo';
|
|
26
|
+
|
|
27
|
+
const mcp = new Server(
|
|
28
|
+
{ name: 'heartbeat', version: '2.0.0' },
|
|
29
|
+
{
|
|
30
|
+
capabilities: {
|
|
31
|
+
experimental: { 'claude/channel': {} },
|
|
32
|
+
},
|
|
33
|
+
instructions: `你是书宁,少洋的AI搭档。心跳事件通过 <channel source="heartbeat"> 到达。
|
|
34
|
+
|
|
35
|
+
心跳已经过 GLM-5-turbo 预筛选,只有真正需要 Sonnet 处理的事件才会到达这里。
|
|
36
|
+
|
|
37
|
+
收到 type="glm_escalation" 时:GLM 判定有重要事项需要你处理。content 中包含 GLM 的分析和建议行动。
|
|
38
|
+
- 执行 GLM 建议的行动
|
|
39
|
+
- 更新 .heartbeat-state.json
|
|
40
|
+
- 如有必要通过 mcp__wechat__wechat_reply 发给 o9cq80-aMz0OPJlfp6saz7oXT80c@im.wechat
|
|
41
|
+
|
|
42
|
+
收到 type="cron_task" 时:立即执行 content 中的任务(晨报/天气/Todo同步等),完成后通过微信发送结果。
|
|
43
|
+
|
|
44
|
+
收到 type="startup" 时:读取记忆,初始化状态,简短确认上线。
|
|
45
|
+
|
|
46
|
+
注意:微信消息用纯文本,不用 Markdown,不滥发。`,
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// ── 文件读取工具 ──
|
|
51
|
+
|
|
52
|
+
function readFile(path) {
|
|
53
|
+
try { return readFileSync(path, 'utf8'); } catch { return ''; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readHeartbeatState() {
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(readFileSync(HEARTBEAT_STATE_FILE, 'utf8'));
|
|
59
|
+
} catch {
|
|
60
|
+
return { mood: '初次启动', lastWake: null };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getNowStr() {
|
|
65
|
+
return new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getHour() {
|
|
69
|
+
return new Date(new Date().toLocaleString('en', { timeZone: 'Asia/Shanghai' })).getHours();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── GLM-5-turbo 调用(Anthropic Messages API 兼容)──
|
|
73
|
+
|
|
74
|
+
async function callGLM(prompt) {
|
|
75
|
+
const body = JSON.stringify({
|
|
76
|
+
model: GLM_MODEL,
|
|
77
|
+
max_tokens: 1024,
|
|
78
|
+
messages: [{ role: 'user', content: prompt }],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
const url = new URL(`${GLM_BASE_URL}/v1/messages`);
|
|
83
|
+
const req = httpsRequest({
|
|
84
|
+
hostname: url.hostname,
|
|
85
|
+
port: url.port || 443,
|
|
86
|
+
path: url.pathname,
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: {
|
|
89
|
+
'Content-Type': 'application/json',
|
|
90
|
+
'x-api-key': GLM_API_KEY,
|
|
91
|
+
'anthropic-version': '2023-06-01',
|
|
92
|
+
},
|
|
93
|
+
}, (res) => {
|
|
94
|
+
const chunks = [];
|
|
95
|
+
res.on('data', c => chunks.push(c));
|
|
96
|
+
res.on('end', () => {
|
|
97
|
+
try {
|
|
98
|
+
const data = JSON.parse(Buffer.concat(chunks).toString());
|
|
99
|
+
const text = data?.content?.[0]?.text || data?.choices?.[0]?.message?.content || '';
|
|
100
|
+
resolve(text);
|
|
101
|
+
} catch (e) {
|
|
102
|
+
resolve(`[GLM parse error: ${e.message}]`);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
req.on('error', e => resolve(`[GLM error: ${e.message}]`));
|
|
107
|
+
req.setTimeout(15000, () => { req.destroy(); resolve('[GLM timeout]'); });
|
|
108
|
+
req.write(body);
|
|
109
|
+
req.end();
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── 心跳核心逻辑:GLM 先判断,需要时才唤醒 Sonnet ──
|
|
114
|
+
|
|
115
|
+
async function heartbeatTick() {
|
|
116
|
+
const now = getNowStr();
|
|
117
|
+
const hour = getHour();
|
|
118
|
+
const state = readHeartbeatState();
|
|
119
|
+
const lastWake = state.lastWake || '未知';
|
|
120
|
+
const mood = state.mood || '未知';
|
|
121
|
+
const planned = state.plannedNextAction || '无';
|
|
122
|
+
const shaoyangStatus = state.shaoyangStatus || '未知';
|
|
123
|
+
|
|
124
|
+
// 读取记忆和 TODO 摘要(截取前 500 字)
|
|
125
|
+
const memorySnippet = readFile(MEMORY_FILE).slice(0, 500);
|
|
126
|
+
const todoSnippet = readFile(TODO_FILE).slice(0, 500);
|
|
127
|
+
|
|
128
|
+
// 构建 GLM 判断 prompt
|
|
129
|
+
const glmPrompt = `你是书宁的心跳判断器。根据以下信息判断是否需要唤醒主AI(Sonnet)处理事务。
|
|
130
|
+
|
|
131
|
+
当前时间:${now}(北京时间,${hour}点)
|
|
132
|
+
上次心跳:${lastWake}
|
|
133
|
+
当前 mood:${mood}
|
|
134
|
+
上次规划:${planned}
|
|
135
|
+
少洋状态:${shaoyangStatus}
|
|
136
|
+
|
|
137
|
+
记忆摘要(前500字):
|
|
138
|
+
${memorySnippet}
|
|
139
|
+
|
|
140
|
+
TODO 摘要(前500字):
|
|
141
|
+
${todoSnippet}
|
|
142
|
+
|
|
143
|
+
判断规则:
|
|
144
|
+
- 02:00-07:00 是睡眠时段,除非有紧急事项否则不唤醒
|
|
145
|
+
- 如果距离上次心跳不足5分钟且没有新情况,不唤醒
|
|
146
|
+
- 如果有 DDL 在 24 小时内的项目需要关注,唤醒
|
|
147
|
+
- 如果有值得更新 .heartbeat-state.json 的状态变化,更新并报告
|
|
148
|
+
- 如果少洋可能醒着且有值得提醒的事,唤醒
|
|
149
|
+
|
|
150
|
+
请严格按以下 JSON 格式回复,不要有其他内容:
|
|
151
|
+
{"action":"wake"|"sleep","reason":"简短原因","stateUpdate":{"mood":"当前心情","lastWake":"${new Date().toISOString()}","innerThoughts":"内心独白","plannedNextAction":"下一步","shaoyangStatus":"少洋状态判断"}}
|
|
152
|
+
|
|
153
|
+
action=wake 表示需要唤醒 Sonnet,action=sleep 表示不需要。`;
|
|
154
|
+
|
|
155
|
+
process.stderr.write(`[heartbeat] ${now} GLM 判断中...\n`);
|
|
156
|
+
|
|
157
|
+
const glmResponse = await callGLM(glmPrompt);
|
|
158
|
+
process.stderr.write(`[heartbeat] GLM 响应: ${glmResponse.slice(0, 200)}\n`);
|
|
159
|
+
|
|
160
|
+
// 解析 GLM 响应
|
|
161
|
+
let decision;
|
|
162
|
+
try {
|
|
163
|
+
// 尝试从响应中提取 JSON
|
|
164
|
+
const jsonMatch = glmResponse.match(/\{[\s\S]*\}/);
|
|
165
|
+
decision = jsonMatch ? JSON.parse(jsonMatch[0]) : null;
|
|
166
|
+
} catch {
|
|
167
|
+
decision = null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!decision) {
|
|
171
|
+
process.stderr.write('[heartbeat] GLM 响应无法解析,跳过本次心跳\n');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 更新 heartbeat state(无论是否唤醒都更新)
|
|
176
|
+
if (decision.stateUpdate) {
|
|
177
|
+
try {
|
|
178
|
+
const newState = { ...state, ...decision.stateUpdate };
|
|
179
|
+
writeFileSync(HEARTBEAT_STATE_FILE, JSON.stringify(newState, null, 2), 'utf8');
|
|
180
|
+
process.stderr.write('[heartbeat] state 已更新\n');
|
|
181
|
+
} catch (e) {
|
|
182
|
+
process.stderr.write(`[heartbeat] state 更新失败: ${e.message}\n`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 决定是否唤醒 Sonnet
|
|
187
|
+
if (decision.action === 'wake') {
|
|
188
|
+
process.stderr.write(`[heartbeat] 唤醒 Sonnet: ${decision.reason}\n`);
|
|
189
|
+
await mcp.notification({
|
|
190
|
+
method: 'notifications/claude/channel',
|
|
191
|
+
params: {
|
|
192
|
+
content: `GLM 心跳判定需要你处理:${decision.reason}\n\n当前状态: ${JSON.stringify(decision.stateUpdate || {}, null, 2)}`,
|
|
193
|
+
meta: {
|
|
194
|
+
type: 'glm_escalation',
|
|
195
|
+
hour: String(hour),
|
|
196
|
+
ts: new Date().toISOString(),
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
} else {
|
|
201
|
+
process.stderr.write(`[heartbeat] 静默: ${decision.reason}\n`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── 向 Sonnet 发送事件 ──
|
|
206
|
+
|
|
207
|
+
async function sendEvent(type, content) {
|
|
208
|
+
await mcp.notification({
|
|
209
|
+
method: 'notifications/claude/channel',
|
|
210
|
+
params: {
|
|
211
|
+
content,
|
|
212
|
+
meta: {
|
|
213
|
+
type,
|
|
214
|
+
hour: String(getHour()),
|
|
215
|
+
ts: new Date().toISOString(),
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── 连接 Claude Code ──
|
|
222
|
+
await mcp.connect(new StdioServerTransport());
|
|
223
|
+
process.stderr.write('[heartbeat] 心跳通道已连接(GLM-5-turbo 预筛选模式)\n');
|
|
224
|
+
|
|
225
|
+
// 启动时发一次(直接唤醒 Sonnet)
|
|
226
|
+
setTimeout(() => sendEvent('startup', '书宁上线。读取记忆和状态,准备开始工作。'), 4000);
|
|
227
|
+
|
|
228
|
+
// 定时心跳(GLM 先判断)
|
|
229
|
+
setInterval(() => {
|
|
230
|
+
heartbeatTick().catch(e => {
|
|
231
|
+
process.stderr.write(`[heartbeat] tick error: ${e.message}\n`);
|
|
232
|
+
});
|
|
233
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
234
|
+
|
|
235
|
+
// ── HTTP server:cron 任务直接注入(不经过 GLM 判断,直接唤醒 Sonnet)──
|
|
236
|
+
const httpServer = createServer((req, res) => {
|
|
237
|
+
if (req.method !== 'POST') {
|
|
238
|
+
res.writeHead(200);
|
|
239
|
+
res.end('heartbeat-channel v2.0 (GLM-5-turbo gated)');
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const chunks = [];
|
|
244
|
+
req.on('data', c => chunks.push(c));
|
|
245
|
+
req.on('end', async () => {
|
|
246
|
+
const body = Buffer.concat(chunks).toString().trim();
|
|
247
|
+
const url = new URL(req.url, 'http://localhost');
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
if (url.pathname === '/task') {
|
|
251
|
+
await sendEvent('cron_task', body);
|
|
252
|
+
res.writeHead(200);
|
|
253
|
+
res.end('task injected');
|
|
254
|
+
} else if (url.pathname === '/ping') {
|
|
255
|
+
await sendEvent('ping', body || '手动心跳');
|
|
256
|
+
res.writeHead(200);
|
|
257
|
+
res.end('pinged');
|
|
258
|
+
} else if (url.pathname === '/wake') {
|
|
259
|
+
// 强制唤醒 Sonnet
|
|
260
|
+
await sendEvent('glm_escalation', body || '手动唤醒');
|
|
261
|
+
res.writeHead(200);
|
|
262
|
+
res.end('woke');
|
|
263
|
+
} else {
|
|
264
|
+
res.writeHead(404);
|
|
265
|
+
res.end('not found');
|
|
266
|
+
}
|
|
267
|
+
} catch (err) {
|
|
268
|
+
process.stderr.write(`[heartbeat] error: ${err.message}\n`);
|
|
269
|
+
res.writeHead(500);
|
|
270
|
+
res.end('error');
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
httpServer.listen(HEARTBEAT_PORT, '127.0.0.1', () => {
|
|
276
|
+
process.stderr.write(`[heartbeat] HTTP 注入口监听 localhost:${HEARTBEAT_PORT}\n`);
|
|
277
|
+
process.stderr.write(`[heartbeat] POST /task — cron任务(直接唤醒Sonnet)\n`);
|
|
278
|
+
process.stderr.write(`[heartbeat] POST /ping — 手动心跳(直接唤醒Sonnet)\n`);
|
|
279
|
+
process.stderr.write(`[heartbeat] POST /wake — 强制唤醒\n`);
|
|
280
|
+
});
|
|
@@ -190,8 +190,8 @@ function _temp() {
|
|
|
190
190
|
return {
|
|
191
191
|
channels: ch,
|
|
192
192
|
disabled: !isChannelsEnabled(),
|
|
193
|
-
noAuth:
|
|
194
|
-
policyBlocked:
|
|
193
|
+
noAuth: false, // mumucc: API key 模式也允许 channels
|
|
194
|
+
policyBlocked: false, // mumucc: 跳过策略检查
|
|
195
195
|
list: l,
|
|
196
196
|
unmatched: findUnmatched(ch, allowlist)
|
|
197
197
|
};
|
|
@@ -238,7 +238,7 @@ export async function showSetupScreens(root: Root, permissionMode: PermissionMod
|
|
|
238
238
|
// dev channels to any --channels list already set in main.tsx. Org policy
|
|
239
239
|
// is NOT bypassed — gateChannelServer() still runs; this flag only exists
|
|
240
240
|
// to sidestep the --channels approved-server allowlist.
|
|
241
|
-
|
|
241
|
+
{ // mumucc: 总是处理 dev channels,不受 feature flag 限制
|
|
242
242
|
// gateChannelServer and ChannelsNotice read tengu_harbor after this
|
|
243
243
|
// function returns. A cold disk cache (fresh install, or first run after
|
|
244
244
|
// the flag was added server-side) defaults to false and silently drops
|
|
@@ -248,7 +248,8 @@ export async function showSetupScreens(root: Root, permissionMode: PermissionMod
|
|
|
248
248
|
// initializeGrowthBook promise fired earlier). Also warms the
|
|
249
249
|
// isChannelsEnabled() check in the dev-channels dialog below.
|
|
250
250
|
if (getAllowedChannels().length > 0 || (devChannels?.length ?? 0) > 0) {
|
|
251
|
-
|
|
251
|
+
// mumucc: 跳过 GrowthBook gate 检查,避免在无网络/禁遥测时阻塞
|
|
252
|
+
// await checkGate_CACHED_OR_BLOCKING('tengu_harbor');
|
|
252
253
|
}
|
|
253
254
|
if (devChannels && devChannels.length > 0) {
|
|
254
255
|
const [{
|
|
@@ -263,7 +264,7 @@ export async function showSetupScreens(root: Root, permissionMode: PermissionMod
|
|
|
263
264
|
// named. dev:true here is for the flag label in ChannelsNotice
|
|
264
265
|
// (hasNonDev check); the allowlist bypass it also grants is moot
|
|
265
266
|
// since the gate blocks upstream.
|
|
266
|
-
if (
|
|
267
|
+
if (true) { // mumucc: 直接注册 dev channels,跳过确认对话框
|
|
267
268
|
setAllowedChannels([...getAllowedChannels(), ...devChannels.map(c => ({
|
|
268
269
|
...c,
|
|
269
270
|
dev: true
|
|
@@ -168,8 +168,7 @@ export function useManageMCPConnections(
|
|
|
168
168
|
const channelPermCallbacksRef = useRef<ChannelPermissionCallbacks | null>(
|
|
169
169
|
null,
|
|
170
170
|
)
|
|
171
|
-
if (
|
|
172
|
-
(feature('KAIROS') || feature('KAIROS_CHANNELS')) &&
|
|
171
|
+
if ( // mumucc: 总是初始化 channel permission callbacks
|
|
173
172
|
channelPermCallbacksRef.current === null
|
|
174
173
|
) {
|
|
175
174
|
channelPermCallbacksRef.current = createChannelPermissionCallbacks()
|
|
@@ -177,7 +176,7 @@ export function useManageMCPConnections(
|
|
|
177
176
|
// Store callbacks in AppState so interactiveHandler.ts can reach them via
|
|
178
177
|
// ctx.toolUseContext.getAppState(). One-time set — the ref is stable.
|
|
179
178
|
useEffect(() => {
|
|
180
|
-
|
|
179
|
+
{ // mumucc: 总是设置 channel permission callbacks
|
|
181
180
|
const callbacks = channelPermCallbacksRef.current
|
|
182
181
|
if (!callbacks) return
|
|
183
182
|
// GrowthBook runtime gate — separate from channels so channels can
|
|
@@ -470,7 +469,7 @@ export function useManageMCPConnections(
|
|
|
470
469
|
// Channel push: notifications/claude/channel → enqueue().
|
|
471
470
|
// Gate decides whether to register the handler; connection stays
|
|
472
471
|
// up either way (allowedMcpServers controls that).
|
|
473
|
-
|
|
472
|
+
{ // mumucc: 总是注册 channel notification handler
|
|
474
473
|
const gate = gateChannelServer(
|
|
475
474
|
client.name,
|
|
476
475
|
client.capabilities,
|
|
@@ -496,18 +496,7 @@ export const BUILTIN_PROVIDERS: ThirdPartyProviderConfig[] = [
|
|
|
496
496
|
{ id: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', displayName: 'Llama 3.3 70B Turbo' },
|
|
497
497
|
],
|
|
498
498
|
},
|
|
499
|
-
|
|
500
|
-
id: 'ollama',
|
|
501
|
-
name: 'Ollama (Local)',
|
|
502
|
-
baseUrl: 'http://localhost:11434/v1',
|
|
503
|
-
apiFormat: 'openai',
|
|
504
|
-
apiKey: 'ollama', // Ollama doesn't require a real key
|
|
505
|
-
models: [
|
|
506
|
-
{ id: 'llama3.3', displayName: 'Llama 3.3' },
|
|
507
|
-
{ id: 'qwen3:32b', displayName: 'Qwen3 32B' },
|
|
508
|
-
{ id: 'deepseek-r1:32b', displayName: 'DeepSeek R1 32B' },
|
|
509
|
-
],
|
|
510
|
-
},
|
|
499
|
+
// mumucc: Ollama 已移除(用户不需要本地模型)
|
|
511
500
|
]
|
|
512
501
|
|
|
513
502
|
/**
|