@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
@@ -46,10 +46,10 @@ function formatDangerousFileResponse(filePath, ext) {
46
46
  summarize "${filePath}" --model google/gemini-3-flash-preview
47
47
 
48
48
  【方式2 - 问答/详细分析】
49
- python3 skills/agent-gemini/scripts/ask_gemini.py "${filePath}" "请总结这份文件的核心内容"
49
+ ${process.platform === 'win32' ? 'python' : 'python3'} skills/agent-gemini/scripts/ask_gemini.py "${filePath}" "请总结这份文件的核心内容"
50
50
 
51
51
  【方式3 - 提取纯文本】
52
- python3 -c "import pdfplumber; print(''.join(p.extract_text() for p in pdfplumber.open('${filePath}').pages))"
52
+ ${process.platform === 'win32' ? 'python' : 'python3'} -c "import pdfplumber; print(''.join(p.extract_text() for p in pdfplumber.open('${filePath}').pages))"
53
53
 
54
54
  💡 推荐:方式1 最快,方式2 最灵活`;
55
55
  }
@@ -160,8 +160,8 @@ export default function ReadPlugin(ctx) {
160
160
  return `⚠️ 这是二进制文件 (${ext}),read 工具会显示乱码。
161
161
 
162
162
  建议处理方式:
163
- - 图片分析: python3 skills/agent-vision/scripts/vision.py "${filePath}" "描述图片内容"
164
- - 音视频: python3 skills/agent-hearing/scripts/hear.py "${filePath}"
163
+ - 图片分析: ${process.platform === 'win32' ? 'python' : 'python3'} skills/agent-vision/scripts/vision.py "${filePath}" "描述图片内容"
164
+ - 音视频: ${process.platform === 'win32' ? 'python' : 'python3'} skills/agent-hearing/scripts/hear.py "${filePath}"
165
165
  - 压缩包: unzip -l "${filePath}" 或 tar -tf "${filePath}"
166
166
  - 字体/其他: 用 file 命令查看类型`;
167
167
  }
@@ -0,0 +1,315 @@
1
+ // search-memory.mjs — 魂器四层记忆统一搜索工具
2
+ // 1. 短期对话 (conversations.db)
3
+ // 2. 结构化记忆 (memories.db FTS5 + MEMORY.md)
4
+ // 3. 知识库 (knowledge/)
5
+ // 4. 长期记忆 (memory/long-term/)
6
+ import { tool } from "@opencode-ai/plugin";
7
+ import { DatabaseSync } from "node:sqlite";
8
+ import { join } from "node:path";
9
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
10
+
11
+ const PROJECT_DIR = process.cwd();
12
+ const DB_PATH = join(PROJECT_DIR, "memory", "short-term", "conversations.db");
13
+ const MEMORIES_DB = join(PROJECT_DIR, "memory", "short-term", "memories.db");
14
+ const MEMORY_MD = join(PROJECT_DIR, "memory", "MEMORY.md");
15
+ const LONG_TERM_DIR = join(PROJECT_DIR, "memory", "long-term");
16
+ const KNOWLEDGE_DIR = join(PROJECT_DIR, "knowledge");
17
+
18
+ // ─── Layer 5: ChromaDB 向量语义搜索 ─────────────────────
19
+ function searchVector(query, limit = 5) {
20
+ try {
21
+ // 查找 memory_manager.py 脚本路径
22
+ const { execSync } = require("node:child_process");
23
+ const scriptsDir = join(PROJECT_DIR, "..", "agent-soul-skills", "scripts");
24
+ const pyScript = join(scriptsDir, "memory_manager.py");
25
+ if (!existsSync(pyScript)) return [];
26
+
27
+ // 尝试找到 python3
28
+ let python = "python3";
29
+ try { execSync("python3 --version 2>&1", { timeout: 3000 }); } catch {
30
+ try { execSync("python --version 2>&1", { timeout: 3000 }); python = "python"; } catch {
31
+ return [];
32
+ }
33
+ }
34
+
35
+ const result = execSync(
36
+ `${python} "${pyScript}" vector-search --json "${query}"`,
37
+ { encoding: "utf-8", timeout: 30000, cwd: PROJECT_DIR }
38
+ ).trim();
39
+
40
+ if (!result || result.startsWith("[INFO]") || result.startsWith("用法")) return [];
41
+
42
+ const parsed = JSON.parse(result);
43
+ if (!Array.isArray(parsed)) return [];
44
+ return parsed.slice(0, limit);
45
+ } catch (err) {
46
+ // ChromaDB 不可用或未安装,静默跳过
47
+ return [];
48
+ }
49
+ }
50
+ // ─── Layer 1: 短期对话搜索 ─────────────────────────────
51
+ function searchConversations(query, limit = 15) {
52
+ if (!existsSync(DB_PATH)) return [];
53
+ const db = new DatabaseSync(DB_PATH, { readOnly: true });
54
+ try {
55
+ return db
56
+ .prepare(
57
+ "SELECT role, content, datetime(timestamp, 'unixepoch', 'localtime') as time, session_id FROM conversations WHERE content LIKE ? ORDER BY timestamp DESC LIMIT ?"
58
+ )
59
+ .all(`%${query}%`, limit);
60
+ } finally { db.close(); }
61
+ }
62
+
63
+ // ─── Layer 2: 结构化记忆 FTS5 搜索 ─────────────────────
64
+ function searchStructured(query, limit = 10) {
65
+ if (!existsSync(MEMORIES_DB)) return [];
66
+ const db = new DatabaseSync(MEMORIES_DB, { readOnly: true });
67
+ try {
68
+ // 先尝试 FTS5
69
+ const hasFts = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='sessions_fts'").get();
70
+ if (hasFts) {
71
+ try {
72
+ return db.prepare(
73
+ "SELECT id as session_id, date, summary, participant FROM sessions_fts WHERE sessions_fts MATCH ? ORDER BY rank LIMIT ?"
74
+ ).all(`"${query}"*`, limit);
75
+ } catch {
76
+ // FTS5 match failed, fall through to LIKE
77
+ }
78
+ }
79
+ return db.prepare(
80
+ "SELECT id as session_id, date, summary, participant FROM sessions WHERE summary LIKE ? OR content LIKE ? LIMIT ?"
81
+ ).all(`%${query}%`, `%${query}%`, limit);
82
+ } finally { db.close(); }
83
+ }
84
+
85
+ // ─── Layer 2b: MEMORY.md 搜索 ──────────────────────────
86
+ function searchMemoryMd(query) {
87
+ if (!existsSync(MEMORY_MD)) return [];
88
+ const content = readFileSync(MEMORY_MD, "utf-8");
89
+ const results = [];
90
+ const q = query.toLowerCase();
91
+ for (const line of content.split("\n")) {
92
+ if (line.toLowerCase().includes(q) && line.trim()) {
93
+ results.push(line.trim().slice(0, 200));
94
+ }
95
+ }
96
+ return results.slice(0, 5);
97
+ }
98
+
99
+ // ─── Layer 3: 知识库搜索 ───────────────────────────────
100
+ function searchKnowledge(query, limit = 10) {
101
+ if (!existsSync(KNOWLEDGE_DIR)) return [];
102
+ const results = [];
103
+ const q = query.toLowerCase();
104
+
105
+ function scanDir(dir, category = "") {
106
+ if (!existsSync(dir)) return;
107
+ for (const entry of readdirSync(dir)) {
108
+ const full = join(dir, entry);
109
+ try {
110
+ if (statSync(full).isDirectory()) {
111
+ scanDir(full, entry);
112
+ } else if (entry.endsWith(".md") && !entry.startsWith(".")) {
113
+ try {
114
+ const content = readFileSync(full, "utf-8");
115
+ const lines = content.split("\n");
116
+ for (let i = 0; i < lines.length; i++) {
117
+ if (lines[i].toLowerCase().includes(q)) {
118
+ results.push({
119
+ file: category ? `${category}/${entry}` : entry,
120
+ title: lines[0].replace(/^#+\s*/, "").slice(0, 80) || entry,
121
+ snippet: lines.slice(Math.max(0, i - 1), i + 2).join(" ").slice(0, 200),
122
+ });
123
+ if (results.length >= limit) return;
124
+ }
125
+ }
126
+ } catch {}
127
+ }
128
+ } catch {}
129
+ }
130
+ }
131
+
132
+ scanDir(KNOWLEDGE_DIR);
133
+ return results;
134
+ }
135
+
136
+ // ─── Layer 4: 长期记忆搜索 ─────────────────────────────
137
+ function searchLongTerm(query, limit = 10) {
138
+ if (!existsSync(LONG_TERM_DIR)) return [];
139
+ const results = [];
140
+ const q = query.toLowerCase();
141
+ const files = readdirSync(LONG_TERM_DIR)
142
+ .filter(f => f.endsWith(".md"))
143
+ .sort()
144
+ .reverse() // newest first
145
+ .slice(0, 30);
146
+
147
+ for (const file of files) {
148
+ try {
149
+ const content = readFileSync(join(LONG_TERM_DIR, file), "utf-8");
150
+ const lines = content.split("\n");
151
+ for (let i = 0; i < lines.length; i++) {
152
+ if (lines[i].toLowerCase().includes(q)) {
153
+ results.push({
154
+ date: file.replace(".md", ""),
155
+ snippet: lines.slice(Math.max(0, i - 1), i + 2).join(" ").slice(0, 200),
156
+ });
157
+ if (results.length >= limit) return results;
158
+ }
159
+ }
160
+ } catch {}
161
+ }
162
+ return results;
163
+ }
164
+
165
+ // ─── Plugin ────────────────────────────────────────────
166
+ export default function SearchMemoryPlugin(ctx) {
167
+ return {
168
+ tool: {
169
+ // 统一记忆搜索 — 跨四层
170
+ search_memory: tool({
171
+ description:
172
+ "【魂器四层记忆统一搜索】搜索所有记忆层:短期对话、结构化记忆、MEMORY.md、知识库、长期记忆。返回结果标注来源。用于查找历史讨论、已知事实、知识卡片等。",
173
+ args: {
174
+ query: tool.schema.string().describe("搜索关键词"),
175
+ limit: tool.schema.number().optional().describe("每层返回结果上限,默认10"),
176
+ },
177
+ async execute(args, context) {
178
+ const query = args.query;
179
+ const limit = args.limit || 10;
180
+ if (!query) return "请提供搜索关键词";
181
+
182
+ let output = `## 🔍 记忆搜索: "${query}"\n\n`;
183
+
184
+ // Layer 1
185
+ const conv = searchConversations(query, limit);
186
+ if (conv.length > 0) {
187
+ output += `### 💬 短期对话 (${conv.length} 条)\n\n`;
188
+ for (const r of conv) {
189
+ const emoji = r.role === "user" ? "👤" : "🤖";
190
+ output += `- ${emoji} [${r.time}] ${r.content.slice(0, 300)}\n`;
191
+ }
192
+ output += "\n";
193
+ }
194
+
195
+ // Layer 2
196
+ const struct = searchStructured(query, limit);
197
+ if (struct.length > 0) {
198
+ output += `### 📋 结构化记忆 (${struct.length} 条)\n\n`;
199
+ for (const r of struct) {
200
+ output += `- 📅 ${r.date || "?"} | ${(r.summary || "").slice(0, 200)}\n`;
201
+ }
202
+ output += "\n";
203
+ }
204
+
205
+ // Layer 2b
206
+ const memMd = searchMemoryMd(query);
207
+ if (memMd.length > 0) {
208
+ output += `### 📝 MEMORY.md (${memMd.length} 条)\n\n`;
209
+ for (const r of memMd) {
210
+ output += `- ${r}\n`;
211
+ }
212
+ output += "\n";
213
+ }
214
+
215
+ // Layer 5: ChromaDB 向量语义搜索
216
+ const vec = searchVector(query, limit);
217
+ if (vec.length > 0) {
218
+ output += `### 🧬 向量语义 (${vec.length} 条)\n\n`;
219
+ for (const r of vec) {
220
+ const dist = (r.distance * 100).toFixed(1);
221
+ output += `- [${r.collection}] ${(r.content || "").slice(0, 200)} (相关性 ${dist}%)\n`;
222
+ }
223
+ output += "\n";
224
+ }
225
+
226
+ // Layer 3
227
+ const know = searchKnowledge(query, limit);
228
+ if (know.length > 0) {
229
+ output += `### 📚 知识库 (${know.length} 条)\n\n`;
230
+ for (const r of know) {
231
+ output += `- **${r.title}** \`${r.file}\` — ${r.snippet}\n`;
232
+ }
233
+ output += "\n";
234
+ }
235
+
236
+ // Layer 4
237
+ const lt = searchLongTerm(query, limit);
238
+ if (lt.length > 0) {
239
+ output += `### 🗄️ 长期记忆 (${lt.length} 条)\n\n`;
240
+ for (const r of lt) {
241
+ output += `- 📅 ${r.date} — ${r.snippet}\n`;
242
+ }
243
+ output += "\n";
244
+ }
245
+
246
+ const total = conv.length + struct.length + memMd.length + know.length + lt.length + vec.length;
247
+ if (total === 0) {
248
+ return `未找到与 "${query}" 相关的记忆。`;
249
+ }
250
+ output += `---\n共 ${total} 条结果(短期 ${conv.length} + 结构 ${struct.length + memMd.length} + 知识 ${know.length} + 长期 ${lt.length} + 向量 ${vec.length})`;
251
+ return output;
252
+ },
253
+ }),
254
+
255
+ // 回顾最近对话
256
+ recall_memory: tool({
257
+ description:
258
+ "回顾最近的对话记忆。返回最新对话记录,帮助回忆近期讨论内容。",
259
+ args: {
260
+ limit: tool.schema.number().optional().describe("返回条数,默认15"),
261
+ },
262
+ async execute(args, context) {
263
+ const limit = args.limit || 15;
264
+ if (!existsSync(DB_PATH)) return "记忆数据库暂无记录。";
265
+
266
+ const db = new DatabaseSync(DB_PATH, { readOnly: true });
267
+ let results = [];
268
+ try {
269
+ results = db
270
+ .prepare(
271
+ "SELECT role, content, datetime(timestamp, 'unixepoch', 'localtime') as time, session_id FROM conversations ORDER BY timestamp DESC LIMIT ?"
272
+ )
273
+ .all(limit);
274
+ } finally { db.close(); }
275
+
276
+ if (results.length === 0) return "记忆数据库中暂无对话记录。";
277
+
278
+ let output = `## 🕐 最近 ${results.length} 条对话\n\n`;
279
+ for (const r of results) {
280
+ const emoji = r.role === "user" ? "👤" : "🤖";
281
+ output += `${emoji} [${r.time}] ${r.content.slice(0, 400)}\n\n---\n\n`;
282
+ }
283
+ return output;
284
+ },
285
+ }),
286
+
287
+ // 知识库专项搜索
288
+ search_knowledge: tool({
289
+ description:
290
+ "搜索魂器知识库(knowledge/ 目录)。查找 Agent 已学习的知识卡片,包括审计方法论、内控知识、会计标准等。",
291
+ args: {
292
+ query: tool.schema.string().describe("搜索关键词"),
293
+ category: tool.schema.string().optional().describe("限定知识分类,如 methodology, system, philosophy 等"),
294
+ limit: tool.schema.number().optional().describe("返回条数,默认10"),
295
+ },
296
+ async execute(args, context) {
297
+ const query = args.query;
298
+ const limit = args.limit || 10;
299
+ if (!query) return "请提供搜索关键词";
300
+
301
+ const results = searchKnowledge(query, limit);
302
+ if (results.length === 0) return `知识库中未找到与 "${query}" 相关的卡片。`;
303
+
304
+ let output = `## 📚 知识库搜索: "${query}"\n\n`;
305
+ for (const r of results) {
306
+ output += `- **${r.title}** \`${r.file}\`\n ${r.snippet}\n\n`;
307
+ }
308
+ output += `---\n共 ${results.length} 条`;
309
+ return output;
310
+ },
311
+ }),
312
+ },
313
+ };
314
+ }
315
+
package/AGENTS.md.example CHANGED
@@ -17,13 +17,15 @@
17
17
 
