@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
|
@@ -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
|
-
|
|
21
|
-
# 首选: 统一记忆搜索
|
|
22
|
-
python3 scripts/memory_search.py "关键词"
|
|
20
|
+
**始终优先使用以下 OpenCode 工具**(零进程开销,引擎内执行):
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
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
|
+
});
|
package/bin/hunqi-knowledge
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
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 —
|
|
2
|
+
# on-session-created.sh — session 创建事件 hook
|
|
3
|
+
# 灵魂注入已统一由 hunqi-plugin(OpenCode 插件)处理,此脚本保留用于未来扩展
|
|
3
4
|
# 环境变量: HOOK_SESSION_ID, HOOK_OPENCODE_URL
|
|
4
5
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=$(
|
|
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 统一处理 ✅`);
|