@neomei/agent-soul-framework 4.5.7 → 4.5.14
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/.opencode/tools/read-plugin.js +4 -4
- package/.opencode/tools/search-memory.mjs +258 -39
- package/AGENTS.md.example +8 -6
- package/bin/hunqi +8 -1
- package/bin/hunqi-knowledge +8 -1
- package/connectors/feishu/hooks/on-session-created.js +6 -0
- package/connectors/feishu/hooks/on-session-created.sh +3 -94
- package/connectors/feishu/hooks/on-session-idle.js +6 -0
- package/connectors/feishu/hooks/on-session-idle.sh +3 -56
- package/connectors/feishu/scripts/session-cleanup.sh +10 -8
- package/connectors/feishu/stop.sh +12 -0
- package/connectors/feishu/systemd/hunqi-core@.service +1 -1
- package/connectors/feishu/watchdog.sh +2 -2
- package/connectors/qiwei/hooks/on-session-created.js +6 -0
- package/connectors/qiwei/hooks/on-session-created.sh +6 -0
- package/connectors/qiwei/hooks/on-session-idle.js +6 -0
- package/connectors/qiwei/hooks/on-session-idle.sh +6 -0
- package/connectors/qiwei/scripts/session-cleanup.sh +74 -0
- package/connectors/qiwei/start.sh +91 -0
- package/connectors/qiwei/systemd/channel-qiwei@.service +63 -0
- package/dist/cli/hunqi.js +107 -37
- package/dist/cli/hunqi.js.map +1 -1
- package/dist/content-filter.d.ts +15 -0
- package/dist/content-filter.js +105 -0
- package/dist/content-filter.js.map +1 -0
- package/dist/heartbeat/runner.js +8 -1
- package/dist/heartbeat/runner.js.map +1 -1
- package/dist/memory/manager.js +12 -2
- package/dist/memory/manager.js.map +1 -1
- package/dist/memory/search.js +32 -10
- package/dist/memory/search.js.map +1 -1
- package/dist/memory/structured.d.ts +1 -1
- package/dist/memory/structured.js +31 -18
- package/dist/memory/structured.js.map +1 -1
- package/dist/plugin/index.d.ts +1 -0
- package/dist/plugin/index.js +24 -9
- package/dist/plugin/index.js.map +1 -1
- package/heartbeat_wrapper.sh +5 -5
- package/package.json +2 -3
- package/plugin/index.js +26 -9
- package/plugin/manifest.json +3 -2
- package/scripts/session-cleanup.sh +2 -2
- package/connectors/feishu/hooks/query-memory.mjs +0 -27
|
@@ -25,7 +25,7 @@ Environment="USER=%I"
|
|
|
25
25
|
EnvironmentFile=-%h/.hunqi/.env
|
|
26
26
|
|
|
27
27
|
# 启动命令 - 使用 wrapper 解析 opencode 位置并启动 serve
|
|
28
|
-
ExecStart=/bin/sh -c 'OPENCODE=$(
|
|
28
|
+
ExecStart=/bin/sh -c 'OPENCODE=$(sh -c "opencode-feishu --resolve-opencode 2>/dev/null || opencode-qiwei --resolve-opencode 2>/dev/null" || command -v opencode); exec "$OPENCODE" serve --port 19876'
|
|
29
29
|
|
|
30
30
|
# 停止命令(精确匹配端口,避免误杀)
|
|
31
31
|
ExecStop=/bin/sh -c 'PID=$(ss -tlnp 2>/dev/null | grep ":19876 " | sed -n "s/.*pid=\\([0-9]*\\).*/\\1/p"); [ -n "$PID" ] && kill "$PID" || true'
|
|
@@ -59,7 +59,7 @@ while true; do
|
|
|
59
59
|
continue
|
|
60
60
|
fi
|
|
61
61
|
|
|
62
|
-
RUNNING=$(echo "$STATUS" | python3 -c "import sys,json; print(json.load(sys.stdin).get('running',False))" 2>/dev/null)
|
|
62
|
+
RUNNING=$(echo "$STATUS" | (python3 -c "import sys,json; print(json.load(sys.stdin).get('running',False))" 2>/dev/null || python -c "import sys,json; print(json.load(sys.stdin).get('running',False))" 2>/dev/null || echo "False"))
|
|
63
63
|
if [ "$RUNNING" != "True" ]; then
|
|
64
64
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] opencode-feishu 未在运行,启动..."
|
|
65
65
|
$FEISHU_CMD start --daemon
|
|
@@ -79,7 +79,7 @@ while true; do
|
|
|
79
79
|
continue
|
|
80
80
|
fi
|
|
81
81
|
|
|
82
|
-
LAST_TIME=$(echo "$LAST_MSG" | python3 -c "import sys,json; print(json.load(sys.stdin).get('time',0))" 2>/dev/null)
|
|
82
|
+
LAST_TIME=$(echo "$LAST_MSG" | (python3 -c "import sys,json; print(json.load(sys.stdin).get('time',0))" 2>/dev/null || python -c "import sys,json; print(json.load(sys.stdin).get('time',0))" 2>/dev/null || echo "0"))
|
|
83
83
|
if [ -z "$LAST_TIME" ] || [ "$LAST_TIME" = "0" ]; then
|
|
84
84
|
LAST_TIME_SEC=$NOW
|
|
85
85
|
else
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// on-session-created.js — session hook (cross-platform)
|
|
2
|
+
// 灵魂注入已统一由 hunqi-plugin(OpenCode 插件)处理,此脚本保留用于未来扩展
|
|
3
|
+
// 环境变量: HOOK_SESSION_ID, HOOK_OPENCODE_URL
|
|
4
|
+
|
|
5
|
+
const sessionId = process.env.HOOK_SESSION_ID || "?";
|
|
6
|
+
console.log(`[hook:on-session-created] session=${sessionId} — 灵魂注入由 hunqi-plugin 统一处理 ✅`);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// on-session-idle.js — session hook (cross-platform)
|
|
2
|
+
// 灵魂注入已统一由 hunqi-plugin(OpenCode 插件)处理,此脚本保留用于未来扩展
|
|
3
|
+
// 环境变量: HOOK_SESSION_ID, HOOK_OPENCODE_URL
|
|
4
|
+
|
|
5
|
+
const sessionId = process.env.HOOK_SESSION_ID || "?";
|
|
6
|
+
console.log(`[hook:on-session-idle] session=${sessionId} — 灵魂注入由 hunqi-plugin 统一处理 ✅`);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# session-cleanup.sh — 清理消息过多的飞书 session 映射
|
|
3
|
+
# 当 session 消息数超过阈值时,从 feishu-sessions.json 中移除映射
|
|
4
|
+
# 下次对话会自动创建新 session
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
SESSIONS_FILE="${HOME}/.config/opencode/feishu-sessions.json"
|
|
9
|
+
# Cross-platform DB path
|
|
10
|
+
if [ -d "${HOME}/Library/Application Support/opencode" ]; then
|
|
11
|
+
DB_FILE="${HOME}/Library/Application Support/opencode/opencode.db"
|
|
12
|
+
elif [ -d "${HOME}/.local/share/opencode" ]; then
|
|
13
|
+
DB_FILE="${HOME}/.local/share/opencode/opencode.db"
|
|
14
|
+
else
|
|
15
|
+
DB_FILE="${HOME}/.local/share/opencode/opencode.db"
|
|
16
|
+
fi
|
|
17
|
+
MAX_MESSAGES="${MAX_MESSAGES_PER_SESSION:-50}"
|
|
18
|
+
|
|
19
|
+
if [ ! -f "$SESSIONS_FILE" ]; then
|
|
20
|
+
echo "[session-cleanup] feishu-sessions.json not found"
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
if [ ! -f "$DB_FILE" ]; then
|
|
25
|
+
echo "[session-cleanup] opencode.db not found"
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# 读取当前 sessions
|
|
30
|
+
SESSIONS=$(cat "$SESSIONS_FILE")
|
|
31
|
+
|
|
32
|
+
# 使用单一 Python 脚本安全处理所有操作,避免注入
|
|
33
|
+
python3 - "$SESSIONS_FILE" "$DB_FILE" "$MAX_MESSAGES" <<'PYEOF'
|
|
34
|
+
import json, sqlite3, sys, os
|
|
35
|
+
|
|
36
|
+
sessions_file = sys.argv[1]
|
|
37
|
+
db_file = sys.argv[2]
|
|
38
|
+
max_messages = int(sys.argv[3])
|
|
39
|
+
|
|
40
|
+
with open(sessions_file, "r") as f:
|
|
41
|
+
data = json.load(f)
|
|
42
|
+
|
|
43
|
+
sessions = data.get("sessions", [])
|
|
44
|
+
if not sessions:
|
|
45
|
+
print("[session-cleanup] No sessions found")
|
|
46
|
+
sys.exit(0)
|
|
47
|
+
|
|
48
|
+
conn = sqlite3.connect(db_file)
|
|
49
|
+
try:
|
|
50
|
+
cleaned = 0
|
|
51
|
+
remaining = []
|
|
52
|
+
for s in sessions:
|
|
53
|
+
sid = s.get("sessionId", "")
|
|
54
|
+
cursor = conn.execute("SELECT COUNT(*) FROM message WHERE session_id = ?", (sid,))
|
|
55
|
+
msg_count = cursor.fetchone()[0]
|
|
56
|
+
|
|
57
|
+
if msg_count > max_messages:
|
|
58
|
+
print(f"[session-cleanup] {sid} has {msg_count} messages > {max_messages}, removing mapping")
|
|
59
|
+
cleaned += 1
|
|
60
|
+
else:
|
|
61
|
+
print(f"[session-cleanup] {sid} has {msg_count} messages <= {max_messages}, keeping")
|
|
62
|
+
remaining.append(s)
|
|
63
|
+
|
|
64
|
+
if cleaned > 0:
|
|
65
|
+
data["sessions"] = remaining
|
|
66
|
+
with open(sessions_file, "w") as f:
|
|
67
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
68
|
+
print(f"[session-cleanup] Cleaned {cleaned} session(s), next message will create new session")
|
|
69
|
+
else:
|
|
70
|
+
print(f"[session-cleanup] All sessions are within limit ({max_messages} messages)")
|
|
71
|
+
finally:
|
|
72
|
+
conn.close()
|
|
73
|
+
PYEOF
|
|
74
|
+
# Note: Python script above handles all output and exit codes directly
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# hunqi-qiwei - 魂器企业微信连接器启动脚本
|
|
4
|
+
# 只启动 opencode-qiwei 插件,依赖 hunqi-core 提供的 OpenCode server
|
|
5
|
+
#
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
|
+
PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
10
|
+
|
|
11
|
+
if [ -f "$PROJECT_DIR/.env" ]; then
|
|
12
|
+
set -a && source "$PROJECT_DIR/.env" && set +a
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
cd "$PROJECT_DIR"
|
|
16
|
+
|
|
17
|
+
OPENCODE_PORT="${OPENCODE_PORT:-19876}"
|
|
18
|
+
|
|
19
|
+
# 自动检测 opencode-qiwei 启动方式(npm link 在重启后经常断裂)
|
|
20
|
+
resolve_opencode_qiwei() {
|
|
21
|
+
# 1. 全局命令
|
|
22
|
+
if command -v opencode-qiwei &>/dev/null; then
|
|
23
|
+
echo "opencode-qiwei"
|
|
24
|
+
return 0
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# 2. npx
|
|
28
|
+
if command -v npx &>/dev/null; then
|
|
29
|
+
echo "npx opencode-qiwei"
|
|
30
|
+
return 0
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# 3. npm 全局路径
|
|
34
|
+
if command -v npm &>/dev/null; then
|
|
35
|
+
local npm_prefix
|
|
36
|
+
npm_prefix=$(npm prefix -g 2>/dev/null)
|
|
37
|
+
if [ -n "$npm_prefix" ] && [ -x "$npm_prefix/bin/opencode-qiwei" ]; then
|
|
38
|
+
echo "$npm_prefix/bin/opencode-qiwei"
|
|
39
|
+
return 0
|
|
40
|
+
fi
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# 4. 源码回退(开发环境)
|
|
44
|
+
local src_paths=(
|
|
45
|
+
"/home/$USER/文档/projects/opencode-qiwei/bin/opencode-qiwei"
|
|
46
|
+
"$PROJECT_DIR/../opencode-qiwei/bin/opencode-qiwei"
|
|
47
|
+
)
|
|
48
|
+
for p in "${src_paths[@]}"; do
|
|
49
|
+
if [ -f "$p" ]; then
|
|
50
|
+
local node_cmd
|
|
51
|
+
node_cmd=$(command -v node 2>/dev/null || echo "node")
|
|
52
|
+
echo "$node_cmd $p"
|
|
53
|
+
return 0
|
|
54
|
+
fi
|
|
55
|
+
done
|
|
56
|
+
|
|
57
|
+
return 1
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
echo "🚀 魂器企业微信连接器 (hunqi-qiwei)"
|
|
61
|
+
echo ""
|
|
62
|
+
|
|
63
|
+
# 检查 opencode-qiwei 是否可用
|
|
64
|
+
QIWEI_CMD=$(resolve_opencode_qiwei) || {
|
|
65
|
+
echo "❌ opencode-qiwei 未安装"
|
|
66
|
+
echo ""
|
|
67
|
+
echo "请安装:"
|
|
68
|
+
echo " npm install -g @neomei/opencode-qiwei"
|
|
69
|
+
exit 1
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# 检查 OpenCode server 是否已运行
|
|
73
|
+
echo "检查 OpenCode server (port $OPENCODE_PORT)..."
|
|
74
|
+
if ! curl -s http://localhost:$OPENCODE_PORT/session >/dev/null 2>&1; then
|
|
75
|
+
echo "❌ OpenCode server 未运行"
|
|
76
|
+
echo ""
|
|
77
|
+
echo "hunqi-qiwei 依赖 hunqi-core,请先启动核心:"
|
|
78
|
+
echo " ./connectors/feishu/core-start.sh (核心启动脚本跨频道通用)"
|
|
79
|
+
echo ""
|
|
80
|
+
echo "或使用 systemd 同时启动两者:"
|
|
81
|
+
echo " sudo systemctl start hunqi-core@\$USER"
|
|
82
|
+
echo " sudo systemctl start channel-qiwei@\$USER"
|
|
83
|
+
exit 1
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
echo "✅ OpenCode server 已就绪"
|
|
87
|
+
echo ""
|
|
88
|
+
|
|
89
|
+
# 启动企业微信桥接
|
|
90
|
+
echo "启动企业微信桥接(流式消息 + 权限交互 + 魂器集成)..."
|
|
91
|
+
exec $QIWEI_CMD start --daemon
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=魂器企业微信频道 (Channel Qiwei) - Hunqi Core 的企微接入
|
|
3
|
+
# 注意:install-systemd.sh 安装时会将 %h 替换为实际家目录。
|
|
4
|
+
# 系统级 systemd 中 %h 展开为 /root(而非 User= 指定的用户),不可直接使用。
|
|
5
|
+
Documentation=https://github.com/NeoMei/agent-soul-framework
|
|
6
|
+
After=network-online.target hunqi-core@%i.service
|
|
7
|
+
Wants=network-online.target
|
|
8
|
+
|
|
9
|
+
# 强依赖核心服务
|
|
10
|
+
Requires=hunqi-core@%i.service
|
|
11
|
+
After=hunqi-core@%i.service
|
|
12
|
+
|
|
13
|
+
# 挂起后重启
|
|
14
|
+
After=suspend.target hibernate.target hybrid-sleep.target
|
|
15
|
+
|
|
16
|
+
[Service]
|
|
17
|
+
Type=simple
|
|
18
|
+
User=%I
|
|
19
|
+
Group=%I
|
|
20
|
+
|
|
21
|
+
# 工作目录
|
|
22
|
+
WorkingDirectory=%h/.hunqi/agent-soul-framework/packages/agent-soul-framework
|
|
23
|
+
|
|
24
|
+
# 加载环境变量
|
|
25
|
+
Environment="HOME=%h"
|
|
26
|
+
Environment="USER=%I"
|
|
27
|
+
|
|
28
|
+
# 从 .env 文件加载环境变量
|
|
29
|
+
EnvironmentFile=-%h/.hunqi/.env
|
|
30
|
+
|
|
31
|
+
# 启动前:清理消息过多的 session 映射(防止上下文无限膨胀)
|
|
32
|
+
ExecStartPre=-/bin/sh -c '%h/.hunqi/agent-soul-framework/packages/agent-soul-framework/connectors/qiwei/scripts/session-cleanup.sh'
|
|
33
|
+
|
|
34
|
+
# 启动前等待核心就绪
|
|
35
|
+
ExecStartPre=/bin/sh -c 'for i in $(seq 1 30); do if curl -s http://localhost:19876/session >/dev/null 2>&1; then exit 0; fi; sleep 1; done; exit 1'
|
|
36
|
+
|
|
37
|
+
# 启动命令 - 使用 ~/.hunqi/bin 下的 wrapper,避免依赖 nvm 版本
|
|
38
|
+
ExecStart=%h/.hunqi/bin/opencode-qiwei start --daemon
|
|
39
|
+
|
|
40
|
+
# 停止命令
|
|
41
|
+
ExecStop=/bin/sh -c 'pkill -f "opencode-qiwei start" 2>/dev/null || true; sleep 1; pkill -9 -f "opencode-qiwei start" 2>/dev/null || true'
|
|
42
|
+
|
|
43
|
+
# 重载配置(修改 qiwei.json 后执行 systemctl reload 即可生效)
|
|
44
|
+
ExecReload=/bin/sh -c '%h/.hunqi/bin/opencode-qiwei stop; sleep 2; %h/.hunqi/bin/opencode-qiwei start --daemon'
|
|
45
|
+
|
|
46
|
+
# 重启策略
|
|
47
|
+
Restart=always
|
|
48
|
+
RestartSec=5
|
|
49
|
+
StartLimitInterval=60s
|
|
50
|
+
StartLimitBurst=3
|
|
51
|
+
|
|
52
|
+
# 日志
|
|
53
|
+
StandardOutput=journal
|
|
54
|
+
StandardError=journal
|
|
55
|
+
SyslogIdentifier=channel-qiwei
|
|
56
|
+
|
|
57
|
+
# 进程管理
|
|
58
|
+
KillMode=mixed
|
|
59
|
+
KillSignal=SIGTERM
|
|
60
|
+
TimeoutStopSec=30
|
|
61
|
+
|
|
62
|
+
[Install]
|
|
63
|
+
WantedBy=multi-user.target
|
package/dist/cli/hunqi.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync, readFileSync, mkdirSync, copyFileSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { join } from 'node:path';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
8
9
|
const PACKAGE_ROOT = join(import.meta.dirname, '..', '..');
|
|
9
10
|
function loadJSON(filepath) {
|
|
10
11
|
try {
|
|
@@ -18,7 +19,7 @@ function findSkillsPackage() {
|
|
|
18
19
|
const candidates = [
|
|
19
20
|
join(PACKAGE_ROOT, '..', 'agent-soul-skills'),
|
|
20
21
|
join(PACKAGE_ROOT, 'node_modules', '@neomei', 'agent-soul-skills'),
|
|
21
|
-
join(
|
|
22
|
+
join(homedir(), '.config', 'agent-soul-framework', 'skills'),
|
|
22
23
|
];
|
|
23
24
|
for (const root of candidates) {
|
|
24
25
|
if (existsSync(join(root, 'skills', 'skill-creator', 'scripts', 'skill_creator.py')))
|
|
@@ -50,7 +51,7 @@ async function cmdStatus() {
|
|
|
50
51
|
}
|
|
51
52
|
try {
|
|
52
53
|
const { execSync } = await import('node:child_process');
|
|
53
|
-
const ocVer = execSync('opencode --version
|
|
54
|
+
const ocVer = execSync((process.platform === 'win32' ? 'opencode.cmd' : 'opencode') + ' --version', { encoding: 'utf-8' }).trim();
|
|
54
55
|
console.log(' OpenCode: ' + ocVer);
|
|
55
56
|
}
|
|
56
57
|
catch {
|
|
@@ -156,7 +157,8 @@ async function cmdSkillCreate(args) {
|
|
|
156
157
|
const flag = args.includes('--dry-run') ? '--dry-run' : args.includes('--force') ? '--force' : '';
|
|
157
158
|
try {
|
|
158
159
|
const { execSync } = await import('node:child_process');
|
|
159
|
-
|
|
160
|
+
const pyCmd = process.platform === 'win32' ? 'python' : 'python3';
|
|
161
|
+
execSync(pyCmd + ' ' + join(skillsRoot, 'skills', 'skill-creator', 'scripts', 'skill_creator.py') + ' ' + flag, {
|
|
160
162
|
cwd: process.cwd(), stdio: 'inherit', timeout: 120000
|
|
161
163
|
});
|
|
162
164
|
}
|
|
@@ -169,7 +171,12 @@ async function cmdInteractive() {
|
|
|
169
171
|
const { execSync } = await import('node:child_process');
|
|
170
172
|
const script = join(PACKAGE_ROOT, 'agent-soul-framework.sh');
|
|
171
173
|
if (existsSync(script)) {
|
|
172
|
-
|
|
174
|
+
if (process.platform === 'win32') {
|
|
175
|
+
console.log(' ⚠️ TUI 模式在 Windows 上不支持,请使用 opencode serve 模式');
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
execSync('bash ' + script + ' interactive', { cwd: process.cwd(), stdio: 'inherit' });
|
|
179
|
+
}
|
|
173
180
|
return;
|
|
174
181
|
}
|
|
175
182
|
}
|
|
@@ -187,7 +194,7 @@ async function cmdDoctor() {
|
|
|
187
194
|
const cwd = process.cwd();
|
|
188
195
|
let checkDir = cwd;
|
|
189
196
|
if (!existsSync(join(checkDir, 'soul', 'SOUL.md'))) {
|
|
190
|
-
const defaultDir = join(
|
|
197
|
+
const defaultDir = join(homedir(), '.agent-soul-framework');
|
|
191
198
|
if (existsSync(join(defaultDir, 'soul', 'SOUL.md'))) {
|
|
192
199
|
checkDir = defaultDir;
|
|
193
200
|
}
|
|
@@ -197,7 +204,7 @@ async function cmdDoctor() {
|
|
|
197
204
|
console.log(' Node.js: ' + nodeVer + ' ✅');
|
|
198
205
|
try {
|
|
199
206
|
const { execSync } = await import('node:child_process');
|
|
200
|
-
const ocVer = execSync('opencode --version
|
|
207
|
+
const ocVer = execSync((process.platform === 'win32' ? 'opencode.cmd' : 'opencode') + ' --version', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
201
208
|
console.log(' OpenCode: ' + ocVer + ' ✅');
|
|
202
209
|
}
|
|
203
210
|
catch {
|
|
@@ -212,13 +219,13 @@ async function cmdDoctor() {
|
|
|
212
219
|
}
|
|
213
220
|
try {
|
|
214
221
|
const { execSync } = await import('node:child_process');
|
|
215
|
-
const fv = execSync('opencode-feishu --version
|
|
222
|
+
const fv = execSync((process.platform === 'win32' ? 'opencode-feishu.cmd' : 'opencode-feishu') + ' --version', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
216
223
|
console.log(' opencode-feishu: ' + fv + ' ✅');
|
|
217
224
|
}
|
|
218
225
|
catch {
|
|
219
226
|
console.log(' opencode-feishu: 未安装 ⚠️');
|
|
220
227
|
}
|
|
221
|
-
const feishuConfig = join(
|
|
228
|
+
const feishuConfig = join(homedir(), '.config', 'opencode', 'feishu.json');
|
|
222
229
|
if (existsSync(feishuConfig)) {
|
|
223
230
|
try {
|
|
224
231
|
const cfg = JSON.parse(readFileSync(feishuConfig, 'utf-8'));
|
|
@@ -231,8 +238,8 @@ async function cmdDoctor() {
|
|
|
231
238
|
else {
|
|
232
239
|
console.log(' 飞书配置: 未配置 ⚠️ (opencode-feishu setup)');
|
|
233
240
|
}
|
|
234
|
-
const
|
|
235
|
-
if (existsSync(
|
|
241
|
+
const qiweiCfg = join(homedir(), '.config', 'opencode', 'qiwei.json');
|
|
242
|
+
if (existsSync(qiweiCfg)) {
|
|
236
243
|
console.log(' 企微配置: 已配置 ✅');
|
|
237
244
|
}
|
|
238
245
|
else {
|
|
@@ -266,7 +273,10 @@ async function cmdDoctor() {
|
|
|
266
273
|
}
|
|
267
274
|
try {
|
|
268
275
|
const { execSync } = await import('node:child_process');
|
|
269
|
-
const
|
|
276
|
+
const isWin = process.platform === 'win32';
|
|
277
|
+
const cron = isWin
|
|
278
|
+
? ''
|
|
279
|
+
: execSync('crontab -l 2>/dev/null || echo ""', { encoding: 'utf-8', timeout: 5000 });
|
|
270
280
|
if (cron.includes('heartbeat_wrapper')) {
|
|
271
281
|
console.log(' 心跳调度: crontab 已配置 ✅');
|
|
272
282
|
}
|
|
@@ -291,7 +301,7 @@ async function cmdDoctor() {
|
|
|
291
301
|
async function cmdSetup() {
|
|
292
302
|
let cwd = process.cwd();
|
|
293
303
|
if (!existsSync(join(cwd, 'soul', 'SOUL.md'))) {
|
|
294
|
-
const defaultDir = join(
|
|
304
|
+
const defaultDir = join(homedir(), '.agent-soul-framework');
|
|
295
305
|
if (existsSync(join(defaultDir, 'soul', 'SOUL.md'))) {
|
|
296
306
|
cwd = defaultDir;
|
|
297
307
|
}
|
|
@@ -379,8 +389,12 @@ async function cmdSetup() {
|
|
|
379
389
|
try {
|
|
380
390
|
const { execSync } = await import('node:child_process');
|
|
381
391
|
const cronLine = '*/30 * * * * cd ' + cwd + ' && ./heartbeat_wrapper.sh';
|
|
382
|
-
const
|
|
383
|
-
|
|
392
|
+
const isWin2 = process.platform === 'win32';
|
|
393
|
+
const existing = isWin2 ? '' : execSync('crontab -l 2>/dev/null || echo ""', { encoding: 'utf-8' });
|
|
394
|
+
if (isWin2) {
|
|
395
|
+
console.log(' 📅 心跳调度: Windows 请用 Task Scheduler 替代 crontab');
|
|
396
|
+
}
|
|
397
|
+
else if (!existing.includes('heartbeat_wrapper.sh')) {
|
|
384
398
|
const newCron = existing.trim() + (existing.trim() ? '\n' : '') + cronLine + '\n';
|
|
385
399
|
execSync('printf "%s" "$1" | crontab -', { encoding: 'utf-8', env: { ...process.env, '1': newCron } });
|
|
386
400
|
console.log(' 📅 心跳调度: crontab 已配置');
|
|
@@ -392,7 +406,7 @@ async function cmdSetup() {
|
|
|
392
406
|
catch {
|
|
393
407
|
console.log(' 📅 心跳调度: 跳过');
|
|
394
408
|
}
|
|
395
|
-
const feishuConfig = join(
|
|
409
|
+
const feishuConfig = join(homedir(), '.config', 'opencode', 'feishu.json');
|
|
396
410
|
if (!existsSync(feishuConfig)) {
|
|
397
411
|
console.log('\n 📱 飞书连接配置\n ' + '─'.repeat(40));
|
|
398
412
|
console.log(' 未检测到飞书配置,现在开始设置...\n');
|
|
@@ -413,7 +427,8 @@ async function cmdSetup() {
|
|
|
413
427
|
if (existsSync(src) && !existsSync(dst)) {
|
|
414
428
|
copyFileSync(src, dst);
|
|
415
429
|
try {
|
|
416
|
-
|
|
430
|
+
if (process.platform !== 'win32')
|
|
431
|
+
execSync('chmod +x ' + dst, { stdio: 'ignore', timeout: 3000 });
|
|
417
432
|
}
|
|
418
433
|
catch { }
|
|
419
434
|
}
|
|
@@ -455,7 +470,8 @@ async function cmdSetup() {
|
|
|
455
470
|
if (existsSync(src) && !existsSync(dst)) {
|
|
456
471
|
copyFileSync(src, dst);
|
|
457
472
|
try {
|
|
458
|
-
|
|
473
|
+
if (process.platform !== 'win32')
|
|
474
|
+
esc2('chmod +x ' + dst, { stdio: 'ignore', timeout: 3000 });
|
|
459
475
|
}
|
|
460
476
|
catch { }
|
|
461
477
|
}
|
|
@@ -472,8 +488,14 @@ async function cmdSetup() {
|
|
|
472
488
|
catch { }
|
|
473
489
|
try {
|
|
474
490
|
const { execSync } = await import('node:child_process');
|
|
475
|
-
|
|
476
|
-
|
|
491
|
+
let feishuStatus;
|
|
492
|
+
try {
|
|
493
|
+
feishuStatus = execSync('opencode-feishu status', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
feishuStatus = 'stopped';
|
|
497
|
+
}
|
|
498
|
+
if (feishuStatus.includes('stopped') || feishuStatus.includes('not running')) {
|
|
477
499
|
console.log(' 📱 飞书桥接: 正在启动...');
|
|
478
500
|
execSync('opencode-feishu start --daemon', { stdio: 'ignore', timeout: 10000 });
|
|
479
501
|
console.log(' 📱 飞书桥接: 已启动 ✅');
|
|
@@ -486,7 +508,7 @@ async function cmdSetup() {
|
|
|
486
508
|
console.log(' 📱 飞书桥接: 请手动运行 opencode-feishu start --daemon');
|
|
487
509
|
}
|
|
488
510
|
}
|
|
489
|
-
const qiweiConfig = join(
|
|
511
|
+
const qiweiConfig = join(homedir(), '.config', 'opencode', 'qiwei.json');
|
|
490
512
|
if (!existsSync(qiweiConfig)) {
|
|
491
513
|
console.log('\n 💬 企业微信配置\n ' + '─'.repeat(40));
|
|
492
514
|
console.log(' 未检测到企微配置。');
|
|
@@ -495,12 +517,51 @@ async function cmdSetup() {
|
|
|
495
517
|
}
|
|
496
518
|
else {
|
|
497
519
|
console.log(' 💬 企微连接: 已配置');
|
|
520
|
+
// 补充 hooks 配置(如果缺失)
|
|
521
|
+
try {
|
|
522
|
+
const { execSync: esc3 } = await import('node:child_process');
|
|
523
|
+
const qiweiRaw = readFileSync(qiweiConfig, 'utf-8');
|
|
524
|
+
const qiweiJson = JSON.parse(qiweiRaw);
|
|
525
|
+
if (!qiweiJson.hooks || !qiweiJson.hooks.onSessionCreated) {
|
|
526
|
+
const hooksDir = join(cwd, 'connectors', 'qiwei', 'hooks');
|
|
527
|
+
const srcHooksDir = join(PACKAGE_ROOT, 'connectors', 'qiwei', 'hooks');
|
|
528
|
+
if (existsSync(srcHooksDir)) {
|
|
529
|
+
if (!existsSync(hooksDir))
|
|
530
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
531
|
+
for (const f of ['on-session-created.sh', 'on-session-idle.sh']) {
|
|
532
|
+
const src = join(srcHooksDir, f);
|
|
533
|
+
const dst = join(hooksDir, f);
|
|
534
|
+
if (existsSync(src) && !existsSync(dst)) {
|
|
535
|
+
copyFileSync(src, dst);
|
|
536
|
+
try {
|
|
537
|
+
if (process.platform !== 'win32')
|
|
538
|
+
esc3('chmod +x ' + dst, { stdio: 'ignore', timeout: 3000 });
|
|
539
|
+
}
|
|
540
|
+
catch { }
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
qiweiJson.hooks = {
|
|
545
|
+
onSessionCreated: 'connectors/qiwei/hooks/on-session-created.sh',
|
|
546
|
+
onSessionIdle: 'connectors/qiwei/hooks/on-session-idle.sh',
|
|
547
|
+
};
|
|
548
|
+
writeFileSync(qiweiConfig, JSON.stringify(qiweiJson, null, 2));
|
|
549
|
+
console.log(' 🪝 灵魂 hooks: 已注入 qiwei.json ✅');
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
catch { }
|
|
498
553
|
try {
|
|
499
554
|
const { execSync } = await import('node:child_process');
|
|
500
|
-
|
|
555
|
+
let qstatus;
|
|
556
|
+
try {
|
|
557
|
+
qstatus = execSync('opencode-qiwei status', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
qstatus = 'stopped';
|
|
561
|
+
}
|
|
501
562
|
if (qstatus.includes('stopped') || qstatus.includes('not running')) {
|
|
502
563
|
console.log(' 💬 企微桥接: 正在启动...');
|
|
503
|
-
execSync('opencode-qiwei start', { stdio: 'ignore', timeout: 10000 });
|
|
564
|
+
execSync('opencode-qiwei start --daemon', { stdio: 'ignore', timeout: 10000 });
|
|
504
565
|
console.log(' 💬 企微桥接: 已启动 ✅');
|
|
505
566
|
}
|
|
506
567
|
else {
|
|
@@ -508,7 +569,7 @@ async function cmdSetup() {
|
|
|
508
569
|
}
|
|
509
570
|
}
|
|
510
571
|
catch {
|
|
511
|
-
console.log(' 💬 企微桥接: 请手动运行 opencode-qiwei start');
|
|
572
|
+
console.log(' 💬 企微桥接: 请手动运行 opencode-qiwei start --daemon');
|
|
512
573
|
}
|
|
513
574
|
}
|
|
514
575
|
try {
|
|
@@ -557,8 +618,14 @@ async function startFeishu(feishuConfig) {
|
|
|
557
618
|
return;
|
|
558
619
|
try {
|
|
559
620
|
const { execSync } = await import('node:child_process');
|
|
560
|
-
|
|
561
|
-
|
|
621
|
+
let feishuStat;
|
|
622
|
+
try {
|
|
623
|
+
feishuStat = execSync('opencode-feishu status', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
feishuStat = 'stopped';
|
|
627
|
+
}
|
|
628
|
+
if (feishuStat.includes('stopped') || feishuStat.includes('not running')) {
|
|
562
629
|
console.log(' 📱 飞书桥接: 启动中...');
|
|
563
630
|
execSync('opencode-feishu start --daemon', { stdio: 'ignore', timeout: 10000 });
|
|
564
631
|
console.log(' 📱 飞书桥接: 已启动 ✅');
|
|
@@ -576,9 +643,15 @@ async function startQiwei(qiweiConfig) {
|
|
|
576
643
|
return;
|
|
577
644
|
try {
|
|
578
645
|
const { execSync } = await import('node:child_process');
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
execSync('opencode-qiwei
|
|
646
|
+
let qiweiStat;
|
|
647
|
+
try {
|
|
648
|
+
qiweiStat = execSync('opencode-qiwei status', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
649
|
+
}
|
|
650
|
+
catch {
|
|
651
|
+
qiweiStat = 'stopped';
|
|
652
|
+
}
|
|
653
|
+
if (qiweiStat.includes('stopped') || qiweiStat.includes('not running')) {
|
|
654
|
+
execSync('opencode-qiwei start --daemon', { stdio: 'ignore', timeout: 10000 });
|
|
582
655
|
console.log(' 💬 企微桥接: 已启动 ✅');
|
|
583
656
|
}
|
|
584
657
|
else {
|
|
@@ -625,15 +698,11 @@ async function cmdStart() {
|
|
|
625
698
|
console.log(' ⚠️ 请手动启动: opencode serve --port 19876');
|
|
626
699
|
}
|
|
627
700
|
}
|
|
628
|
-
const feishuConfig = join(
|
|
629
|
-
const qiweiConfig = join(
|
|
701
|
+
const feishuConfig = join(homedir(), '.config', 'opencode', 'feishu.json');
|
|
702
|
+
const qiweiConfig = join(homedir(), '.config', 'opencode', 'qiwei.json');
|
|
630
703
|
const hasFeishu = existsSync(feishuConfig);
|
|
631
704
|
const hasQiwei = existsSync(qiweiConfig);
|
|
632
|
-
if (hasFeishu && hasQiwei) {
|
|
633
|
-
await startFeishu(feishuConfig);
|
|
634
|
-
await startQiwei(qiweiConfig);
|
|
635
|
-
}
|
|
636
|
-
else if (!hasFeishu && !hasQiwei) {
|
|
705
|
+
if (!hasFeishu && !hasQiwei) {
|
|
637
706
|
console.log('\n 📡 选择通信通道:\n');
|
|
638
707
|
console.log(' 1. 飞书(推荐)— 扫码自动配置');
|
|
639
708
|
console.log(' 2. 企业微信 — 手动输入 botId + secret');
|
|
@@ -674,9 +743,10 @@ async function cmdStart() {
|
|
|
674
743
|
if (answer)
|
|
675
744
|
await setupQiweiInteractive();
|
|
676
745
|
}
|
|
677
|
-
|
|
746
|
+
// Re-check after setup may have created config files
|
|
747
|
+
if (existsSync(feishuConfig))
|
|
678
748
|
await startFeishu(feishuConfig);
|
|
679
|
-
if (
|
|
749
|
+
if (existsSync(qiweiConfig))
|
|
680
750
|
await startQiwei(qiweiConfig);
|
|
681
751
|
try {
|
|
682
752
|
const { execSync } = await import('node:child_process');
|