@neomei/agent-soul-framework 4.5.6 → 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 +315 -0
- 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 +122 -38
- 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 +3 -4
- 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
|
@@ -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
|
}
|
|
@@ -325,13 +335,27 @@ async function cmdSetup() {
|
|
|
325
335
|
copyExamples(join(cwd, '.opencode'));
|
|
326
336
|
copyExamples(join(cwd, 'memory'));
|
|
327
337
|
copyExamples(join(cwd, 'knowledge'));
|
|
338
|
+
// 复制 OpenCode 配置文件
|
|
339
|
+
const srcOcDir = join(PACKAGE_ROOT, '.opencode');
|
|
340
|
+
if (existsSync(srcOcDir)) {
|
|
341
|
+
if (!existsSync(join(cwd, '.opencode')))
|
|
342
|
+
mkdirSync(join(cwd, '.opencode'), { recursive: true });
|
|
343
|
+
for (const f of ['opencode.json', 'config.json']) {
|
|
344
|
+
const src = join(srcOcDir, f + '.example');
|
|
345
|
+
const dst = join(cwd, '.opencode', f);
|
|
346
|
+
if (existsSync(src) && !existsSync(dst)) {
|
|
347
|
+
copyFileSync(src, dst);
|
|
348
|
+
copied++;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
328
352
|
// 复制 OpenCode tools (记忆搜索等)
|
|
329
353
|
const srcToolsDir = join(PACKAGE_ROOT, '.opencode', 'tools');
|
|
330
354
|
const dstToolsDir = join(cwd, '.opencode', 'tools');
|
|
331
355
|
if (existsSync(srcToolsDir)) {
|
|
332
356
|
if (!existsSync(dstToolsDir))
|
|
333
357
|
mkdirSync(dstToolsDir, { recursive: true });
|
|
334
|
-
for (const f of ['search-memory.mjs']) {
|
|
358
|
+
for (const f of ['search-memory.mjs', 'read-plugin.js']) {
|
|
335
359
|
const src = join(srcToolsDir, f);
|
|
336
360
|
const dst = join(dstToolsDir, f);
|
|
337
361
|
if (existsSync(src) && !existsSync(dst)) {
|
|
@@ -365,8 +389,12 @@ async function cmdSetup() {
|
|
|
365
389
|
try {
|
|
366
390
|
const { execSync } = await import('node:child_process');
|
|
367
391
|
const cronLine = '*/30 * * * * cd ' + cwd + ' && ./heartbeat_wrapper.sh';
|
|
368
|
-
const
|
|
369
|
-
|
|
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')) {
|
|
370
398
|
const newCron = existing.trim() + (existing.trim() ? '\n' : '') + cronLine + '\n';
|
|
371
399
|
execSync('printf "%s" "$1" | crontab -', { encoding: 'utf-8', env: { ...process.env, '1': newCron } });
|
|
372
400
|
console.log(' 📅 心跳调度: crontab 已配置');
|
|
@@ -378,7 +406,7 @@ async function cmdSetup() {
|
|
|
378
406
|
catch {
|
|
379
407
|
console.log(' 📅 心跳调度: 跳过');
|
|
380
408
|
}
|
|
381
|
-
const feishuConfig = join(
|
|
409
|
+
const feishuConfig = join(homedir(), '.config', 'opencode', 'feishu.json');
|
|
382
410
|
if (!existsSync(feishuConfig)) {
|
|
383
411
|
console.log('\n 📱 飞书连接配置\n ' + '─'.repeat(40));
|
|
384
412
|
console.log(' 未检测到飞书配置,现在开始设置...\n');
|
|
@@ -399,7 +427,8 @@ async function cmdSetup() {
|
|
|
399
427
|
if (existsSync(src) && !existsSync(dst)) {
|
|
400
428
|
copyFileSync(src, dst);
|
|
401
429
|
try {
|
|
402
|
-
|
|
430
|
+
if (process.platform !== 'win32')
|
|
431
|
+
execSync('chmod +x ' + dst, { stdio: 'ignore', timeout: 3000 });
|
|
403
432
|
}
|
|
404
433
|
catch { }
|
|
405
434
|
}
|
|
@@ -441,7 +470,8 @@ async function cmdSetup() {
|
|
|
441
470
|
if (existsSync(src) && !existsSync(dst)) {
|
|
442
471
|
copyFileSync(src, dst);
|
|
443
472
|
try {
|
|
444
|
-
|
|
473
|
+
if (process.platform !== 'win32')
|
|
474
|
+
esc2('chmod +x ' + dst, { stdio: 'ignore', timeout: 3000 });
|
|
445
475
|
}
|
|
446
476
|
catch { }
|
|
447
477
|
}
|
|
@@ -458,8 +488,14 @@ async function cmdSetup() {
|
|
|
458
488
|
catch { }
|
|
459
489
|
try {
|
|
460
490
|
const { execSync } = await import('node:child_process');
|
|
461
|
-
|
|
462
|
-
|
|
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')) {
|
|
463
499
|
console.log(' 📱 飞书桥接: 正在启动...');
|
|
464
500
|
execSync('opencode-feishu start --daemon', { stdio: 'ignore', timeout: 10000 });
|
|
465
501
|
console.log(' 📱 飞书桥接: 已启动 ✅');
|
|
@@ -472,7 +508,7 @@ async function cmdSetup() {
|
|
|
472
508
|
console.log(' 📱 飞书桥接: 请手动运行 opencode-feishu start --daemon');
|
|
473
509
|
}
|
|
474
510
|
}
|
|
475
|
-
const qiweiConfig = join(
|
|
511
|
+
const qiweiConfig = join(homedir(), '.config', 'opencode', 'qiwei.json');
|
|
476
512
|
if (!existsSync(qiweiConfig)) {
|
|
477
513
|
console.log('\n 💬 企业微信配置\n ' + '─'.repeat(40));
|
|
478
514
|
console.log(' 未检测到企微配置。');
|
|
@@ -481,12 +517,51 @@ async function cmdSetup() {
|
|
|
481
517
|
}
|
|
482
518
|
else {
|
|
483
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 { }
|
|
484
553
|
try {
|
|
485
554
|
const { execSync } = await import('node:child_process');
|
|
486
|
-
|
|
555
|
+
let qstatus;
|
|
556
|
+
try {
|
|
557
|
+
qstatus = execSync('opencode-qiwei status', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
qstatus = 'stopped';
|
|
561
|
+
}
|
|
487
562
|
if (qstatus.includes('stopped') || qstatus.includes('not running')) {
|
|
488
563
|
console.log(' 💬 企微桥接: 正在启动...');
|
|
489
|
-
execSync('opencode-qiwei start', { stdio: 'ignore', timeout: 10000 });
|
|
564
|
+
execSync('opencode-qiwei start --daemon', { stdio: 'ignore', timeout: 10000 });
|
|
490
565
|
console.log(' 💬 企微桥接: 已启动 ✅');
|
|
491
566
|
}
|
|
492
567
|
else {
|
|
@@ -494,7 +569,7 @@ async function cmdSetup() {
|
|
|
494
569
|
}
|
|
495
570
|
}
|
|
496
571
|
catch {
|
|
497
|
-
console.log(' 💬 企微桥接: 请手动运行 opencode-qiwei start');
|
|
572
|
+
console.log(' 💬 企微桥接: 请手动运行 opencode-qiwei start --daemon');
|
|
498
573
|
}
|
|
499
574
|
}
|
|
500
575
|
try {
|
|
@@ -543,8 +618,14 @@ async function startFeishu(feishuConfig) {
|
|
|
543
618
|
return;
|
|
544
619
|
try {
|
|
545
620
|
const { execSync } = await import('node:child_process');
|
|
546
|
-
|
|
547
|
-
|
|
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')) {
|
|
548
629
|
console.log(' 📱 飞书桥接: 启动中...');
|
|
549
630
|
execSync('opencode-feishu start --daemon', { stdio: 'ignore', timeout: 10000 });
|
|
550
631
|
console.log(' 📱 飞书桥接: 已启动 ✅');
|
|
@@ -562,9 +643,15 @@ async function startQiwei(qiweiConfig) {
|
|
|
562
643
|
return;
|
|
563
644
|
try {
|
|
564
645
|
const { execSync } = await import('node:child_process');
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
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 });
|
|
568
655
|
console.log(' 💬 企微桥接: 已启动 ✅');
|
|
569
656
|
}
|
|
570
657
|
else {
|
|
@@ -611,15 +698,11 @@ async function cmdStart() {
|
|
|
611
698
|
console.log(' ⚠️ 请手动启动: opencode serve --port 19876');
|
|
612
699
|
}
|
|
613
700
|
}
|
|
614
|
-
const feishuConfig = join(
|
|
615
|
-
const qiweiConfig = join(
|
|
701
|
+
const feishuConfig = join(homedir(), '.config', 'opencode', 'feishu.json');
|
|
702
|
+
const qiweiConfig = join(homedir(), '.config', 'opencode', 'qiwei.json');
|
|
616
703
|
const hasFeishu = existsSync(feishuConfig);
|
|
617
704
|
const hasQiwei = existsSync(qiweiConfig);
|
|
618
|
-
if (hasFeishu && hasQiwei) {
|
|
619
|
-
await startFeishu(feishuConfig);
|
|
620
|
-
await startQiwei(qiweiConfig);
|
|
621
|
-
}
|
|
622
|
-
else if (!hasFeishu && !hasQiwei) {
|
|
705
|
+
if (!hasFeishu && !hasQiwei) {
|
|
623
706
|
console.log('\n 📡 选择通信通道:\n');
|
|
624
707
|
console.log(' 1. 飞书(推荐)— 扫码自动配置');
|
|
625
708
|
console.log(' 2. 企业微信 — 手动输入 botId + secret');
|
|
@@ -660,9 +743,10 @@ async function cmdStart() {
|
|
|
660
743
|
if (answer)
|
|
661
744
|
await setupQiweiInteractive();
|
|
662
745
|
}
|
|
663
|
-
|
|
746
|
+
// Re-check after setup may have created config files
|
|
747
|
+
if (existsSync(feishuConfig))
|
|
664
748
|
await startFeishu(feishuConfig);
|
|
665
|
-
if (
|
|
749
|
+
if (existsSync(qiweiConfig))
|
|
666
750
|
await startQiwei(qiweiConfig);
|
|
667
751
|
try {
|
|
668
752
|
const { execSync } = await import('node:child_process');
|