@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.
Files changed (43) hide show
  1. package/.opencode/tools/read-plugin.js +4 -4
  2. package/.opencode/tools/search-memory.mjs +315 -0
  3. package/AGENTS.md.example +8 -6
  4. package/bin/hunqi +8 -1
  5. package/bin/hunqi-knowledge +8 -1
  6. package/connectors/feishu/hooks/on-session-created.js +6 -0
  7. package/connectors/feishu/hooks/on-session-created.sh +3 -94
  8. package/connectors/feishu/hooks/on-session-idle.js +6 -0
  9. package/connectors/feishu/hooks/on-session-idle.sh +3 -56
  10. package/connectors/feishu/scripts/session-cleanup.sh +10 -8
  11. package/connectors/feishu/stop.sh +12 -0
  12. package/connectors/feishu/systemd/hunqi-core@.service +1 -1
  13. package/connectors/feishu/watchdog.sh +2 -2
  14. package/connectors/qiwei/hooks/on-session-created.js +6 -0
  15. package/connectors/qiwei/hooks/on-session-created.sh +6 -0
  16. package/connectors/qiwei/hooks/on-session-idle.js +6 -0
  17. package/connectors/qiwei/hooks/on-session-idle.sh +6 -0
  18. package/connectors/qiwei/scripts/session-cleanup.sh +74 -0
  19. package/connectors/qiwei/start.sh +91 -0
  20. package/connectors/qiwei/systemd/channel-qiwei@.service +63 -0
  21. package/dist/cli/hunqi.js +122 -38
  22. package/dist/cli/hunqi.js.map +1 -1
  23. package/dist/content-filter.d.ts +15 -0
  24. package/dist/content-filter.js +105 -0
  25. package/dist/content-filter.js.map +1 -0
  26. package/dist/heartbeat/runner.js +8 -1
  27. package/dist/heartbeat/runner.js.map +1 -1
  28. package/dist/memory/manager.js +12 -2
  29. package/dist/memory/manager.js.map +1 -1
  30. package/dist/memory/search.js +32 -10
  31. package/dist/memory/search.js.map +1 -1
  32. package/dist/memory/structured.d.ts +1 -1
  33. package/dist/memory/structured.js +31 -18
  34. package/dist/memory/structured.js.map +1 -1
  35. package/dist/plugin/index.d.ts +1 -0
  36. package/dist/plugin/index.js +24 -9
  37. package/dist/plugin/index.js.map +1 -1
  38. package/heartbeat_wrapper.sh +5 -5
  39. package/package.json +3 -4
  40. package/plugin/index.js +26 -9
  41. package/plugin/manifest.json +3 -2
  42. package/scripts/session-cleanup.sh +2 -2
  43. 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,6 @@
1
+ #!/usr/bin/env bash
2
+ # on-session-idle.sh — session idle 事件 hook
3
+ # 灵魂注入已统一由 hunqi-plugin(OpenCode 插件)处理,此脚本保留用于未来扩展
4
+ # 环境变量: HOOK_SESSION_ID, HOOK_OPENCODE_URL
5
+
6
+ echo "[hook:onSessionIdle] session=${HOOK_SESSION_ID} — 灵魂注入由 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(process.env.HOME || '/', '.config', 'agent-soul-framework', 'skills'),
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 2>&1', { encoding: 'utf-8' }).trim();
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
- execSync('python3 ' + join(skillsRoot, 'skills', 'skill-creator', 'scripts', 'skill_creator.py') + ' ' + flag, {
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
- execSync('bash ' + script + ' interactive', { cwd: process.cwd(), stdio: 'inherit' });
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(process.env.HOME || '~', '.agent-soul-framework');
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 2>&1', { encoding: 'utf-8', timeout: 5000 }).trim();
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 2>&1', { encoding: 'utf-8', timeout: 5000 }).trim();
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(process.env.HOME || '~', '.config', 'opencode', 'feishu.json');
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 qiweiConfig_d = join(process.env.HOME || '~', '.config', 'opencode', 'qiwei.json');
235
- if (existsSync(qiweiConfig_d)) {
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 cron = execSync('crontab -l 2>/dev/null || echo ""', { encoding: 'utf-8', timeout: 5000 });
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(process.env.HOME || '~', '.agent-soul-framework');
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 existing = execSync('crontab -l 2>/dev/null || echo ""', { encoding: 'utf-8' });
369
- if (!existing.includes('heartbeat_wrapper.sh')) {
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(process.env.HOME || '~', '.config', 'opencode', 'feishu.json');
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
- execSync('chmod +x ' + dst, { stdio: 'ignore', timeout: 3000 });
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
- esc2('chmod +x ' + dst, { stdio: 'ignore', timeout: 3000 });
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
- const status = execSync('opencode-feishu status 2>/dev/null || echo "stopped"', { encoding: 'utf-8', timeout: 5000 }).trim();
462
- if (status.includes('stopped') || status.includes('not running')) {
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(process.env.HOME || '~', '.config', 'opencode', 'qiwei.json');
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
- const qstatus = execSync('opencode-qiwei status 2>/dev/null || echo "stopped"', { encoding: 'utf-8', timeout: 5000 }).trim();
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
- const status = execSync('opencode-feishu status 2>/dev/null || echo "stopped"', { encoding: 'utf-8', timeout: 5000 }).trim();
547
- if (status.includes('stopped') || status.includes('not running')) {
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
- const qs = execSync('opencode-qiwei status 2>/dev/null || echo "stopped"', { encoding: 'utf-8', timeout: 5000 }).trim();
566
- if (qs.includes('stopped') || qs.includes('not running')) {
567
- execSync('opencode-qiwei start', { stdio: 'ignore', timeout: 10000 });
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(process.env.HOME || '~', '.config', 'opencode', 'feishu.json');
615
- const qiweiConfig = join(process.env.HOME || '~', '.config', 'opencode', 'qiwei.json');
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
- if (hasFeishu)
746
+ // Re-check after setup may have created config files
747
+ if (existsSync(feishuConfig))
664
748
  await startFeishu(feishuConfig);
665
- if (hasQiwei)
749
+ if (existsSync(qiweiConfig))
666
750
  await startQiwei(qiweiConfig);
667
751
  try {
668
752
  const { execSync } = await import('node:child_process');