18
18
  ## 记忆搜索
19
19
 
20
- ```bash
21
- # 首选: 统一记忆搜索
22
- python3 scripts/memory_search.py "关键词"
20
+ **始终优先使用以下 OpenCode 工具**(零进程开销,引擎内执行):
23
21
 
24
- # FTS5 会话搜索
25
- python3 scripts/memory_structured.py search "关键词"
26
- ```
22
+ | 工具 | 用途 |
23
+ |------|------|
24
+ | `search_memory "关键词"` | 五层统一搜索:短期对话 + 结构化 + MEMORY.md + 知识库 + ChromaDB 语义 |
25
+ | `recall_memory` | 回顾最近对话,了解当前上下文 |
26
+ | `search_knowledge "关键词"` | 专项搜索知识库卡片 |
27
+
28
+ 在回答用户问题之前,先 `search_memory` 检查是否有相关历史信息。
27
29
 
28
30
  ---
29
31
 
package/bin/hunqi CHANGED
@@ -1,2 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import "../dist/cli/hunqi.js";
2
+ import("../dist/cli/hunqi.js").catch(err => {
3
+ if (err.code === "ERR_MODULE_NOT_FOUND") {
4
+ console.error("❌ 构建产物缺失。请运行 npm run build 或 npm install -g @neomei/agent-soul-framework");
5
+ } else {
6
+ console.error("启动失败:", err.message);
7
+ }
8
+ process.exit(1);
9
+ });
@@ -1,2 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import "../dist/bin/hunqi-knowledge.js";
2
+ import("../dist/bin/hunqi-knowledge.js").catch(err => {
3
+ if (err.code === "ERR_MODULE_NOT_FOUND") {
4
+ console.error("❌ 构建产物缺失。请运行 npm run build 或 npm install -g @neomei/agent-soul-framework");
5
+ } else {
6
+ console.error("启动失败:", err.message);
7
+ }
8
+ process.exit(1);
9
+ });
@@ -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 统一处理 ✅`);
@@ -1,97 +1,6 @@
1
1
  #!/usr/bin/env bash
2
- # on-session-created.sh — 新建 session 时注入灵魂文件(缓存优化版)
2
+ # on-session-created.sh — session 创建事件 hook
3
+ # 灵魂注入已统一由 hunqi-plugin(OpenCode 插件)处理,此脚本保留用于未来扩展
3
4
  # 环境变量: HOOK_SESSION_ID, HOOK_OPENCODE_URL
4
5
 
5
- set -e
6
-
7
- SESSION_ID="${HOOK_SESSION_ID}"
8
- OPENCODE_URL="${HOOK_OPENCODE_URL:-http://localhost:19876}"
9
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
10
- PROJECT_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
11
-
12
- # 读取 opencode serve 密码
13
- OPENCODE_PASSWORD="${OPENCODE_SERVER_PASSWORD:-}"
14
- if [ -z "$OPENCODE_PASSWORD" ] && [ -f "$PROJECT_DIR/.env" ]; then
15
- OPENCODE_PASSWORD=$(grep "^OPENCODE_SERVER_PASSWORD=" "$PROJECT_DIR/.env" | cut -d= -f2- | tr -d '"\'\'')
16
- fi
17
-
18
- CURL_AUTH=()
19
- if [ -n "$OPENCODE_PASSWORD" ]; then
20
- AUTH_TOKEN=$(printf '%s:%s' 'opencode' "$OPENCODE_PASSWORD" | base64 -w 0)
21
- CURL_AUTH=(-H "Authorization: Basic ${AUTH_TOKEN}")
22
- fi
23
-
24
- # 缓存文件路径
25
- SOUL_CACHE="/tmp/hunqi-soul-cache.txt"
26
- SOUL_FILES=(
27
- "$PROJECT_DIR/soul/IDENTITY.md"
28
- "$PROJECT_DIR/soul/SOUL.md"
29
- "$PROJECT_DIR/soul/USER.md"
30
- "$PROJECT_DIR/soul/AGENTS.md"
31
- )
32
-
33
- # 检查是否需要更新缓存(任一灵魂文件比缓存新)
34
- needs_update=false
35
- if [ ! -f "$SOUL_CACHE" ]; then
36
- needs_update=true
37
- else
38
- cache_mtime=$(stat -c %Y "$SOUL_CACHE" 2>/dev/null || stat -f %m "$SOUL_CACHE")
39
- for f in "${SOUL_FILES[@]}"; do
40
- if [ -f "$f" ]; then
41
- file_mtime=$(stat -c %Y "$f" 2>/dev/null || stat -f %m "$f")
42
- if [ "$file_mtime" -gt "$cache_mtime" ]; then
43
- needs_update=true
44
- break
45
- fi
46
- fi
47
- done
48
- fi
49
-
50
- # 更新缓存
51
- if [ "$needs_update" = true ]; then
52
- SEPARATOR=$'\n\n---\n\n'
53
- SYSTEM_TEXT=""
54
- for f in "${SOUL_FILES[@]}"; do
55
- if [ -f "$f" ]; then
56
- BASENAME=$(basename "$f")
57
- CONTENT=$(cat "$f")
58
- SYSTEM_TEXT="${SYSTEM_TEXT}${SEPARATOR}=== ${BASENAME} ===${SEPARATOR}${CONTENT}"
59
- fi
60
- done
61
- # 去掉开头的分隔符
62
- SYSTEM_TEXT="${SYSTEM_TEXT#${SEPARATOR}}"
63
- echo "$SYSTEM_TEXT" > "$SOUL_CACHE"
64
- echo "[hook] soul cache updated (${#SYSTEM_TEXT} chars)"
65
- fi
66
-
67
- # 从缓存读取
68
- SYSTEM_TEXT=$(cat "$SOUL_CACHE")
69
-
70
- if [ -z "$SYSTEM_TEXT" ]; then
71
- echo "[hook:onSessionCreated] warning: soul cache is empty"
72
- exit 0
73
- fi
74
-
75
- echo "[hook:onSessionCreated] injecting soul for session ${SESSION_ID}..."
76
-
77
- # 使用 jq 或纯 bash 构造 JSON,避免 python3 进程开销
78
- if command -v jq &>/dev/null; then
79
- JSON_PAYLOAD=$(jq -n --arg system "$SYSTEM_TEXT" '{system: $system, noReply: true, parts: []}')
80
- else
81
- # 纯 bash fallback(简单转义)
82
- JSON_PAYLOAD="{\"system\":$(printf '%s' "$SYSTEM_TEXT" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))'),\"noReply\":true,\"parts\":[]}"
83
- fi
84
-
85
- RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "${OPENCODE_URL}/session/${SESSION_ID}/message" \
86
- -H "Content-Type: application/json" \
87
- "${CURL_AUTH[@]}" \
88
- -d "$JSON_PAYLOAD" 2>&1)
89
-
90
- HTTP_CODE=$(echo "$RESPONSE" | tail -1)
91
-
92
- if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ]; then
93
- echo "[hook:onSessionCreated] soul injected (${#SYSTEM_TEXT} chars) ✅"
94
- else
95
- echo "[hook:onSessionCreated] soul injection failed: HTTP $HTTP_CODE" >&2
96
- exit 1
97
- fi
6
+ echo "[hook:onSessionCreated] session=${HOOK_SESSION_ID} — 灵魂注入由 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 统一处理 ✅`);
@@ -1,59 +1,6 @@
1
1
  #!/usr/bin/env bash
