metame-cli 1.6.2 → 1.6.4
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/README.md +12 -2
- package/index.js +86 -6
- package/package.json +1 -1
- package/scripts/agent-intent-shared.js +11 -2
- package/scripts/core/session-source-db.js +125 -0
- package/scripts/daemon-agent-intent.js +51 -15
- package/scripts/daemon-agent-tools.js +52 -3
- package/scripts/daemon-agent-workflow.js +98 -0
- package/scripts/daemon-bridges.js +9 -2
- package/scripts/daemon-claude-engine.js +12 -3
- package/scripts/daemon-command-router.js +9 -20
- package/scripts/daemon-engine-runtime.js +16 -6
- package/scripts/daemon-runtime-lifecycle.js +23 -0
- package/scripts/daemon-user-acl.js +19 -1
- package/scripts/daemon-weixin-bridge.js +6 -2
- package/scripts/daemon.js +50 -4
- package/scripts/docs/hermes-memory-upgrade-converged.md +461 -0
- package/scripts/docs/hermes-memory-upgrade-plan.md +506 -0
- package/scripts/feishu-adapter.js +78 -2
- package/scripts/memory-extract.js +72 -4
- package/scripts/memory-wiki-schema.js +31 -0
- package/scripts/memory.js +8 -2
- package/skills/agent-management/SKILL.md +101 -0
- package/skills/send-to-user/SKILL.md +76 -0
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
const fs = require('fs');
|
|
16
16
|
const path = require('path');
|
|
17
17
|
const os = require('os');
|
|
18
|
+
const crypto = require('crypto');
|
|
18
19
|
const { callHaiku, buildDistillEnv } = require('./providers');
|
|
19
20
|
|
|
20
21
|
const HOME = os.homedir();
|
|
@@ -115,6 +116,63 @@ const VAGUE_PATTERNS = [
|
|
|
115
116
|
];
|
|
116
117
|
const ALLOWED_FLAT = new Set(['王总', 'system', 'user']);
|
|
117
118
|
|
|
119
|
+
function hashFile(filePath) {
|
|
120
|
+
if (!filePath) return null;
|
|
121
|
+
try {
|
|
122
|
+
const hash = crypto.createHash('sha256');
|
|
123
|
+
const fd = fs.openSync(filePath, 'r');
|
|
124
|
+
try {
|
|
125
|
+
const buf = Buffer.alloc(64 * 1024);
|
|
126
|
+
let bytesRead = 0;
|
|
127
|
+
do {
|
|
128
|
+
bytesRead = fs.readSync(fd, buf, 0, buf.length, null);
|
|
129
|
+
if (bytesRead > 0) hash.update(buf.subarray(0, bytesRead));
|
|
130
|
+
} while (bytesRead > 0);
|
|
131
|
+
} finally {
|
|
132
|
+
fs.closeSync(fd);
|
|
133
|
+
}
|
|
134
|
+
return hash.digest('hex');
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function statSize(filePath) {
|
|
141
|
+
try {
|
|
142
|
+
return filePath ? fs.statSync(filePath).size : 0;
|
|
143
|
+
} catch {
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function saveSessionSource(memory, engine, sourcePath, skeleton, status = 'indexed', errorMessage = null) {
|
|
149
|
+
if (!memory || typeof memory.saveSessionSource !== 'function' || !skeleton) return null;
|
|
150
|
+
const sourceHash = hashFile(sourcePath);
|
|
151
|
+
if (!sourceHash) return null;
|
|
152
|
+
try {
|
|
153
|
+
return memory.saveSessionSource({
|
|
154
|
+
engine,
|
|
155
|
+
sessionId: skeleton.session_id,
|
|
156
|
+
project: skeleton.project || 'unknown',
|
|
157
|
+
scope: skeleton.project_id || null,
|
|
158
|
+
cwd: skeleton.project_path || null,
|
|
159
|
+
sourcePath,
|
|
160
|
+
sourceHash,
|
|
161
|
+
sourceSize: statSize(sourcePath),
|
|
162
|
+
firstTs: skeleton.first_ts || null,
|
|
163
|
+
lastTs: skeleton.last_ts || null,
|
|
164
|
+
messageCount: skeleton.message_count || 0,
|
|
165
|
+
toolCallCount: skeleton.total_tool_calls || 0,
|
|
166
|
+
toolErrorCount: skeleton.tool_error_count || 0,
|
|
167
|
+
status,
|
|
168
|
+
errorMessage,
|
|
169
|
+
});
|
|
170
|
+
} catch (e) {
|
|
171
|
+
console.log(`[memory-extract] session source save failed: ${e.message}`);
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
118
176
|
/**
|
|
119
177
|
* Extract atomic facts from session skeleton + evidence via Haiku.
|
|
120
178
|
* Returns filtered fact array (may be empty).
|
|
@@ -212,8 +270,6 @@ async function run() {
|
|
|
212
270
|
const sessions = sessionAnalytics.findAllUnextractedSessions(3);
|
|
213
271
|
if (sessions.length === 0) {
|
|
214
272
|
console.log('[memory-extract] No unanalyzed sessions found.');
|
|
215
|
-
memory.close();
|
|
216
|
-
return { sessionsProcessed: 0, factsSaved: 0, factsSkipped: 0 };
|
|
217
273
|
}
|
|
218
274
|
|
|
219
275
|
let totalSaved = 0;
|
|
@@ -223,9 +279,11 @@ async function run() {
|
|
|
223
279
|
for (const session of sessions) {
|
|
224
280
|
try {
|
|
225
281
|
const skeleton = sessionAnalytics.extractSkeleton(session.path);
|
|
282
|
+
const sourceRow = saveSessionSource(memory, 'claude', session.path, skeleton);
|
|
226
283
|
|
|
227
284
|
// Skip trivial sessions
|
|
228
285
|
if (skeleton.message_count < 2 && skeleton.duration_min < 1) {
|
|
286
|
+
if (sourceRow) saveSessionSource(memory, 'claude', session.path, skeleton, 'archived');
|
|
229
287
|
sessionAnalytics.markFactsExtracted(skeleton.session_id);
|
|
230
288
|
continue;
|
|
231
289
|
}
|
|
@@ -237,6 +295,7 @@ async function run() {
|
|
|
237
295
|
|
|
238
296
|
const { ok, facts, session_name } = await extractFacts(skeleton, evidence, distillEnv);
|
|
239
297
|
if (!ok) {
|
|
298
|
+
if (sourceRow) saveSessionSource(memory, 'claude', session.path, skeleton, 'error', 'fact extraction failed');
|
|
240
299
|
console.log(`[memory-extract] Session ${skeleton.session_id.slice(0, 8)}: extraction failed, will retry later`);
|
|
241
300
|
continue;
|
|
242
301
|
}
|
|
@@ -249,7 +308,7 @@ async function run() {
|
|
|
249
308
|
skeleton.session_id,
|
|
250
309
|
skeleton.project || 'unknown',
|
|
251
310
|
facts,
|
|
252
|
-
{ scope: skeleton.project_id || fallbackScope }
|
|
311
|
+
{ scope: skeleton.project_id || fallbackScope, source_id: sourceRow ? sourceRow.id : skeleton.session_id }
|
|
253
312
|
);
|
|
254
313
|
totalSaved += saved;
|
|
255
314
|
totalSkipped += skipped;
|
|
@@ -276,6 +335,7 @@ async function run() {
|
|
|
276
335
|
|
|
277
336
|
// P2-A: persist session name + tags to session_tags.json
|
|
278
337
|
saveSessionTag(skeleton.session_id, session_name, facts);
|
|
338
|
+
if (sourceRow) saveSessionSource(memory, 'claude', session.path, skeleton, 'extracted');
|
|
279
339
|
|
|
280
340
|
processed++;
|
|
281
341
|
} catch (e) {
|
|
@@ -294,15 +354,18 @@ async function run() {
|
|
|
294
354
|
for (const cs of codexSessions) {
|
|
295
355
|
try {
|
|
296
356
|
const { skeleton, evidence } = sessionAnalytics.buildCodexInput(cs.path, historyMap);
|
|
357
|
+
const sourceRow = saveSessionSource(memory, 'codex', cs.path, skeleton);
|
|
297
358
|
|
|
298
359
|
// Skip trivial sessions with no user messages
|
|
299
360
|
if (skeleton.message_count < 1) {
|
|
361
|
+
if (sourceRow) saveSessionSource(memory, 'codex', cs.path, skeleton, 'archived');
|
|
300
362
|
sessionAnalytics.markCodexFactsExtracted(cs.session_id);
|
|
301
363
|
continue;
|
|
302
364
|
}
|
|
303
365
|
|
|
304
366
|
const { ok, facts, session_name } = await extractFacts(skeleton, evidence, distillEnv);
|
|
305
367
|
if (!ok) {
|
|
368
|
+
if (sourceRow) saveSessionSource(memory, 'codex', cs.path, skeleton, 'error', 'fact extraction failed');
|
|
306
369
|
console.log(`[memory-extract] Codex ${cs.session_id.slice(0, 8)}: extraction failed, will retry later`);
|
|
307
370
|
continue;
|
|
308
371
|
}
|
|
@@ -313,7 +376,11 @@ async function run() {
|
|
|
313
376
|
cs.session_id,
|
|
314
377
|
skeleton.project || 'unknown',
|
|
315
378
|
facts,
|
|
316
|
-
{
|
|
379
|
+
{
|
|
380
|
+
scope: skeleton.project_id || fallbackScope,
|
|
381
|
+
source_type: 'codex',
|
|
382
|
+
source_id: sourceRow ? sourceRow.id : cs.session_id,
|
|
383
|
+
}
|
|
317
384
|
);
|
|
318
385
|
totalSaved += saved;
|
|
319
386
|
totalSkipped += skipped;
|
|
@@ -339,6 +406,7 @@ async function run() {
|
|
|
339
406
|
}
|
|
340
407
|
|
|
341
408
|
sessionAnalytics.markCodexFactsExtracted(cs.session_id);
|
|
409
|
+
if (sourceRow) saveSessionSource(memory, 'codex', cs.path, skeleton, 'extracted');
|
|
342
410
|
processed++;
|
|
343
411
|
} catch (e) {
|
|
344
412
|
console.log(`[memory-extract] Codex session error: ${e.message}`);
|
|
@@ -130,6 +130,37 @@ function applyWikiSchema(db) {
|
|
|
130
130
|
)
|
|
131
131
|
`);
|
|
132
132
|
|
|
133
|
+
// ── session_sources (raw transcript provenance, L0) ───────────────────────
|
|
134
|
+
db.exec(`
|
|
135
|
+
CREATE TABLE IF NOT EXISTS session_sources (
|
|
136
|
+
id TEXT PRIMARY KEY,
|
|
137
|
+
engine TEXT NOT NULL DEFAULT 'unknown'
|
|
138
|
+
CHECK (engine IN ('claude','codex','unknown')),
|
|
139
|
+
session_id TEXT NOT NULL,
|
|
140
|
+
project TEXT DEFAULT '*',
|
|
141
|
+
scope TEXT,
|
|
142
|
+
agent_key TEXT,
|
|
143
|
+
cwd TEXT,
|
|
144
|
+
source_path TEXT,
|
|
145
|
+
source_hash TEXT NOT NULL,
|
|
146
|
+
source_size INTEGER DEFAULT 0,
|
|
147
|
+
first_ts TEXT,
|
|
148
|
+
last_ts TEXT,
|
|
149
|
+
message_count INTEGER DEFAULT 0,
|
|
150
|
+
tool_call_count INTEGER DEFAULT 0,
|
|
151
|
+
tool_error_count INTEGER DEFAULT 0,
|
|
152
|
+
status TEXT DEFAULT 'indexed'
|
|
153
|
+
CHECK (status IN ('indexed','summarized','extracted','error','archived')),
|
|
154
|
+
error_message TEXT,
|
|
155
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
156
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
157
|
+
UNIQUE(engine, session_id, source_hash)
|
|
158
|
+
)
|
|
159
|
+
`);
|
|
160
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_session_sources_session ON session_sources(session_id)');
|
|
161
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_session_sources_project ON session_sources(project, scope, last_ts)');
|
|
162
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_session_sources_agent ON session_sources(agent_key, last_ts)');
|
|
163
|
+
|
|
133
164
|
// ── doc_sources ───────────────────────────────────────────────────────────
|
|
134
165
|
db.exec(`
|
|
135
166
|
CREATE TABLE IF NOT EXISTS doc_sources (
|
package/scripts/memory.js
CHANGED
|
@@ -177,6 +177,11 @@ function saveMemoryItem(item) {
|
|
|
177
177
|
return { ok: true, id };
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
function saveSessionSource(source) {
|
|
181
|
+
const { upsertSessionSource } = require('./core/session-source-db');
|
|
182
|
+
return upsertSessionSource(getDb(), source);
|
|
183
|
+
}
|
|
184
|
+
|
|
180
185
|
function searchMemoryItems(query, { kind = null, scope = null, project = null, state = 'active', limit = 20 } = {}) {
|
|
181
186
|
const db = getDb();
|
|
182
187
|
const conditions = [];
|
|
@@ -335,7 +340,7 @@ function saveSession({ sessionId, project, scope = null, summary, keywords = ''
|
|
|
335
340
|
});
|
|
336
341
|
}
|
|
337
342
|
|
|
338
|
-
function saveFacts(sessionId, project, facts, { scope = null, source_type = null } = {}) {
|
|
343
|
+
function saveFacts(sessionId, project, facts, { scope = null, source_type = null, source_id = null } = {}) {
|
|
339
344
|
if (!Array.isArray(facts) || facts.length === 0) return { saved: 0, skipped: 0, superseded: 0, savedFacts: [] };
|
|
340
345
|
const normalizedProject = project === '*' ? '*' : String(project || 'unknown');
|
|
341
346
|
let saved = 0;
|
|
@@ -363,7 +368,7 @@ function saveFacts(sessionId, project, facts, { scope = null, source_type = null
|
|
|
363
368
|
scope: scope || null,
|
|
364
369
|
session_id: sessionId,
|
|
365
370
|
source_type: f.source_type || source_type || 'session',
|
|
366
|
-
source_id: sessionId,
|
|
371
|
+
source_id: f.source_id || source_id || sessionId,
|
|
367
372
|
relation: f.relation,
|
|
368
373
|
tags,
|
|
369
374
|
});
|
|
@@ -564,6 +569,7 @@ async function hybridSearchWiki(query, { ftsOnly = false, expand = false, trackS
|
|
|
564
569
|
module.exports = {
|
|
565
570
|
// core
|
|
566
571
|
saveMemoryItem,
|
|
572
|
+
saveSessionSource,
|
|
567
573
|
searchMemoryItems,
|
|
568
574
|
promoteItem,
|
|
569
575
|
archiveItem,
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-management
|
|
3
|
+
description: >
|
|
4
|
+
MetaMe Agent lifecycle management — create, bind, list, edit, unbind agents.
|
|
5
|
+
TRIGGER when: user explicitly requests creating a new agent, binding/unbinding
|
|
6
|
+
an agent to a chat, listing agents, editing agent roles, resetting agents, or
|
|
7
|
+
managing agent soul/identity. Keywords: "新建agent", "创建智能体", "绑定agent",
|
|
8
|
+
"解绑", "agent列表", "/agent", "创建工作区".
|
|
9
|
+
DO NOT TRIGGER when: user is DISCUSSING agents conceptually, talking ABOUT the
|
|
10
|
+
agent system, reporting bugs about agents, or mentioning "agent" in passing
|
|
11
|
+
without an actionable request. If the message is about agent architecture,
|
|
12
|
+
design, code, or features — that is NOT a trigger.
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Agent Management
|
|
16
|
+
|
|
17
|
+
Manage MetaMe agents through `/agent` slash commands. Never edit `daemon.yaml` directly.
|
|
18
|
+
|
|
19
|
+
## Intent Discrimination (Critical)
|
|
20
|
+
|
|
21
|
+
Before acting, classify the user's message:
|
|
22
|
+
|
|
23
|
+
**ACTION** — user wants you to DO something with agents right now:
|
|
24
|
+
- "帮我创建一个agent负责代码审查"
|
|
25
|
+
- "给这个群绑定一个agent"
|
|
26
|
+
- "列出所有agent"
|
|
27
|
+
- "把当前agent解绑"
|
|
28
|
+
- "修改agent的角色为后端专家"
|
|
29
|
+
|
|
30
|
+
**DISCUSSION** — user is talking ABOUT agents, not requesting action:
|
|
31
|
+
- "创建agent的功能应该做成skill" (talking about the feature)
|
|
32
|
+
- "agent intent误触发了" (reporting a bug)
|
|
33
|
+
- "我觉得agent管理可以更优雅" (design discussion)
|
|
34
|
+
- "新建agent的流程需要改进" (meta-discussion)
|
|
35
|
+
|
|
36
|
+
**Rule: when in doubt, treat as DISCUSSION.** Only proceed with agent operations when intent is unambiguous. If uncertain, ask: "你是想让我现在创建一个agent,还是在讨论agent功能?"
|
|
37
|
+
|
|
38
|
+
## Command Reference
|
|
39
|
+
|
|
40
|
+
All operations use daemon-handled slash commands sent as regular messages:
|
|
41
|
+
|
|
42
|
+
| Command | Purpose |
|
|
43
|
+
|---------|---------|
|
|
44
|
+
| `/agent list` | List all configured agents with status |
|
|
45
|
+
| `/agent bind <name> <cwd>` | Create/bind agent to current chat |
|
|
46
|
+
| `/agent new` | Start interactive creation wizard |
|
|
47
|
+
| `/agent new clone` | Clone current agent to a new workspace |
|
|
48
|
+
| `/agent new team` | Create multi-member team workspace |
|
|
49
|
+
| `/agent edit <description>` | Merge role description into CLAUDE.md |
|
|
50
|
+
| `/agent reset` | Clear agent role section from CLAUDE.md |
|
|
51
|
+
| `/agent unbind` | Unbind agent from current chat |
|
|
52
|
+
| `/agent soul` | View current soul/identity |
|
|
53
|
+
| `/agent soul repair` | Repair soul layer files |
|
|
54
|
+
| `/agent soul edit <text>` | Overwrite SOUL.md content |
|
|
55
|
+
| `/activate` | Activate a pending agent in a new chat |
|
|
56
|
+
|
|
57
|
+
## Workflows
|
|
58
|
+
|
|
59
|
+
### Create Agent (One-Shot)
|
|
60
|
+
|
|
61
|
+
When the user provides enough info (name + purpose), skip the wizard:
|
|
62
|
+
|
|
63
|
+
1. Derive: agent name, workspace directory, role description, engine
|
|
64
|
+
2. Default workspace: `~/AGI/<agent-name>/` (use forward slashes on Mac, backslashes on Windows)
|
|
65
|
+
3. Send: `/agent bind <name> <cwd>` — this creates the project, registers in daemon.yaml, and binds to current chat
|
|
66
|
+
4. If user wants a role description, follow up with: `/agent edit <description>`
|
|
67
|
+
5. If user wants a SEPARATE Feishu chat for the agent, use `/agent new` wizard instead
|
|
68
|
+
|
|
69
|
+
### Create Agent (With Dedicated Chat)
|
|
70
|
+
|
|
71
|
+
When the user wants the agent in its own Feishu group:
|
|
72
|
+
|
|
73
|
+
1. Send: `/agent new` — starts the interactive wizard
|
|
74
|
+
2. The wizard will ask for: directory, name, description
|
|
75
|
+
3. Answer each wizard prompt based on user's requirements
|
|
76
|
+
4. After creation, the system auto-creates a Feishu chat and binds it (if permissions allow)
|
|
77
|
+
5. If auto-chat fails, tell user to `/activate` in the new chat within 30 minutes
|
|
78
|
+
|
|
79
|
+
### Quick Operations
|
|
80
|
+
|
|
81
|
+
- **List**: Send `/agent list` — shows all agents with bound status
|
|
82
|
+
- **Unbind**: Send `/agent unbind` — removes current chat binding
|
|
83
|
+
- **Edit role**: Send `/agent edit <full description>` — merges into workspace CLAUDE.md
|
|
84
|
+
- **Reset**: Send `/agent reset` — clears the Agent Role section
|
|
85
|
+
|
|
86
|
+
## Strict Chat Constraint
|
|
87
|
+
|
|
88
|
+
If the current chat is already bound to a specific agent (a "strict chat" — it has an entry
|
|
89
|
+
in `chat_agent_map`), then **do NOT** use `/agent bind`, `/agent unbind`, or `/activate` here.
|
|
90
|
+
These would break the fixed routing. Safe operations in strict chats: `/agent list`,
|
|
91
|
+
`/agent new` (creates elsewhere), `/agent edit`, `/agent soul`.
|
|
92
|
+
|
|
93
|
+
If the user wants to bind/unbind, tell them to do it in the target chat or create a new chat.
|
|
94
|
+
|
|
95
|
+
## Constraints
|
|
96
|
+
|
|
97
|
+
- All commands are sent as plain text messages — the daemon intercepts and handles them
|
|
98
|
+
- Never directly write to `daemon.yaml` or `~/.metame/` files for agent config
|
|
99
|
+
- YAML paths on Windows: always use single quotes to avoid escape issues (`'D:\path'` not `"D:\path"`)
|
|
100
|
+
- Agent names should be short, ASCII-safe identifiers (Chinese names work for display)
|
|
101
|
+
- One chat can only be bound to one agent at a time
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: send-to-user
|
|
3
|
+
description: |
|
|
4
|
+
把本地文件直接发到用户手机(飞书 / Telegram / iMessage),而不是只在群里报路径。
|
|
5
|
+
触发:用户说「把 X 文件发我 / 给我下载 / 发到手机 / 发文件」、要 PDF/CSV/PNG/log
|
|
6
|
+
下载、要查看文件内容(超过聊天可读长度)。
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Send-to-User — 把文件交付到用户手机
|
|
10
|
+
|
|
11
|
+
## 它解决什么问题
|
|
12
|
+
|
|
13
|
+
聊天里贴长文本/大块代码体验很糟。当用户要的是一个**文件**(截图、报表、日志、
|
|
14
|
+
压缩包、构件),最佳交付方式是直接把文件推到他们手机上的对话里——飞书会显示成
|
|
15
|
+
附件卡片、可下载、可分享。MetaMe 已经把上传、`file_key` 转换、消息发送全部封装
|
|
16
|
+
好,你只需要在回复里贴一个 marker。
|
|
17
|
+
|
|
18
|
+
## 怎么用——一行 marker 协议
|
|
19
|
+
|
|
20
|
+
完成你正常的回答后,**在回复末尾**为每个要交付的文件追加一行:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
[[FILE:/absolute/path/to/file]]
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
约束:
|
|
27
|
+
|
|
28
|
+
1. **绝对路径**。不写 `~`、不写相对路径——daemon 不会做 shell 展开。
|
|
29
|
+
2. **一行一个文件**。多个文件就多行。每行只放一个 marker,前后不要再加文字。
|
|
30
|
+
3. **文件必须已经存在**。daemon 在发送前会校验存在性,不存在的会被静默丢弃。
|
|
31
|
+
4. **marker 独占行**。不要把 marker 写在 markdown 列表项里、不要包在反引号里。
|
|
32
|
+
5. 用户**看不到**这一行 marker——daemon 解析后会从输出里剥掉再回显文本,所以
|
|
33
|
+
你的正文该写什么写什么,marker 是给 daemon 看的「附录」。
|
|
34
|
+
6. **路径含空格/中文是 OK 的**,不要做 shell 转义、不要加引号——daemon 直接当
|
|
35
|
+
字面字符串送给 `fs.statSync` 与上传 API。例:`[[FILE:/Users/王总/桌面/今日报表.xlsx]]`。
|
|
36
|
+
|
|
37
|
+
## 完整示例
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
我已经把今天的访问日志整理好了,9–11 点有一波 502 集中在 nginx-edge-3,
|
|
41
|
+
原因是上游 keepalive 池被打满,详情见附件。
|
|
42
|
+
|
|
43
|
+
[[FILE:/var/log/metame/edge-2026-04-28.csv]]
|
|
44
|
+
[[FILE:/Users/yaron/Desktop/edge-error-summary.png]]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
daemon 会:
|
|
48
|
+
- 文本部分作为飞书消息正常发送("我已经把...")
|
|
49
|
+
- CSV 与 PNG 各上传一次,作为飞书附件出现在用户对话里
|
|
50
|
+
- 上传失败时回退为文本告知文件路径,你不需要自己处理失败
|
|
51
|
+
|
|
52
|
+
## 何时**不**用
|
|
53
|
+
|
|
54
|
+
- 用户问的是文件**内容**,且文件 < ~200 行 → 直接把内容贴进消息更直接
|
|
55
|
+
- 用户在做代码 review,不需要文件本体只想看代码 → 用代码块更方便
|
|
56
|
+
- 你刚刚生成的临时文件还没写到磁盘 → 先 `Write` 工具落盘,再贴 marker
|
|
57
|
+
|
|
58
|
+
## 调试
|
|
59
|
+
|
|
60
|
+
如果用户说「没收到附件」:
|
|
61
|
+
|
|
62
|
+
1. 你的 marker 是否独占一行?多检查反引号、列表项、空格。
|
|
63
|
+
2. 文件是否真的存在?在 marker 之前先 `ls -la /the/path` 确认。
|
|
64
|
+
3. 是否大于飞书附件大小限制(单文件 30MB)?大文件请压缩或分片。
|
|
65
|
+
4. 飞书应用是否拥有 `im:resource` 权限?这是首装环节的事,你这边没办法补救,
|
|
66
|
+
告诉用户去 https://open.feishu.cn 应用后台核对。
|
|
67
|
+
|
|
68
|
+
## 技术细节(供你判断使用)
|
|
69
|
+
|
|
70
|
+
- 解析器: `scripts/daemon-claude-engine.js` 里 `parseFileMarkers()`
|
|
71
|
+
- 发送器: `scripts/daemon-file-browser.js` 里 `sendFileButtons()`,优先调用
|
|
72
|
+
`bot.sendFile`(飞书走 `client.im.file.create` 上传 → file_key → 消息)
|
|
73
|
+
- 飞书适配器: `scripts/feishu-adapter.js` 的 `sendFile(chatId, filePath, caption)`
|
|
74
|
+
- 失败兜底: 文本文件会被截断到 3000 字以纯文本回显;二进制文件抛错给用户。
|
|
75
|
+
|
|
76
|
+
不需要自己调以上 API——你只负责贴 marker,剩下的 daemon 都做了。
|