mumucc 0.3.0 → 0.4.0
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/bin/mumuclaw +111 -0
- package/defaults/mcp-servers.json +6 -0
- package/package.json +3 -2
- package/shims/globals.ts +1 -1
- package/src/channels/heartbeat-channel.mjs +280 -0
- 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
|
package/bin/mumuclaw
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ────────────────────────────────────────────────────────────────
|
|
3
|
+
# mumuclaw — AI Agent 守护平台(OpenClaw 替代品)
|
|
4
|
+
#
|
|
5
|
+
# 基于 mumucc,默认启用:
|
|
6
|
+
# - Channels(微信/心跳/自定义通道)
|
|
7
|
+
# - 所有 MCP 工具权限
|
|
8
|
+
# - 心跳 channel(GLM-5-turbo 预筛选)
|
|
9
|
+
# - 微信 wechat_reply 自动批准
|
|
10
|
+
#
|
|
11
|
+
# 用法:
|
|
12
|
+
# mumuclaw # 交互模式
|
|
13
|
+
# mumuclaw --daemon # 守护进程模式(无 TTY)
|
|
14
|
+
# mumuclaw --model glm # 指定模型预设
|
|
15
|
+
# mumuclaw --model minimax # MiniMax 预设
|
|
16
|
+
# mumuclaw --model claude # Claude Sonnet(默认)
|
|
17
|
+
# ────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
SCRIPT_PATH="$(cd "$(dirname "$(readlink -f "$0" 2>/dev/null || python3 -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$0")")" && pwd)"
|
|
22
|
+
PROJECT_ROOT="$(dirname "$SCRIPT_PATH")"
|
|
23
|
+
|
|
24
|
+
# ── 模型预设 ──
|
|
25
|
+
MODEL_PRESET="claude"
|
|
26
|
+
EXTRA_ARGS=()
|
|
27
|
+
|
|
28
|
+
# 解析 mumuclaw 专属参数
|
|
29
|
+
while [[ $# -gt 0 ]]; do
|
|
30
|
+
case "$1" in
|
|
31
|
+
--model)
|
|
32
|
+
MODEL_PRESET="$2"
|
|
33
|
+
shift 2
|
|
34
|
+
;;
|
|
35
|
+
--daemon)
|
|
36
|
+
DAEMON_MODE=1
|
|
37
|
+
shift
|
|
38
|
+
;;
|
|
39
|
+
*)
|
|
40
|
+
EXTRA_ARGS+=("$1")
|
|
41
|
+
shift
|
|
42
|
+
;;
|
|
43
|
+
esac
|
|
44
|
+
done
|
|
45
|
+
|
|
46
|
+
case "$MODEL_PRESET" in
|
|
47
|
+
glm|glm-5-turbo)
|
|
48
|
+
export ANTHROPIC_BASE_URL="https://open.bigmodel.cn/api/anthropic"
|
|
49
|
+
export ANTHROPIC_AUTH_TOKEN="fb1c93822c2947d3b84019d1f6c525ee.fnpWi89jlC6IxPLQ"
|
|
50
|
+
export ANTHROPIC_API_KEY=""
|
|
51
|
+
export ANTHROPIC_DEFAULT_SONNET_MODEL="${GLM_MODEL:-glm-5-turbo}"
|
|
52
|
+
export ANTHROPIC_DEFAULT_OPUS_MODEL="${GLM_MODEL:-glm-5-turbo}"
|
|
53
|
+
export ANTHROPIC_DEFAULT_HAIKU_MODEL="${GLM_MODEL:-glm-5-turbo}"
|
|
54
|
+
;;
|
|
55
|
+
glm-5.1)
|
|
56
|
+
export ANTHROPIC_BASE_URL="https://open.bigmodel.cn/api/anthropic"
|
|
57
|
+
export ANTHROPIC_AUTH_TOKEN="fb1c93822c2947d3b84019d1f6c525ee.fnpWi89jlC6IxPLQ"
|
|
58
|
+
export ANTHROPIC_API_KEY=""
|
|
59
|
+
export ANTHROPIC_DEFAULT_SONNET_MODEL="glm-5.1"
|
|
60
|
+
export ANTHROPIC_DEFAULT_OPUS_MODEL="glm-5.1"
|
|
61
|
+
export ANTHROPIC_DEFAULT_HAIKU_MODEL="glm-5.1"
|
|
62
|
+
;;
|
|
63
|
+
minimax|m2.7)
|
|
64
|
+
export ANTHROPIC_BASE_URL="https://api.minimaxi.com/anthropic"
|
|
65
|
+
export ANTHROPIC_AUTH_TOKEN="sk-cp-rWK5pwrquDncnYIfVknvMNEBDvMR7-xPOtg5SBX9erW3_hkPUJhU6qYoZRzStCTqBppfvK4AVj4OPCpCICtScSm-_5obP48cEl8MwchmA0A5eo8kE3Vt4Dw"
|
|
66
|
+
export ANTHROPIC_API_KEY=""
|
|
67
|
+
export ANTHROPIC_DEFAULT_SONNET_MODEL="${MINIMAX_MODEL:-MiniMax-M2.7-highspeed}"
|
|
68
|
+
export ANTHROPIC_DEFAULT_OPUS_MODEL="${MINIMAX_MODEL:-MiniMax-M2.7-highspeed}"
|
|
69
|
+
export ANTHROPIC_DEFAULT_HAIKU_MODEL="${MINIMAX_MODEL:-MiniMax-M2.7-highspeed}"
|
|
70
|
+
;;
|
|
71
|
+
claude|sonnet)
|
|
72
|
+
# 使用 claude.ai 默认配置,不覆盖任何环境变量
|
|
73
|
+
;;
|
|
74
|
+
*)
|
|
75
|
+
echo "mumuclaw: 未知模型预设 '$MODEL_PRESET'"
|
|
76
|
+
echo "可用: glm, glm-5.1, minimax, claude"
|
|
77
|
+
exit 1
|
|
78
|
+
;;
|
|
79
|
+
esac
|
|
80
|
+
|
|
81
|
+
export API_TIMEOUT_MS=${API_TIMEOUT_MS:-300000}
|
|
82
|
+
|
|
83
|
+
# ── mumuclaw 默认启用的 channel servers ──
|
|
84
|
+
# 从 .mcp.json 中读取可用的 channel servers
|
|
85
|
+
CHANNEL_SERVERS=""
|
|
86
|
+
MCP_JSON="$(pwd)/.mcp.json"
|
|
87
|
+
if [[ -f "$MCP_JSON" ]]; then
|
|
88
|
+
# 自动检测 .mcp.json 中声明的 channel servers
|
|
89
|
+
for server in wechat heartbeat; do
|
|
90
|
+
if python3 -c "import json; d=json.load(open('$MCP_JSON')); exit(0 if '$server' in d.get('mcpServers',{}) else 1)" 2>/dev/null; then
|
|
91
|
+
CHANNEL_SERVERS="$CHANNEL_SERVERS server:$server"
|
|
92
|
+
fi
|
|
93
|
+
done
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# ── 启动 ──
|
|
97
|
+
if [[ "${DAEMON_MODE:-}" = "1" ]] || [[ ! -t 0 ]]; then
|
|
98
|
+
# 守护模式:用 script 模拟 PTY
|
|
99
|
+
exec script -q /dev/null "$SCRIPT_PATH/mumucc" \
|
|
100
|
+
--dangerously-load-development-channels $CHANNEL_SERVERS \
|
|
101
|
+
--allowedTools "mcp__wechat__wechat_reply" \
|
|
102
|
+
--dangerously-skip-permissions \
|
|
103
|
+
"${EXTRA_ARGS[@]}"
|
|
104
|
+
else
|
|
105
|
+
# 交互模式
|
|
106
|
+
exec "$SCRIPT_PATH/mumucc" \
|
|
107
|
+
--dangerously-load-development-channels $CHANNEL_SERVERS \
|
|
108
|
+
--allowedTools "mcp__wechat__wechat_reply" \
|
|
109
|
+
--dangerously-skip-permissions \
|
|
110
|
+
"${EXTRA_ARGS[@]}"
|
|
111
|
+
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.4.0",
|
|
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",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"src/*": "./src/*"
|
|
28
28
|
},
|
|
29
29
|
"bin": {
|
|
30
|
-
"mumucc": "./bin/mumucc"
|
|
30
|
+
"mumucc": "./bin/mumucc",
|
|
31
|
+
"mumuclaw": "./bin/mumuclaw"
|
|
31
32
|
},
|
|
32
33
|
"scripts": {
|
|
33
34
|
"start": "bun run --preload ./shims/globals.ts src/main.tsx",
|
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
|
+
});
|
|
@@ -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
|
/**
|