2
- # on-session-idle.sh — session idle 时(上下文压缩后)重新注入灵魂(缓存优化版)
2
+ # on-session-idle.sh — session idle 事件 hook
3
+ # 灵魂注入已统一由 hunqi-plugin(OpenCode 插件)处理,此脚本保留用于未来扩展
3
4
  # 环境变量: HOOK_SESSION_ID, HOOK_OPENCODE_URL
4
5
 
5
- set -e
6
-
7
- SESSION_ID="${HOOK_SESSION_ID}"
8
- OPENCODE_URL="${HOOK_OPENCODE_URL:-http://localhost:19876}"
9
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
10
- PROJECT_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
11
-
12
- # 读取 opencode serve 密码
13
- OPENCODE_PASSWORD="${OPENCODE_SERVER_PASSWORD:-}"
14
- if [ -z "$OPENCODE_PASSWORD" ] && [ -f "$PROJECT_DIR/.env" ]; then
15
- OPENCODE_PASSWORD=$(grep "^OPENCODE_SERVER_PASSWORD=" "$PROJECT_DIR/.env" | cut -d= -f2- | tr -d '"\'\'')
16
- fi
17
-
18
- CURL_AUTH=()
19
- if [ -n "$OPENCODE_PASSWORD" ]; then
20
- AUTH_TOKEN=$(printf '%s:%s' 'opencode' "$OPENCODE_PASSWORD" | base64 -w 0)
21
- CURL_AUTH=(-H "Authorization: Basic ${AUTH_TOKEN}")
22
- fi
23
-
24
- # 直接使用 session-created 生成的缓存
25
- SOUL_CACHE="/tmp/hunqi-soul-cache.txt"
26
-
27
- if [ ! -f "$SOUL_CACHE" ]; then
28
- echo "[hook:onSessionIdle] warning: soul cache not found, skipping"
29
- exit 0
30
- fi
31
-
32
- SYSTEM_TEXT=$(cat "$SOUL_CACHE")
33
-
34
- if [ -z "$SYSTEM_TEXT" ]; then
35
- echo "[hook:onSessionIdle] warning: soul cache is empty, skipping"
36
- exit 0
37
- fi
38
-
39
- echo "[hook:onSessionIdle] re-injecting soul for session ${SESSION_ID}..."
40
-
41
- # 使用 jq 或纯 bash 构造 JSON
42
- if command -v jq &>/dev/null; then
43
- JSON_PAYLOAD=$(jq -n --arg system "$SYSTEM_TEXT" '{system: $system, noReply: true, parts: []}')
44
- else
45
- JSON_PAYLOAD="{\"system\":$(printf '%s' "$SYSTEM_TEXT" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))'),\"noReply\":true,\"parts\":[]}"
46
- fi
47
-
48
- RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "${OPENCODE_URL}/session/${SESSION_ID}/message" \
49
- -H "Content-Type: application/json" \
50
- "${CURL_AUTH[@]}" \
51
- -d "$JSON_PAYLOAD" 2>&1)
52
-
53
- HTTP_CODE=$(echo "$RESPONSE" | tail -1)
54
-
55
- if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ]; then
56
- echo "[hook:onSessionIdle] soul re-injected (${#SYSTEM_TEXT} chars) ✅"
57
- else
58
- echo "[hook:onSessionIdle] soul re-injection failed: HTTP $HTTP_CODE" >&2
59
- fi
6
+ echo "[hook:onSessionIdle] session=${HOOK_SESSION_ID} — 灵魂注入由 hunqi-plugin 统一处理 ✅"
@@ -6,7 +6,14 @@
6
6
  set -e
