@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.
Files changed (43) hide show
  1. package/.opencode/tools/read-plugin.js +4 -4
  2. package/.opencode/tools/search-memory.mjs +258 -39
  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 +107 -37
  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 +2 -3
  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
  }
@@ -1,96 +1,315 @@
1
- // search-memory.mjs — 魂器记忆搜索工具
2
- // AI 在会话中主动查询 conversation 数据库
1
+ // search-memory.mjs — 魂器四层记忆统一搜索工具
2
+ // 1. 短期对话 (conversations.db)
3
+ // 2. 结构化记忆 (memories.db FTS5 + MEMORY.md)
4
+ // 3. 知识库 (knowledge/)
5
+ // 4. 长期记忆 (memory/long-term/)
3
6
  import { tool } from "@opencode-ai/plugin";
4
7
  import { DatabaseSync } from "node:sqlite";
5
8
  import { join } from "node:path";
6
- import { existsSync } from "node:fs";
9
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
7
10
 
8
11
  const PROJECT_DIR = process.cwd();
9
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");
10
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: 短期对话搜索 ─────────────────────────────
11
51
  function searchConversations(query, limit = 15) {
12
52
  if (!existsSync(DB_PATH)) return [];
13
-
14
53
  const db = new DatabaseSync(DB_PATH, { readOnly: true });
15
54
  try {
16
55
  return db
17
56
  .prepare(
18
- "SELECT role, content, datetime(timestamp, 'unixepoch', 'localtime') as time FROM conversations WHERE content LIKE ? ORDER BY timestamp DESC LIMIT ?"
57
+ "SELECT role, content, datetime(timestamp, 'unixepoch', 'localtime') as time, session_id FROM conversations WHERE content LIKE ? ORDER BY timestamp DESC LIMIT ?"
19
58
  )
20
- .all(%%, limit);
21
- } finally {
22
- db.close();
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
+ }
23
95
  }
96
+ return results.slice(0, 5);
24
97
  }
25
98
 
26
- function getRecent(limit = 10) {
27
- if (!existsSync(DB_PATH)) return [];
99
+ // ─── Layer 3: 知识库搜索 ───────────────────────────────
100
+ function searchKnowledge(query, limit = 10) {
101
+ if (!existsSync(KNOWLEDGE_DIR)) return [];
102
+ const results = [];
103
+ const q = query.toLowerCase();
28
104
 
29
- const db = new DatabaseSync(DB_PATH, { readOnly: true });
30
- try {
31
- return db
32
- .prepare(
33
- "SELECT role, content, datetime(timestamp, 'unixepoch', 'localtime') as time FROM conversations ORDER BY timestamp DESC LIMIT ?"
34
- )
35
- .all(limit);
36
- } finally {
37
- db.close();
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 {}
38
161
  }
162
+ return results;
39
163
  }
40
164
 
165
+ // ─── Plugin ────────────────────────────────────────────
41
166
  export default function SearchMemoryPlugin(ctx) {
42
167
  return {
43
168
  tool: {
169
+ // 统一记忆搜索 — 跨四层
44
170
  search_memory: tool({
45
171
  description:
46
- "搜索历史对话记忆数据库。用于查找之前讨论过的话题、问题、决策等。调用此工具可以找到与特定关键词相关的过往对话。",
172
+ "【魂器四层记忆统一搜索】搜索所有记忆层:短期对话、结构化记忆、MEMORY.md、知识库、长期记忆。返回结果标注来源。用于查找历史讨论、已知事实、知识卡片等。",
47
173
  args: {
48
- query: tool.schema.string().describe("搜索关键词,用于在历史对话中查找相关内容"),
49
- limit: tool.schema.number().optional().describe("返回结果数量上限,默认15"),
174
+ query: tool.schema.string().describe("搜索关键词"),
175
+ limit: tool.schema.number().optional().describe("每层返回结果上限,默认10"),
50
176
  },
51
177
  async execute(args, context) {
52
178
  const query = args.query;
53
- const limit = args.limit || 15;
54
-
179
+ const limit = args.limit || 10;
55
180
  if (!query) return "请提供搜索关键词";
56
181
 
57
- const results = searchConversations(query, limit);
182
+ let output = `## 🔍 记忆搜索: "${query}"\n\n`;
58
183
 
59
- if (results.length === 0) {
60
- return 没有找到与 "" 相关的历史对话记录。;
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";
61
193
  }
62
194
 
63
- let output = ## 记忆搜索结果: ""\n\n找到 条相关对话:\n\n;
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`;
64
279
  for (const r of results) {
65
280
  const emoji = r.role === "user" ? "👤" : "🤖";
66
- output += ${emoji} [] \n\n---\n\n;
281
+ output += `${emoji} [${r.time}] ${r.content.slice(0, 400)}\n\n---\n\n`;
67
282
  }
68
283
  return output;
69
284
  },
70
285
  }),
71
286
 
72
- recall_memory: tool({
287
+ // 知识库专项搜索
288
+ search_knowledge: tool({
73
289
  description:
74
- "回顾最近的对话记忆。不需要关键词,返回最新的对话记录摘要,帮助回忆最近的讨论内容。",
290
+ "搜索魂器知识库(knowledge/ 目录)。查找 Agent 已学习的知识卡片,包括审计方法论、内控知识、会计标准等。",
75
291
  args: {
76
- limit: tool.schema.number().optional().describe("返回的最近对话条数,默认10"),
292
+ query: tool.schema.string().describe("搜索关键词"),
293
+ category: tool.schema.string().optional().describe("限定知识分类,如 methodology, system, philosophy 等"),
294
+ limit: tool.schema.number().optional().describe("返回条数,默认10"),
77
295
  },
78
296
  async execute(args, context) {
297
+ const query = args.query;
79
298
  const limit = args.limit || 10;
80
- const results = getRecent(limit);
299
+ if (!query) return "请提供搜索关键词";
81
300
 
82
- if (results.length === 0) {
83
- return "记忆数据库中暂无对话记录。";
84
- }
301
+ const results = searchKnowledge(query, limit);
302
+ if (results.length === 0) return `知识库中未找到与 "${query}" 相关的卡片。`;
85
303
 
86
- let output = ## 最近 条对话\n\n;
304
+ let output = `## 📚 知识库搜索: "${query}"\n\n`;
87
305
  for (const r of results) {
88
- const emoji = r.role === "user" ? "👤" : "🤖";
89
- output += ${emoji} [] \n\n---\n\n;
306
+ output += `- **${r.title}** \`${r.file}\`\n ${r.snippet}\n\n`;
90
307
  }
308
+ output += `---\n共 ${results.length} 条`;
91
309
  return output;
92
310
  },
93
311
  }),
94
312
  },
95
313
  };
96
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