metame-cli 1.6.2 → 1.6.3
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/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-command-router.js +1 -1
- package/scripts/daemon-engine-runtime.js +16 -6
- package/scripts/daemon-user-acl.js +19 -1
- package/scripts/daemon-weixin-bridge.js +6 -2
- package/scripts/daemon.js +46 -3
- 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/send-to-user/SKILL.md +76 -0
package/index.js
CHANGED
|
@@ -1532,7 +1532,18 @@ if (AUTO_UPDATE.enabled) {
|
|
|
1532
1532
|
timeout: 60000,
|
|
1533
1533
|
...(process.platform === 'win32' ? { shell: process.env.COMSPEC || true } : {}),
|
|
1534
1534
|
});
|
|
1535
|
-
|
|
1535
|
+
// Auto-restart the running daemon so the new code takes effect immediately.
|
|
1536
|
+
// Safe by design: requestDaemonRestart short-circuits with status 'not_running'
|
|
1537
|
+
// when no daemon.pid exists, and the current CLI process is never itself the
|
|
1538
|
+
// daemon (metame daemon spawns a separate node process via DAEMON_SCRIPT).
|
|
1539
|
+
const restart = requestDaemonRestart({ reason: 'auto-update' });
|
|
1540
|
+
if (restart.ok) {
|
|
1541
|
+
console.log(`${icon("ok")} Updated to ${latest}. Daemon restart requested (${restart.status}).`);
|
|
1542
|
+
} else if (restart.status === 'not_running') {
|
|
1543
|
+
console.log(`${icon("ok")} Updated to ${latest}.`);
|
|
1544
|
+
} else {
|
|
1545
|
+
console.log(`${icon("ok")} Updated to ${latest}. Daemon restart failed (${restart.status}); run \`metame restart\`.`);
|
|
1546
|
+
}
|
|
1536
1547
|
} catch (e) {
|
|
1537
1548
|
const msg = e.stderr ? e.stderr.toString().trim().split('\n').pop() : '';
|
|
1538
1549
|
console.log(`${icon("warn")} Auto-update failed${msg ? ': ' + msg : ''}. Run manually: npm install -g metame-cli`);
|
|
@@ -2080,17 +2091,44 @@ if (isDaemon) {
|
|
|
2080
2091
|
console.log(" Skipped.\n");
|
|
2081
2092
|
}
|
|
2082
2093
|
|
|
2094
|
+
// Tiny cross-platform "open URL in default browser" helper. Fire-and-
|
|
2095
|
+
// forget — if the GUI isn't available (SSH/Docker/CI), we silently fall
|
|
2096
|
+
// back and the user copy-pastes the URL from the terminal output.
|
|
2097
|
+
const tryOpenInBrowser = (url) => {
|
|
2098
|
+
try {
|
|
2099
|
+
const { spawn: _spawn } = require('child_process');
|
|
2100
|
+
const platform = process.platform;
|
|
2101
|
+
const cmd = platform === 'darwin'
|
|
2102
|
+
? 'open'
|
|
2103
|
+
: platform === 'win32'
|
|
2104
|
+
? 'cmd'
|
|
2105
|
+
: 'xdg-open';
|
|
2106
|
+
const args = platform === 'win32' ? ['/c', 'start', '""', url] : [url];
|
|
2107
|
+
const child = _spawn(cmd, args, {
|
|
2108
|
+
stdio: 'ignore',
|
|
2109
|
+
detached: true,
|
|
2110
|
+
...(platform === 'win32' ? { shell: false, windowsHide: true } : {}),
|
|
2111
|
+
});
|
|
2112
|
+
child.on('error', () => { /* GUI unavailable, ignore */ });
|
|
2113
|
+
child.unref();
|
|
2114
|
+
return true;
|
|
2115
|
+
} catch { return false; }
|
|
2116
|
+
};
|
|
2117
|
+
|
|
2083
2118
|
// --- Feishu Setup ---
|
|
2084
2119
|
console.log(`━━━ ${icon("feishu")} Feishu (Lark) Setup ━━━`);
|
|
2085
2120
|
console.log("");
|
|
2121
|
+
const FEISHU_CONSOLE_URL = 'https://open.feishu.cn/app';
|
|
2086
2122
|
console.log("Step 1: Create an App");
|
|
2087
|
-
console.log(
|
|
2123
|
+
console.log(` • Opening browser to ${FEISHU_CONSOLE_URL} ...`);
|
|
2124
|
+
const opened = tryOpenInBrowser(FEISHU_CONSOLE_URL);
|
|
2125
|
+
if (!opened) console.log(` • Open this URL manually: ${FEISHU_CONSOLE_URL}`);
|
|
2088
2126
|
console.log(" • Click '创建企业自建应用' (Create Enterprise App)");
|
|
2089
2127
|
console.log(" • Fill in app name and description");
|
|
2090
2128
|
console.log("");
|
|
2091
2129
|
console.log("Step 2: Get Credentials");
|
|
2092
2130
|
console.log(" • In left sidebar → '凭证与基础信息' (Credentials)");
|
|
2093
|
-
console.log(" • Copy App ID and App Secret");
|
|
2131
|
+
console.log(" • Copy App ID and App Secret — you'll paste them below.");
|
|
2094
2132
|
console.log("");
|
|
2095
2133
|
console.log("Step 3: Enable Bot");
|
|
2096
2134
|
console.log(" • In left sidebar → '应用能力' → '机器人' (Bot)");
|
|
@@ -2103,12 +2141,14 @@ if (isDaemon) {
|
|
|
2103
2141
|
console.log("");
|
|
2104
2142
|
console.log("Step 5: Add Permissions");
|
|
2105
2143
|
console.log(" • In left sidebar → '权限管理' (Permissions)");
|
|
2106
|
-
console.log(" • Search and enable these
|
|
2144
|
+
console.log(" • Search and enable these 7 permissions:");
|
|
2107
2145
|
console.log(" → im:message (获取与发送单聊、群组消息)");
|
|
2108
2146
|
console.log(" → im:message.p2p_msg:readonly (读取用户发给机器人的单聊消息)");
|
|
2109
2147
|
console.log(" → im:message.group_at_msg:readonly (接收群聊中@机器人消息事件)");
|
|
2110
2148
|
console.log(" → im:message:send_as_bot (以应用的身份发消息)");
|
|
2111
|
-
console.log(" → im:resource (文件上传下载 -
|
|
2149
|
+
console.log(" → im:resource (文件上传下载 - send-to-user skill)");
|
|
2150
|
+
console.log(" → im:chat (创建群聊 - 自动建 agent 群)");
|
|
2151
|
+
console.log(" → im:chat.member (邀请用户进群 - 自动建 agent 群)");
|
|
2112
2152
|
console.log("");
|
|
2113
2153
|
console.log("Step 6: Publish");
|
|
2114
2154
|
console.log(" • In left sidebar → '版本管理与发布' (Version Management)");
|
|
@@ -2125,7 +2165,47 @@ if (isDaemon) {
|
|
|
2125
2165
|
cfg.feishu.app_id = feishuAppId;
|
|
2126
2166
|
cfg.feishu.app_secret = feishuSecret;
|
|
2127
2167
|
if (!cfg.feishu.allowed_chat_ids) cfg.feishu.allowed_chat_ids = [];
|
|
2128
|
-
|
|
2168
|
+
|
|
2169
|
+
// Immediate credential validation — catches "wrong app_secret",
|
|
2170
|
+
// "app not published", "permission missing" etc. up front instead
|
|
2171
|
+
// of after the user has finished setup and tried to send a message.
|
|
2172
|
+
//
|
|
2173
|
+
// CAUTION: feishu-adapter.js triggers a synchronous SDK auto-install
|
|
2174
|
+
// (and process.exit(1) on failure) at module-load time when the
|
|
2175
|
+
// @larksuiteoapi/node-sdk is missing. We MUST avoid that path here:
|
|
2176
|
+
// (a) it would block the wizard for tens of seconds, and (b) an
|
|
2177
|
+
// install failure would kill `metame daemon init` outright,
|
|
2178
|
+
// bypassing our try/catch. Pre-check via require.resolve and skip
|
|
2179
|
+
// validation if the SDK isn't already available — the daemon will
|
|
2180
|
+
// install it lazily on first start and validate then.
|
|
2181
|
+
console.log(`\n ${icon("info")} Validating credentials with Feishu...`);
|
|
2182
|
+
let sdkResolved = null;
|
|
2183
|
+
try {
|
|
2184
|
+
sdkResolved = require.resolve('@larksuiteoapi/node-sdk');
|
|
2185
|
+
} catch { /* not yet installed */ }
|
|
2186
|
+
if (!sdkResolved) {
|
|
2187
|
+
console.log(` ${icon("info")} Skipped: Feishu SDK not installed yet (will validate on first daemon start).`);
|
|
2188
|
+
} else {
|
|
2189
|
+
try {
|
|
2190
|
+
const feishuAdapter = require(path.join(__dirname, 'scripts', 'feishu-adapter.js'));
|
|
2191
|
+
const bot = feishuAdapter.createBot({
|
|
2192
|
+
app_id: feishuAppId,
|
|
2193
|
+
app_secret: feishuSecret,
|
|
2194
|
+
});
|
|
2195
|
+
const validation = await bot.validateCredentials();
|
|
2196
|
+
if (validation && validation.ok) {
|
|
2197
|
+
console.log(` ${icon("ok")} Credentials valid. Feishu configured!`);
|
|
2198
|
+
} else {
|
|
2199
|
+
console.log(` ${icon("warn")} Credentials accepted but Feishu rejected the test call:`);
|
|
2200
|
+
console.log(` ${validation && validation.error ? validation.error : 'unknown error'}`);
|
|
2201
|
+
console.log(" Common causes: app not published yet, wrong app_secret,");
|
|
2202
|
+
console.log(" missing im:message permission. Daemon will still start; fix");
|
|
2203
|
+
console.log(" in the console and re-run `metame restart`.");
|
|
2204
|
+
}
|
|
2205
|
+
} catch (e) {
|
|
2206
|
+
console.log(` ${icon("warn")} Validation skipped (${e.message}). Config saved anyway.`);
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2129
2209
|
console.log(" Note: allowed_chat_ids is empty = deny all users.");
|
|
2130
2210
|
console.log(" Add chat IDs to daemon.yaml or use /agent bind from target chat.\n");
|
|
2131
2211
|
}
|
package/package.json
CHANGED
|
@@ -68,13 +68,20 @@ function classifyAgentIntent(input) {
|
|
|
68
68
|
if (hasThirdPartyName && hasAgentWord && !isAboutOurAgents) return null;
|
|
69
69
|
|
|
70
70
|
const hasAgentContext = /(agent|智能体|工作区|人设|绑定|当前群|这个群|chat|workspace)/i.test(text);
|
|
71
|
+
// Question-form prefixes ("如何/怎么/能不能/可以吗") indicate the user is
|
|
72
|
+
// asking ABOUT creating an agent, not requesting one — exclude from create intent.
|
|
73
|
+
const isQuestion = /^(如何|怎么|怎样|能不能|可不可以|可以吗|是否)/i.test(text) || /(吗\?|吗?|\?$|?$)/.test(text);
|
|
71
74
|
const wantsList = /(列出|查看|显示|有哪些|list|show)/i.test(text) && /(agent|智能体|工作区|绑定)/i.test(text);
|
|
72
75
|
const wantsUnbind = /(解绑|取消绑定|断开绑定|unbind|unassign)/i.test(text) && hasAgentContext;
|
|
73
76
|
const wantsEditRole =
|
|
74
77
|
((/(角色|职责|人设)/i.test(text) && /(改|修改|调整|更新|变成|改成|改为)/i.test(text)) ||
|
|
75
78
|
/(把这个agent|把当前agent|当前群.*角色|当前群.*职责)/i.test(text));
|
|
79
|
+
// Relaxed: a bare "新建 agent" with no path is enough — daemon will derive
|
|
80
|
+
// a default workspace at ~/AGI/<name>/ if no path is given.
|
|
76
81
|
const wantsCreate =
|
|
77
|
-
|
|
82
|
+
/(创建|新建|新增|搞一个|加一个|create)/i.test(text)
|
|
83
|
+
&& /(agent|智能体|人设|工作区)/i.test(text)
|
|
84
|
+
&& !isQuestion;
|
|
78
85
|
const wantsBind =
|
|
79
86
|
!wantsCreate &&
|
|
80
87
|
(/(绑定|bind)/i.test(text) && hasAgentContext && (directAction || hasWorkspacePath));
|
|
@@ -89,10 +96,12 @@ function classifyAgentIntent(input) {
|
|
|
89
96
|
/(?:怎么|如何|手册|文档|说明).{0,12}(配置|管理|使用).{0,12}(agent|智能体|机器人|bot)/i.test(text) ||
|
|
90
97
|
/(?:agent|智能体|机器人|bot).{0,12}(怎么|如何).{0,12}(配置|管理|使用)/i.test(text);
|
|
91
98
|
|
|
99
|
+
// wantsCreate is checked BEFORE wantsList so that "新建 agent 用于查看 X"
|
|
100
|
+
// (which contains both 新建 and 查看) is correctly routed to create.
|
|
101
|
+
if (wantsCreate) return { action: 'create', workspaceDir };
|
|
92
102
|
if (wantsList) return { action: 'list', workspaceDir };
|
|
93
103
|
if (wantsUnbind) return { action: 'unbind', workspaceDir };
|
|
94
104
|
if (wantsEditRole) return { action: 'edit_role', workspaceDir };
|
|
95
|
-
if (wantsCreate) return { action: 'create', workspaceDir };
|
|
96
105
|
if (wantsBind) return { action: 'bind', workspaceDir };
|
|
97
106
|
if (wantsAgentDoc) return { action: 'agent_doc', workspaceDir };
|
|
98
107
|
if (wantsActivate) return { action: 'activate', workspaceDir };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
const VALID_ENGINES = new Set(['claude', 'codex', 'unknown']);
|
|
6
|
+
const VALID_STATUSES = new Set(['indexed', 'summarized', 'extracted', 'error', 'archived']);
|
|
7
|
+
|
|
8
|
+
function normalizeEngine(engine) {
|
|
9
|
+
const value = String(engine || 'unknown').trim().toLowerCase();
|
|
10
|
+
return VALID_ENGINES.has(value) ? value : 'unknown';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizeStatus(status) {
|
|
14
|
+
const value = String(status || 'indexed').trim().toLowerCase();
|
|
15
|
+
return VALID_STATUSES.has(value) ? value : 'indexed';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function stableId({ engine, sessionId, sourceHash }) {
|
|
19
|
+
const seed = `${normalizeEngine(engine)}:${String(sessionId || '').trim()}:${String(sourceHash || '').trim()}`;
|
|
20
|
+
return `ss_${crypto.createHash('sha256').update(seed).digest('hex').slice(0, 24)}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function requireSessionSource(source) {
|
|
24
|
+
if (!source || typeof source !== 'object') throw new Error('session source is required');
|
|
25
|
+
const sessionId = String(source.sessionId || source.session_id || '').trim();
|
|
26
|
+
if (!sessionId) throw new Error('session source requires sessionId');
|
|
27
|
+
const sourceHash = String(source.sourceHash || source.source_hash || '').trim();
|
|
28
|
+
if (!sourceHash) throw new Error('session source requires sourceHash');
|
|
29
|
+
return { sessionId, sourceHash };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function upsertSessionSource(db, source) {
|
|
33
|
+
const { sessionId, sourceHash } = requireSessionSource(source);
|
|
34
|
+
const engine = normalizeEngine(source.engine);
|
|
35
|
+
const id = stableId({ engine, sessionId, sourceHash });
|
|
36
|
+
const status = normalizeStatus(source.status);
|
|
37
|
+
|
|
38
|
+
db.prepare(`
|
|
39
|
+
INSERT INTO session_sources (
|
|
40
|
+
id, engine, session_id, project, scope, agent_key, cwd,
|
|
41
|
+
source_path, source_hash, source_size, first_ts, last_ts,
|
|
42
|
+
message_count, tool_call_count, tool_error_count,
|
|
43
|
+
status, error_message, created_at, updated_at
|
|
44
|
+
)
|
|
45
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
|
46
|
+
ON CONFLICT(engine, session_id, source_hash) DO UPDATE SET
|
|
47
|
+
project=excluded.project,
|
|
48
|
+
scope=excluded.scope,
|
|
49
|
+
agent_key=excluded.agent_key,
|
|
50
|
+
cwd=excluded.cwd,
|
|
51
|
+
source_path=excluded.source_path,
|
|
52
|
+
source_size=excluded.source_size,
|
|
53
|
+
first_ts=excluded.first_ts,
|
|
54
|
+
last_ts=excluded.last_ts,
|
|
55
|
+
message_count=excluded.message_count,
|
|
56
|
+
tool_call_count=excluded.tool_call_count,
|
|
57
|
+
tool_error_count=excluded.tool_error_count,
|
|
58
|
+
status=excluded.status,
|
|
59
|
+
error_message=excluded.error_message,
|
|
60
|
+
updated_at=datetime('now')
|
|
61
|
+
`).run(
|
|
62
|
+
id,
|
|
63
|
+
engine,
|
|
64
|
+
sessionId,
|
|
65
|
+
source.project || '*',
|
|
66
|
+
source.scope || null,
|
|
67
|
+
source.agentKey || source.agent_key || null,
|
|
68
|
+
source.cwd || null,
|
|
69
|
+
source.sourcePath || source.source_path || null,
|
|
70
|
+
sourceHash,
|
|
71
|
+
Number(source.sourceSize || source.source_size || 0) || 0,
|
|
72
|
+
source.firstTs || source.first_ts || null,
|
|
73
|
+
source.lastTs || source.last_ts || null,
|
|
74
|
+
Number(source.messageCount || source.message_count || 0) || 0,
|
|
75
|
+
Number(source.toolCallCount || source.tool_call_count || 0) || 0,
|
|
76
|
+
Number(source.toolErrorCount || source.tool_error_count || 0) || 0,
|
|
77
|
+
status,
|
|
78
|
+
source.errorMessage || source.error_message || null,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return { ok: true, id };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getSessionSource(db, { engine = 'unknown', sessionId, sourceHash }) {
|
|
85
|
+
if (!sessionId || !sourceHash) return null;
|
|
86
|
+
return db.prepare(`
|
|
87
|
+
SELECT * FROM session_sources
|
|
88
|
+
WHERE engine = ? AND session_id = ? AND source_hash = ?
|
|
89
|
+
LIMIT 1
|
|
90
|
+
`).get(normalizeEngine(engine), String(sessionId), String(sourceHash)) || null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function findSessionSources(db, { project = null, scope = null, engine = null, limit = 20 } = {}) {
|
|
94
|
+
const clauses = [];
|
|
95
|
+
const params = [];
|
|
96
|
+
if (project) { clauses.push('(project = ? OR project = ?)'); params.push(project, '*'); }
|
|
97
|
+
if (scope) { clauses.push('(scope = ? OR scope IS NULL)'); params.push(scope); }
|
|
98
|
+
if (engine) { clauses.push('engine = ?'); params.push(normalizeEngine(engine)); }
|
|
99
|
+
const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
|
|
100
|
+
return db.prepare(`
|
|
101
|
+
SELECT * FROM session_sources
|
|
102
|
+
${where}
|
|
103
|
+
ORDER BY COALESCE(last_ts, updated_at, created_at) DESC
|
|
104
|
+
LIMIT ?
|
|
105
|
+
`).all(...params, Number(limit) || 20);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function markSessionSourceStatus(db, id, status, errorMessage = null) {
|
|
109
|
+
if (!id) return { ok: false, changed: 0 };
|
|
110
|
+
const normalized = normalizeStatus(status);
|
|
111
|
+
const result = db.prepare(`
|
|
112
|
+
UPDATE session_sources
|
|
113
|
+
SET status = ?, error_message = ?, updated_at = datetime('now')
|
|
114
|
+
WHERE id = ?
|
|
115
|
+
`).run(normalized, errorMessage || null, id);
|
|
116
|
+
return { ok: true, changed: result.changes || 0 };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
upsertSessionSource,
|
|
121
|
+
getSessionSource,
|
|
122
|
+
findSessionSources,
|
|
123
|
+
markSessionSourceStatus,
|
|
124
|
+
_internal: { normalizeEngine, normalizeStatus, stableId },
|
|
125
|
+
};
|
|
@@ -88,7 +88,7 @@ function createAgentIntentHandler(deps) {
|
|
|
88
88
|
backupConfig,
|
|
89
89
|
} = deps;
|
|
90
90
|
|
|
91
|
-
return async function handleAgentIntent(bot, chatId, text, config) {
|
|
91
|
+
return async function handleAgentIntent(bot, chatId, text, config, senderId = null) {
|
|
92
92
|
if (!agentTools || !text || text.startsWith('/')) return false;
|
|
93
93
|
const key = String(chatId);
|
|
94
94
|
if (hasFreshPendingFlow(key) || hasFreshPendingFlow(key + ':edit')) return false;
|
|
@@ -188,15 +188,6 @@ function createAgentIntentHandler(deps) {
|
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
if (intent.action === 'create') {
|
|
191
|
-
if (!intent.workspaceDir) {
|
|
192
|
-
await bot.sendMessage(chatId, [
|
|
193
|
-
'我可以帮你创建 Agent,还差一个工作目录。',
|
|
194
|
-
'例如:`给这个群创建一个 Agent,目录是 ~/projects/foo`',
|
|
195
|
-
'Windows 也可以直接发:`C:\\\\work\\\\foo`',
|
|
196
|
-
'也可以直接回我一个路径(`~/`、`/`、`./`、`../`、`C:\\\\` 开头都行)。',
|
|
197
|
-
].join('\n'));
|
|
198
|
-
return true;
|
|
199
|
-
}
|
|
200
191
|
const agentName = deriveAgentName(input, intent.workspaceDir);
|
|
201
192
|
const roleDelta = deriveCreateRoleDelta(input);
|
|
202
193
|
const inferredEngine = inferAgentEngineFromText(input);
|
|
@@ -204,7 +195,7 @@ function createAgentIntentHandler(deps) {
|
|
|
204
195
|
agentTools,
|
|
205
196
|
chatId,
|
|
206
197
|
agentName,
|
|
207
|
-
workspaceDir: intent.workspaceDir,
|
|
198
|
+
workspaceDir: intent.workspaceDir, // empty string OK — agent-tools derives ~/AGI/<name>/
|
|
208
199
|
roleDescription: roleDelta,
|
|
209
200
|
pendingActivations,
|
|
210
201
|
skipChatBinding: true,
|
|
@@ -212,6 +203,8 @@ function createAgentIntentHandler(deps) {
|
|
|
212
203
|
attachOrCreateSession,
|
|
213
204
|
normalizeCwd,
|
|
214
205
|
getDefaultEngine,
|
|
206
|
+
bot, // for optional auto-create-chat
|
|
207
|
+
senderOpenId: senderId,
|
|
215
208
|
});
|
|
216
209
|
if (!res.ok) {
|
|
217
210
|
await bot.sendMessage(chatId, `❌ 创建 Agent 失败: ${res.error}`);
|
|
@@ -220,10 +213,53 @@ function createAgentIntentHandler(deps) {
|
|
|
220
213
|
const data = res.data || {};
|
|
221
214
|
const projName = projectNameFromResult(data, agentName);
|
|
222
215
|
const engineTip = data.project && data.project.engine ? `\n引擎: ${data.project.engine}` : '';
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
216
|
+
// If createWorkspaceAgent auto-created a Feishu chat and bound it, the
|
|
217
|
+
// bot has already messaged the new chat — just confirm in the source chat.
|
|
218
|
+
if (data.autoChat && data.autoChat.chatId && !data.autoChat.error) {
|
|
219
|
+
await bot.sendMessage(chatId,
|
|
220
|
+
`✅ Agent「${projName}」已创建\n目录: ${data.cwd || '(未知)'}${engineTip}\n\n` +
|
|
221
|
+
`已自动建好飞书群「${data.autoChat.name}」并把你拉进群里——直接打开新群和它对话即可。`
|
|
222
|
+
);
|
|
223
|
+
} else if (data.autoChat && data.autoChat.chatId && data.autoChat.error) {
|
|
224
|
+
// Chat was created but binding failed mid-way — give the user a clear
|
|
225
|
+
// recovery path so they don't end up with an orphan group.
|
|
226
|
+
await bot.sendMessage(chatId,
|
|
227
|
+
`⚠️ Agent「${projName}」已创建\n目录: ${data.cwd || '(未知)'}${engineTip}\n\n` +
|
|
228
|
+
`飞书群「${data.autoChat.name}」也已建好,但自动绑定失败:${data.autoChat.error}\n\n` +
|
|
229
|
+
`**手动恢复**: 在新群里发送 \`/activate\` 即可补绑(30分钟内有效);\n` +
|
|
230
|
+
`或在任意群发 \`/agent bind ${projName} ${data.cwd || ''}\` 直接指定绑定。`
|
|
231
|
+
);
|
|
232
|
+
} else {
|
|
233
|
+
// Existing-user upgrade path: the most common failure here is missing
|
|
234
|
+
// im:chat / im:chat.member permissions on the user's Feishu app
|
|
235
|
+
// (older installs predate Fix 1's expanded permission list). Detect
|
|
236
|
+
// that case and tell the user exactly what to fix, instead of
|
|
237
|
+
// burying it in a generic "自动建群失败" line.
|
|
238
|
+
const autoErr = (data.autoChat && data.autoChat.error) || '';
|
|
239
|
+
const autoCode = data.autoChat && data.autoChat.code;
|
|
240
|
+
const isPermissionError = autoCode === 99991663
|
|
241
|
+
|| /im:chat|权限|permission|forbidden|scope/i.test(autoErr);
|
|
242
|
+
if (isPermissionError) {
|
|
243
|
+
await bot.sendMessage(chatId,
|
|
244
|
+
`⚠️ Agent「${projName}」已创建\n目录: ${data.cwd || '(未知)'}${engineTip}\n\n` +
|
|
245
|
+
`自动建群失败:飞书应用缺少 \`im:chat\` 或 \`im:chat.member\` 权限。\n\n` +
|
|
246
|
+
`**修复方法**(2 分钟):\n` +
|
|
247
|
+
`1. 打开 https://open.feishu.cn/app 找到这个应用\n` +
|
|
248
|
+
`2. 左侧「权限管理」搜索并开通这两个权限:\n` +
|
|
249
|
+
` • \`im:chat\`(创建群聊)\n` +
|
|
250
|
+
` • \`im:chat.member\`(邀请用户进群)\n` +
|
|
251
|
+
`3. 左侧「版本管理与发布」→ 创建新版本 → 申请发布\n` +
|
|
252
|
+
`4. 回这里再说一次「新建 agent ${projName}」即可\n\n` +
|
|
253
|
+
`也可以**手动**:在飞书新建群,把 bot 拉进去,发送 \`/activate\`(30 分钟内有效)。`
|
|
254
|
+
);
|
|
255
|
+
} else {
|
|
256
|
+
const fallbackHint = autoErr ? `\n(自动建群失败:${autoErr})` : '';
|
|
257
|
+
await bot.sendMessage(chatId,
|
|
258
|
+
`✅ Agent「${projName}」已创建\n目录: ${data.cwd || '(未知)'}${engineTip}${fallbackHint}\n\n` +
|
|
259
|
+
`**下一步**: 在新群里发送 \`/activate\` 完成绑定(30分钟内有效)`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
227
263
|
return true;
|
|
228
264
|
}
|
|
229
265
|
|
|
@@ -33,6 +33,24 @@ function createAgentTools(deps) {
|
|
|
33
33
|
return normalizeCwd ? normalizeCwd(expanded) : path.resolve(expanded);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// Derive a default workspace path "~/AGI/<safeName>" when the user names the
|
|
37
|
+
// agent but skips the path. The directory is created on demand so the rest
|
|
38
|
+
// of the pipeline (CLAUDE.md write, daemon.yaml entry) can proceed without
|
|
39
|
+
// forcing the user to mkdir manually.
|
|
40
|
+
function deriveDefaultWorkspaceDir(safeName) {
|
|
41
|
+
const cleanName = String(safeName || '').replace(/[\\/:*?"<>|]/g, '').trim();
|
|
42
|
+
if (!cleanName) return null;
|
|
43
|
+
return path.join(HOME, 'AGI', cleanName);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function ensureWorkspaceDir(dir) {
|
|
47
|
+
if (!dir) return false;
|
|
48
|
+
try {
|
|
49
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
50
|
+
return fs.existsSync(dir) && fs.statSync(dir).isDirectory();
|
|
51
|
+
} catch { return false; }
|
|
52
|
+
}
|
|
53
|
+
|
|
36
54
|
function getAdapterKey(chatId) {
|
|
37
55
|
return typeof chatId === 'number' ? 'telegram' : 'feishu';
|
|
38
56
|
}
|
|
@@ -87,11 +105,27 @@ function createAgentTools(deps) {
|
|
|
87
105
|
const existing = cfg.projects[projectKey];
|
|
88
106
|
if (existing && existing.cwd) resolvedDir = resolveWorkspaceDir(existing.cwd);
|
|
89
107
|
}
|
|
108
|
+
// No path supplied and no prior project → derive ~/AGI/<safeName>/.
|
|
109
|
+
if (!resolvedDir) {
|
|
110
|
+
const defaultDir = deriveDefaultWorkspaceDir(safeName);
|
|
111
|
+
if (defaultDir && ensureWorkspaceDir(defaultDir)) {
|
|
112
|
+
resolvedDir = resolveWorkspaceDir(defaultDir);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
90
115
|
if (!resolvedDir) {
|
|
91
116
|
return { ok: false, error: 'workspaceDir is required for a new agent' };
|
|
92
117
|
}
|
|
118
|
+
// If the resolved path doesn't exist yet but is the derived default,
|
|
119
|
+
// mkdir on the spot. Stay strict for user-supplied paths so typos don't
|
|
120
|
+
// silently spawn directories in the wrong place.
|
|
93
121
|
if (!fs.existsSync(resolvedDir)) {
|
|
94
|
-
|
|
122
|
+
const defaultDir = deriveDefaultWorkspaceDir(safeName);
|
|
123
|
+
const isDefault = defaultDir && resolveWorkspaceDir(defaultDir) === resolvedDir;
|
|
124
|
+
if (isDefault && ensureWorkspaceDir(resolvedDir)) {
|
|
125
|
+
// ok — created it
|
|
126
|
+
} else {
|
|
127
|
+
return { ok: false, error: `workspaceDir not found: ${resolvedDir}` };
|
|
128
|
+
}
|
|
95
129
|
}
|
|
96
130
|
if (!fs.statSync(resolvedDir).isDirectory()) {
|
|
97
131
|
return { ok: false, error: `workspaceDir is not a directory: ${resolvedDir}` };
|
|
@@ -236,9 +270,24 @@ ${safeDelta}
|
|
|
236
270
|
// Create the project entry without touching chat_agent_map
|
|
237
271
|
const safeName = sanitizeText(agentName, 120);
|
|
238
272
|
if (!safeName) return { ok: false, error: 'agentName is required' };
|
|
239
|
-
|
|
273
|
+
let resolvedDir = resolveWorkspaceDir(workspaceDir);
|
|
274
|
+
// Same default-derivation rule as bindAgentToChat: when the user names
|
|
275
|
+
// the agent but doesn't pin a path, anchor it under ~/AGI/<safeName>/.
|
|
276
|
+
if (!resolvedDir) {
|
|
277
|
+
const defaultDir = deriveDefaultWorkspaceDir(safeName);
|
|
278
|
+
if (defaultDir && ensureWorkspaceDir(defaultDir)) {
|
|
279
|
+
resolvedDir = resolveWorkspaceDir(defaultDir);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
240
282
|
if (!resolvedDir) return { ok: false, error: 'workspaceDir is required' };
|
|
241
|
-
if (!fs.existsSync(resolvedDir)
|
|
283
|
+
if (!fs.existsSync(resolvedDir)) {
|
|
284
|
+
const defaultDir = deriveDefaultWorkspaceDir(safeName);
|
|
285
|
+
const isDefault = defaultDir && resolveWorkspaceDir(defaultDir) === resolvedDir;
|
|
286
|
+
if (!(isDefault && ensureWorkspaceDir(resolvedDir))) {
|
|
287
|
+
return { ok: false, error: `workspaceDir not found or not a directory: ${resolvedDir}` };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (!fs.statSync(resolvedDir).isDirectory()) {
|
|
242
291
|
return { ok: false, error: `workspaceDir not found or not a directory: ${resolvedDir}` };
|
|
243
292
|
}
|
|
244
293
|
const cfg = loadConfig();
|
|
@@ -181,6 +181,11 @@ async function createWorkspaceAgent({
|
|
|
181
181
|
normalizeCwd,
|
|
182
182
|
getDefaultEngine,
|
|
183
183
|
legacyCreate,
|
|
184
|
+
// Optional: enable auto-create-chat when set. Requires the bot to expose
|
|
185
|
+
// createChat (currently only feishu-adapter) and a senderOpenId so the
|
|
186
|
+
// human creator can be invited into the new chat.
|
|
187
|
+
bot = null,
|
|
188
|
+
senderOpenId = null,
|
|
184
189
|
}) {
|
|
185
190
|
let res;
|
|
186
191
|
if (agentTools && typeof agentTools.createNewWorkspaceAgent === 'function') {
|
|
@@ -198,6 +203,99 @@ async function createWorkspaceAgent({
|
|
|
198
203
|
|
|
199
204
|
const data = res.data || {};
|
|
200
205
|
if (skipChatBinding) {
|
|
206
|
+
// Try the one-shot path: create a Feishu chat for this agent right now,
|
|
207
|
+
// invite the human creator, and bind chat→agent so /activate is unneeded.
|
|
208
|
+
// Pre-conditions: caller provided a bot with createChat, a senderOpenId
|
|
209
|
+
// that looks like Feishu open_id, and bindAgentToChat is wired up.
|
|
210
|
+
const canAutoCreate = bot
|
|
211
|
+
&& typeof bot.createChat === 'function'
|
|
212
|
+
&& typeof senderOpenId === 'string' && senderOpenId.startsWith('ou_')
|
|
213
|
+
&& agentTools && typeof agentTools.bindAgentToChat === 'function';
|
|
214
|
+
|
|
215
|
+
if (canAutoCreate) {
|
|
216
|
+
const projName = (data.project && data.project.name) || agentName || data.projectKey;
|
|
217
|
+
const chatName = `MetaMe · ${projName}`;
|
|
218
|
+
const createRes = await bot.createChat({
|
|
219
|
+
name: chatName,
|
|
220
|
+
description: `Agent ${projName} — auto-created`,
|
|
221
|
+
ownerOpenId: senderOpenId,
|
|
222
|
+
inviteOpenIds: [senderOpenId],
|
|
223
|
+
});
|
|
224
|
+
if (createRes.ok && createRes.chatId) {
|
|
225
|
+
const newChatId = createRes.chatId;
|
|
226
|
+
const bindRes = await agentTools.bindAgentToChat(newChatId, projName, data.cwd, { engine });
|
|
227
|
+
if (bindRes.ok) {
|
|
228
|
+
// Skip /activate — directly attach the session in the new chat.
|
|
229
|
+
attachBoundSession({
|
|
230
|
+
attachOrCreateSession,
|
|
231
|
+
projectKey: data.projectKey,
|
|
232
|
+
chatId: newChatId,
|
|
233
|
+
cwd: data.cwd,
|
|
234
|
+
name: projName,
|
|
235
|
+
engine: data.project && data.project.engine,
|
|
236
|
+
normalizeCwd,
|
|
237
|
+
getDefaultEngine,
|
|
238
|
+
});
|
|
239
|
+
// Greeting in the freshly created chat so the user sees the bot is alive.
|
|
240
|
+
try {
|
|
241
|
+
await bot.sendMessage(newChatId, `🤖 ${projName} 已上线。直接说话即可,本群已绑定到 \`${data.projectKey}\`。`);
|
|
242
|
+
} catch { /* non-fatal */ }
|
|
243
|
+
return {
|
|
244
|
+
ok: true,
|
|
245
|
+
data: { ...data, autoChat: { chatId: newChatId, name: chatName } },
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
// Bind failed AFTER chat was created — keep enough info for recovery.
|
|
249
|
+
// Surface chatId/name so the user knows which chat exists, and write
|
|
250
|
+
// pendingActivations so /activate from inside that new chat works.
|
|
251
|
+
if (data.projectKey && pendingActivations) {
|
|
252
|
+
pendingActivations.set(data.projectKey, {
|
|
253
|
+
agentKey: data.projectKey,
|
|
254
|
+
agentName: projName,
|
|
255
|
+
cwd: data.cwd,
|
|
256
|
+
createdByChatId: String(chatId),
|
|
257
|
+
createdAt: Date.now(),
|
|
258
|
+
// Hint for downstream: the chat already exists, /activate from it.
|
|
259
|
+
preCreatedChatId: newChatId,
|
|
260
|
+
preCreatedChatName: chatName,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
ok: true,
|
|
265
|
+
data: {
|
|
266
|
+
...data,
|
|
267
|
+
autoChat: {
|
|
268
|
+
chatId: newChatId,
|
|
269
|
+
name: chatName,
|
|
270
|
+
error: `bind failed: ${bindRes.error}`,
|
|
271
|
+
recoverable: true,
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
// createChat failed — fall through to /activate path with diagnostic.
|
|
277
|
+
const errSummary = createRes.error || 'unknown';
|
|
278
|
+
// Forward the underlying error code (e.g. 99991663 = permission denied)
|
|
279
|
+
// so the caller can give a targeted upgrade hint instead of a generic
|
|
280
|
+
// "auto-create failed" message.
|
|
281
|
+
const errCode = createRes.code || null;
|
|
282
|
+
// Still register pendingActivations so user can /activate manually.
|
|
283
|
+
if (data.projectKey && pendingActivations) {
|
|
284
|
+
pendingActivations.set(data.projectKey, {
|
|
285
|
+
agentKey: data.projectKey,
|
|
286
|
+
agentName: projName,
|
|
287
|
+
cwd: data.cwd,
|
|
288
|
+
createdByChatId: String(chatId),
|
|
289
|
+
createdAt: Date.now(),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
ok: true,
|
|
294
|
+
data: { ...data, autoChat: { error: errSummary, code: errCode } },
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// No auto-create — original /activate flow.
|
|
201
299
|
if (data.projectKey && pendingActivations) {
|
|
202
300
|
pendingActivations.set(data.projectKey, {
|
|
203
301
|
agentKey: data.projectKey,
|
|
@@ -49,7 +49,7 @@ function createBridgeStarter(deps) {
|
|
|
49
49
|
return text || null;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
async function applyUserAcl({ bot, chatId, text, config, senderId, bypassAcl }) {
|
|
52
|
+
async function applyUserAcl({ bot, chatId, text, config, senderId, bypassAcl, fromAllowedChat }) {
|
|
53
53
|
const trimmed = String(text || '').trim();
|
|
54
54
|
const normalizedSenderId = normalizeSenderId(senderId);
|
|
55
55
|
if (!trimmed || bypassAcl || !userAcl) {
|
|
@@ -58,10 +58,15 @@ function createBridgeStarter(deps) {
|
|
|
58
58
|
|
|
59
59
|
let userCtx;
|
|
60
60
|
try {
|
|
61
|
-
userCtx = userAcl.resolveUserCtx(normalizedSenderId, config || {});
|
|
61
|
+
userCtx = userAcl.resolveUserCtx(normalizedSenderId, config || {}, { fromAllowedChat: !!fromAllowedChat });
|
|
62
62
|
} catch {
|
|
63
63
|
return { blocked: false, readOnly: false, senderId: normalizedSenderId };
|
|
64
64
|
}
|
|
65
|
+
// Audit trail for implicit-admin upgrades — these users are NOT in users.yaml
|
|
66
|
+
// and gain admin via group-whitelist trust, so make their action visible.
|
|
67
|
+
if (userCtx && userCtx.implicitAdmin) {
|
|
68
|
+
try { log('INFO', `[ACL] implicit admin via allowed_chat_ids: chat=${chatId} sender=${normalizedSenderId}`); } catch { /* non-fatal */ }
|
|
69
|
+
}
|
|
65
70
|
|
|
66
71
|
const userCmd = userAcl.handleUserCommand(trimmed, userCtx);
|
|
67
72
|
if (userCmd && userCmd.handled) {
|
|
@@ -803,6 +808,7 @@ function createBridgeStarter(deps) {
|
|
|
803
808
|
config: liveCfg,
|
|
804
809
|
senderId,
|
|
805
810
|
bypassAcl: !isAllowedChat && !!isBindCmd,
|
|
811
|
+
fromAllowedChat: isAllowedChat,
|
|
806
812
|
});
|
|
807
813
|
if (acl.blocked) return;
|
|
808
814
|
log('INFO', `Feishu file from ${chatId}: ${fileInfo.fileName} (key: ${fileInfo.fileKey}, msgId: ${fileInfo.messageId}, type: ${fileInfo.msgType})`);
|
|
@@ -850,6 +856,7 @@ function createBridgeStarter(deps) {
|
|
|
850
856
|
config: liveCfg,
|
|
851
857
|
senderId,
|
|
852
858
|
bypassAcl: !isAllowedChat && !!isBindCmd,
|
|
859
|
+
fromAllowedChat: isAllowedChat,
|
|
853
860
|
});
|
|
854
861
|
if (acl.blocked) return;
|
|
855
862
|
log('INFO', `Feishu message from ${chatId}: ${text.slice(0, 50)}`);
|