7
7
 
8
8
  SESSIONS_FILE="${HOME}/.config/opencode/feishu-sessions.json"
9
- DB_FILE="${HOME}/.local/share/opencode/opencode.db"
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
10
17
  MAX_MESSAGES="${MAX_MESSAGES_PER_SESSION:-50}"
11
18
 
12
19
  if [ ! -f "$SESSIONS_FILE" ]; then
@@ -23,7 +30,7 @@ fi
23
30
  SESSIONS=$(cat "$SESSIONS_FILE")
24
31
 
25
32
  # 使用单一 Python 脚本安全处理所有操作,避免注入
26
- python3 - "$SESSIONS_FILE" "$DB_FILE" "$MAX_MESSAGES" <<'PYEOF'
33
+ (python3 2>/dev/null || python 2>/dev/null) - "$SESSIONS_FILE" "$DB_FILE" "$MAX_MESSAGES" <<'PYEOF'
27
34
  import json, sqlite3, sys, os
28
35
 
29
36
  sessions_file = sys.argv[1]
@@ -64,9 +71,4 @@ try:
64
71
  finally:
65
72
  conn.close()
66
73
  PYEOF
67
-
68
- if [ "$CLEANED" -gt 0 ]; then
69
- echo "[session-cleanup] Cleaned $CLEANED session(s), next message will create new session"
70
- else
71
- echo "[session-cleanup] All sessions are within limit ($MAX_MESSAGES messages)"
72
- fi
74
+ # Note: Python script above handles all output and exit codes directly
@@ -9,6 +9,18 @@ if [ -f "$PROJECT_DIR/.env" ]; then
9
9
  set -a && source "$PROJECT_DIR/.env" && set +a
10
10
  fi
11
11
 
12
+ # 跨平台端口PID查询 (lsof > ss > netstat)
13
+ get_pid_on_port() {
14
+ local port="$1"
15
+ if command -v lsof &>/dev/null; then
16
+ lsof -iTCP:"$port" -sTCP:LISTEN -t 2>/dev/null | head -1
17
+ elif command -v ss &>/dev/null; then
18
+ ss -tlnp 2>/dev/null | grep ":$port " | sed -n "s/.*pid=\([0-9]*\).*/\1/p" | head -1
19
+ elif command -v netstat &>/dev/null; then
20
+ netstat -tlnp 2>/dev/null | grep ":$port " | awk "{print \$NF}" | cut -d/ -f1 | head -1
21
+ fi
22
+ }
23
+
12
24
  # 自动检测 opencode-feishu 启动方式
13
25
  resolve_opencode_feishu() {
14
26
  if command -v opencode-feishu &>/dev/null; then
@@ -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=$(%h/.hunqi/bin/opencode-feishu --resolve-opencode 2>/dev/null || command -v opencode); exec "$OPENCODE" serve --port 19876'
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
+ #!/usr/bin/env bash
2
+ # on-session-created.sh — session 创建事件 hook
3
+ # 灵魂注入已统一由 hunqi-plugin(OpenCode 插件)处理,此脚本保留用于未来扩展
4
+ # 环境变量: HOOK_SESSION_ID, HOOK_OPENCODE_URL
5
+
6
+ echo "[hook:onSessionCreated] session=${HOOK_SESSION_ID} — 灵魂注入由 hunqi-plugin 统一处理 ✅"