metame-cli 1.5.21 → 1.5.22

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.
@@ -0,0 +1,146 @@
1
+ 'use strict';
2
+
3
+ const VALID_TEAM_COLORS = ['green', 'yellow', 'red', 'blue', 'purple', 'orange', 'pink', 'indigo'];
4
+
5
+ function parseTeamMembers(input, teamName) {
6
+ const memberLines = String(input || '').split(/[,,\n]/).filter(line => line.trim());
7
+ const members = [];
8
+
9
+ for (const line of memberLines) {
10
+ const parts = line.trim().split(':');
11
+ const name = parts[0] && parts[0].trim();
12
+ if (!name) continue;
13
+
14
+ const icon = (parts[1] && parts[1].trim()) || '🤖';
15
+ const rawColor = parts[2] && parts[2].trim().toLowerCase();
16
+ const color = VALID_TEAM_COLORS.includes(rawColor)
17
+ ? rawColor
18
+ : VALID_TEAM_COLORS[members.length % VALID_TEAM_COLORS.length];
19
+
20
+ members.push({
21
+ key: name,
22
+ name: `${teamName} · ${name}`,
23
+ icon,
24
+ color,
25
+ nicknames: [name],
26
+ });
27
+ }
28
+
29
+ return members;
30
+ }
31
+
32
+ function findParentProjectKey({ projects, dirPath, normalizeCwd }) {
33
+ if (!projects || !dirPath) return null;
34
+ const targetDir = normalizeCwd(dirPath);
35
+
36
+ for (const [projKey, proj] of Object.entries(projects)) {
37
+ if (normalizeCwd(proj && proj.cwd ? proj.cwd : '') === targetDir) {
38
+ return projKey;
39
+ }
40
+ }
41
+
42
+ return null;
43
+ }
44
+
45
+ function ensureTeamMemberWorkspace({ fs, path, execSync, teamDir, teamName, member }) {
46
+ const memberDir = path.join(teamDir, member.key);
47
+ if (!fs.existsSync(memberDir)) fs.mkdirSync(memberDir, { recursive: true });
48
+
49
+ const claudeMdPath = path.join(memberDir, 'CLAUDE.md');
50
+ if (!fs.existsSync(claudeMdPath)) {
51
+ fs.writeFileSync(claudeMdPath, `# ${member.name}\n\n(团队成员:${teamName})\n`, 'utf8');
52
+ }
53
+
54
+ try {
55
+ if (typeof execSync === 'function') execSync('git init -q', { cwd: memberDir, stdio: 'ignore' });
56
+ } catch {
57
+ // Git init is a best-effort enhancement for checkpoints.
58
+ }
59
+
60
+ return {
61
+ ...member,
62
+ cwd: memberDir,
63
+ };
64
+ }
65
+
66
+ function registerTeamMembers({
67
+ cfg,
68
+ parentProjectKey,
69
+ members,
70
+ writeConfigSafe,
71
+ backupConfig,
72
+ }) {
73
+ if (!parentProjectKey || !cfg.projects || !cfg.projects[parentProjectKey]) return null;
74
+
75
+ const proj = cfg.projects[parentProjectKey];
76
+ if (!Array.isArray(proj.team)) proj.team = [];
77
+
78
+ for (const member of members) {
79
+ if (proj.team.some(existing => existing && existing.key === member.key)) continue;
80
+ proj.team.push({
81
+ key: member.key,
82
+ name: member.name,
83
+ icon: member.icon,
84
+ color: member.color,
85
+ cwd: member.cwd,
86
+ nicknames: member.nicknames,
87
+ });
88
+ }
89
+
90
+ if (typeof writeConfigSafe === 'function') writeConfigSafe(cfg);
91
+ if (typeof backupConfig === 'function') backupConfig();
92
+ return parentProjectKey;
93
+ }
94
+
95
+ function createTeamWorkspace({
96
+ fs,
97
+ path,
98
+ execSync,
99
+ dirPath,
100
+ teamName,
101
+ members,
102
+ loadConfig,
103
+ normalizeCwd,
104
+ writeConfigSafe,
105
+ backupConfig,
106
+ HOME,
107
+ }) {
108
+ const teamDir = path.join(dirPath, 'team');
109
+ if (!fs.existsSync(teamDir)) fs.mkdirSync(teamDir, { recursive: true });
110
+
111
+ const createdMembers = members.map((member) => ensureTeamMemberWorkspace({
112
+ fs,
113
+ path,
114
+ execSync,
115
+ teamDir,
116
+ teamName,
117
+ member,
118
+ }));
119
+
120
+ const cfg = typeof loadConfig === 'function' ? loadConfig() : { projects: {} };
121
+ const parentProjectKey = findParentProjectKey({
122
+ projects: cfg.projects,
123
+ dirPath,
124
+ normalizeCwd,
125
+ });
126
+
127
+ registerTeamMembers({
128
+ cfg,
129
+ parentProjectKey,
130
+ members: createdMembers,
131
+ writeConfigSafe,
132
+ backupConfig,
133
+ });
134
+
135
+ return {
136
+ teamDir,
137
+ parentProjectKey,
138
+ memberLines: createdMembers.map((member) => `${member.icon} ${member.key}: ${member.cwd.replace(HOME, '~')}`),
139
+ };
140
+ }
141
+
142
+ module.exports = {
143
+ VALID_TEAM_COLORS,
144
+ parseTeamMembers,
145
+ createTeamWorkspace,
146
+ };
package/scripts/daemon.js CHANGED
@@ -507,6 +507,7 @@ function createNullBot(onOutput) {
507
507
  deleteMessage: async () => { },
508
508
  sendFile: noop,
509
509
  downloadFile: noop,
510
+ notifyFinalOutput: async (text) => { if (onOutput) onOutput({ body: text, final: true }); return { message_id: '_virtual' }; },
510
511
  };
511
512
  }
512
513
 
@@ -610,6 +611,7 @@ function createStreamForwardBot(realBot, chatId, onOutput = null, opts = {}) {
610
611
  deleteMessage: async (_, msgId) => { await waitUntilReady(); return realBot.deleteMessage(chatId, msgId); },
611
612
  sendFile: async (_, filePath, caption) => { await waitUntilReady(); return realBot.sendFile(chatId, filePath, caption); },
612
613
  downloadFile: async (...args) => realBot.downloadFile(...args),
614
+ notifyFinalOutput: async (text) => { if (onOutput) onOutput({ body: text, final: true }); return { message_id: '_virtual' }; },
613
615
  };
614
616
  }
615
617
 
@@ -897,10 +899,11 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
897
899
 
898
900
  let _taskFinalized = false;
899
901
  const outputHandler = (output) => {
902
+ const isFinalOutput = !!(output && typeof output === 'object' && output.final);
900
903
  const outStr = typeof output === 'object' ? (output.body || JSON.stringify(output)) : String(output);
901
904
  const displayOut = envelope ? appendTeamTaskResumeHint(outStr, envelope.task_id, envelope.scope_id) : outStr;
902
905
  log('INFO', `Dispatch output from ${targetProject}: ${outStr.slice(0, 200)}`);
903
- if (envelope && taskBoard && !_taskFinalized && outStr.trim().length > 2) {
906
+ if (!isFinalOutput && envelope && taskBoard && !_taskFinalized && outStr.trim().length > 2) {
904
907
  const status = inferTaskStatusFromOutput(outStr);
905
908
  const artifacts = extractArtifactPaths(outStr);
906
909
  const update = {
@@ -916,9 +919,9 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
916
919
  });
917
920
  _taskFinalized = true;
918
921
  }
919
- if (replyFn && outStr.trim().length > 2) {
922
+ if (!isFinalOutput && replyFn && outStr.trim().length > 2) {
920
923
  replyFn(displayOut);
921
- } else if (!replyFn && fullMsg.callback && fullMsg.from && config) {
924
+ } else if (!isFinalOutput && !replyFn && fullMsg.callback && fullMsg.from && config) {
922
925
  // Write result to sender's inbox before dispatching callback
923
926
  try {
924
927
  const inboxDir = path.join(os.homedir(), '.metame', 'memory', 'inbox', fullMsg.from);
@@ -954,6 +957,7 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
954
957
 
955
958
  // ── Reactive lifecycle hook ──
956
959
  try {
960
+ if (!isFinalOutput) return;
957
961
  handleReactiveOutput(targetProject, outStr, loadConfig(), {
958
962
  log,
959
963
  loadState,
@@ -1,4 +1,4 @@
1
- # MetaMe Hook / Intent Engine 配置手册
1
+ # MetaMe Intent 配置手册
2
2
 
3
3
  > 自动部署到 `~/.metame/docs/hook-config.md`。源文件:`scripts/docs/hook-config.md`。只编辑 `scripts/`,不要直接改 `~/.metame/`。
4
4
 
@@ -8,15 +8,17 @@
8
8
 
9
9
  ```
10
10
  UserPromptSubmit (每轮用户输入)
11
- ├── signal-capture.js → 捕获用户偏好信号(写文件,不注入)
12
- └── intent-engine.js → 意图检测 + 按需注入 additionalSystemPrompt
11
+ └── signal-capture.js → 捕获用户偏好信号(写文件,不注入)
12
+
13
+ Daemon runtime (每轮真正发给引擎前)
14
+ └── intent-registry.js → 意图检测 + 按需拼接提示块
13
15
 
14
16
  Stop (每轮结束)
15
17
  └── stop-session-capture.js → session 事件日志 + 工具失败捕获
16
18
  ```
17
19
 
18
20
  `scripts/intent-registry.js` 是单一维护源,负责调用各意图模块并返回提示块。
19
- `intent-engine.js` Claude hook adapter;daemon 里的 Codex 路径也复用同一 registry。
21
+ daemon 在运行时直接复用同一 registry,Claude / Codex 共用这一条注入路径。
20
22
  零匹配 → 零输出(不浪费 token)。
21
23
 
22
24
  ---
@@ -25,13 +27,20 @@ Stop (每轮结束)
25
27
 
26
28
  | 模块 key | 文件 | 触发条件 | 注入内容 |
27
29
  |---------|------|---------|---------|
30
+ | `agent_capability` | `intent-agent-capability.js` | 明确的 Agent 创建/绑定/激活/分身/团队/角色/Soul 管理语境 | 当前真实支持的 `/agent ...` `/activate` 能力提示 |
28
31
  | `team_dispatch` | `intent-team-dispatch.js` | 检测到"告诉/让/发给 + 成员名"等联络意图 | `dispatch_to` 命令提示(仅匹配成员) |
29
32
  | `ops_assist` | `intent-ops-assist.js` | 回退/日志/重启/gc/状态 相关语境 | `/undo` `/restart` `/logs` `/gc` `/status` 命令提示 |
30
- | `task_create` | `intent-task-create.js` | 定时/提醒/每天X点 等调度语境 | `/task-add` 命令用法提示 |
31
33
  | `file_transfer` | `intent-file-transfer.js` | "发给我/发过来/导出" 等文件传输语境 | `[[FILE:...]]` 协议 + 收发规则 |
32
34
  | `weixin_bridge` | `intent-weixin-bridge.js` | "帮我绑定微信/配置微信桥接/开启微信接入/开始微信扫码登录" 等明确桥接语境 | 开启 `weixin.enabled` + `/weixin` 绑定流程提示 |
33
- | `memory_recall` | `intent-memory-recall.js` | "上次/之前/还记得" 等跨会话回忆语境 | `memory-search.js` 命令用法 |
34
- | `doc_router` | `intent-doc-router.js` | "创建/绑定 Agent"、"代码结构/脚本入口"、"hook/intent 配置" 等文档导向语境 | 统一 doc-router 文档指引 |
35
+ | `memory_recall` | `intent-memory-recall.js` | "上次/之前/还记得" 等跨会话回忆语境 | `memory-search.js` CLI 召回提示 |
36
+ | `doc_router` | `intent-doc-router.js` | "代码结构/脚本入口"、"hook/intent 配置" 等文档导向语境 | 统一 doc-router 文档指引 |
37
+ | `perpetual` | `intent-perpetual.js` | 永续/reactive 任务语境 | `dispatch_to` / `/status perpetual` 等永续协议提示 |
38
+ | `research` | `intent-research.js` | `paper_rev` 项目下的研究语境 | 研究方法与文件落点提示 |
39
+
40
+ 注入策略:
41
+ - 零匹配 → 零输出
42
+ - 多模块命中时按优先级裁剪,默认最多注入 2 个 hint、总长不超过约 1200 字符
43
+ - `doc_router` 只作为 fallback;只要已有真实能力提示命中,就不再额外塞文档路由
35
44
  ---
36
45
 
37
46
  ## 开关控制
@@ -40,13 +49,13 @@ Stop (每轮结束)
40
49
 
41
50
  ```yaml
42
51
  hooks:
52
+ agent_capability: true
43
53
  team_dispatch: true # 改为 false 可禁用
44
54
  ops_assist: true
45
- task_create: false # 禁用任务调度提示
46
55
  weixin_bridge: true # 默认开启;只匹配明确的微信配置/绑定语境
47
56
  ```
48
57
 
49
- 改完立即生效(intent-engine 每次运行时读取)。
58
+ 改完立即生效(daemon 每次发请求前都会读取)。
50
59
  **不需要重启 daemon。**
51
60
 
52
61
  ---
@@ -70,19 +79,20 @@ module.exports = function detect<Name>(prompt, config, projectKey) {
70
79
  };
71
80
  ```
72
81
 
73
- 2. **注册到 intent-engine.js**:
82
+ 2. **注册到 intent-registry.js**:
74
83
 
75
84
  ```js
76
- // scripts/hooks/intent-engine.js
85
+ // scripts/intent-registry.js
77
86
  const INTENT_MODULES = {
78
- team_dispatch: './intent-team-dispatch',
79
- ops_assist: './intent-ops-assist',
80
- task_create: './intent-task-create',
81
- your_name: './intent-<name>', // ← 加这行
87
+ agent_capability: require('./hooks/intent-agent-capability'),
88
+ team_dispatch: require('./hooks/intent-team-dispatch'),
89
+ ops_assist: require('./hooks/intent-ops-assist'),
90
+ your_name: require('./hooks/intent-<name>'), // ← 加这行
82
91
  };
83
92
 
84
93
  const DEFAULTS = {
85
94
  // ...
95
+ agent_capability: true,
86
96
  your_name: true, // ← 加这行(默认开启)
87
97
  };
88
98
  ```
@@ -96,11 +106,14 @@ hooks:
96
106
  your_name: true
97
107
  ```
98
108
 
99
- 4. **部署**:`node index.js`(同步文件到 `~/.metame/hooks/`)
109
+ 4. **部署**:`node index.js`
100
110
 
101
111
  5. **验证**:
102
112
  ```bash
103
- echo '{"prompt":"触发词"}' | node ~/.metame/hooks/intent-engine.js
113
+ node - <<'EOF'
114
+ const { buildIntentHintBlock } = require('./scripts/intent-registry');
115
+ console.log(buildIntentHintBlock('触发词', {}, ''));
116
+ EOF
104
117
  ```
105
118
 
106
119
  ---
@@ -109,7 +122,16 @@ echo '{"prompt":"触发词"}' | node ~/.metame/hooks/intent-engine.js
109
122
 
110
123
  ```bash
111
124
  # 测试某个 prompt 是否触发意图
112
- echo '{"prompt":"告诉工匠去做这个"}' | METAME_PROJECT=business node ~/.metame/hooks/intent-engine.js | python3 -m json.tool
125
+ node - <<'EOF'
126
+ const { buildIntentHintBlock } = require('./scripts/intent-registry');
127
+ console.log(buildIntentHintBlock('告诉工匠去做这个', {
128
+ projects: {
129
+ business: {
130
+ team: [{ key: 'builder', name: '工匠', nicknames: ['工匠'] }],
131
+ },
132
+ },
133
+ }, 'business'));
134
+ EOF
113
135
 
114
136
  # 查看当前已注册的 hooks
115
137
  python3 -c "
@@ -128,9 +150,7 @@ for k, v in s.get('hooks', {}).items():
128
150
 
129
151
  | 文件 | 说明 |
130
152
  |------|------|
131
- | `scripts/intent-registry.js` | 共享意图注册表(Claude hook / Codex runtime 共用) |
132
- | `scripts/hooks/intent-engine.js` | Claude hook adapter(源文件) |
133
- | `~/.metame/hooks/intent-engine.js` | 部署副本(copy) |
153
+ | `scripts/intent-registry.js` | 共享意图注册表(daemon 运行时唯一入口) |
134
154
  | `scripts/hooks/intent-*.js` | 各意图模块(源文件) |
135
155
  | `~/.metame/daemon.yaml` | 用户配置(包含 `hooks:` 开关) |
136
156
  | `~/.claude/settings.json` | Claude Code hook 注册表 |
@@ -179,7 +179,7 @@ feishu:
179
179
 
180
180
  在手机端(飞书/Telegram)发送以下任一方式触发创建向导:
181
181
 
182
- - 自然语言:`创建团队`、`新建工作组`、`建个团队` 等(`_detectTeamIntent` 识别,位于 `daemon-command-router.js`)
182
+ - 自然语言:`创建团队`、`新建工作组`、`建个团队` 等(`daemon-agent-intent.js` 统一识别并路由)
183
183
  - 命令:`/agent new team`
184
184
 
185
185
  向导分三步,全部在 `daemon-agent-commands.js` 中实现:
@@ -354,7 +354,7 @@ Claude 看到 hook 注入:
354
354
  | `daemon-admin-commands.js` | `/dispatch peers` 查看配置 + `/dispatch to peer:project` 手动派发 |
355
355
  | `scripts/bin/dispatch_to` | 支持 `peer:project` 格式 → 写 `remote-pending.jsonl` |
356
356
  | `daemon-team-dispatch.js` | `buildTeamRosterHint()` 为远端成员生成 `peer:key` 格式命令 |
357
- | `hooks/team-context.js` | intent hook 注入远端 `peer:key` dispatch 命令 |
357
+ | `hooks/intent-team-dispatch.js` | daemon 意图注入远端 `peer:key` dispatch 命令 |
358
358
 
359
359
  ### 管理命令
360
360
 
@@ -13,7 +13,7 @@
13
13
  |------|--------|------|
14
14
  | `session-analytics.js` | daemon-claude-engine, distill, memory-extract | 会话分析核心库 |
15
15
  | `mentor-engine.js` | daemon-claude-engine, daemon-admin-commands | AI 导师引擎 |
16
- | `intent-registry.js` | daemon-claude-engine, hooks/intent-engine | 意图识别注册表 |
16
+ | `intent-registry.js` | daemon-claude-engine | 意图识别注册表(daemon 运行时注入) |
17
17
  | `daemon-command-session-route.js` | daemon-exec-commands, daemon-ops-commands | 会话路由解析 |
18
18
  | `daemon-siri-bridge.js` | daemon-bridges.js | Siri HTTP 桥接 |
19
19
  | `daemon-siri-imessage.js` | daemon-siri-bridge.js | iMessage 数据库读取 |
@@ -53,7 +53,7 @@
53
53
  - `scripts/daemon-agent-tools.js`
54
54
  - 关键点:自然语言提取 `codex` 关键词;默认 `claude` 不写 `engine` 字段,仅 `codex` 持久化 `engine: codex`;
55
55
  `bindAgentToChat()` 自动调用 `ensureAgentMetadata()` 建立 soul 层;
56
- `_detectTeamIntent()` 自然语言意图识别(含负样本过滤),识别"建团队"意图后自动路由到 `/agent new team` 向导
56
+ `daemon-agent-intent.js` 统一处理 Agent/团队自然语言入口(含负样本过滤、Windows 路径识别、显式动作优先)
57
57
 
58
58
  - 会话命令与兼容边界:
59
59
  - `scripts/daemon-exec-commands.js`
@@ -113,7 +113,7 @@
113
113
  按昵称解析到远端 member 时自动走 `sendRemoteDispatch`
114
114
 
115
115
  - Intent Hook:
116
- - `scripts/hooks/team-context.js`
116
+ - `scripts/hooks/intent-team-dispatch.js`
117
117
  - 关键点:检测通信意图 → 注入 dispatch_to 命令提示;远端成员自动带 `peer:key` 前缀
118
118
 
119
119
  ## Mentor Mode(Step 1-4)定位
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ const { classifyAgentIntent } = require('../agent-intent-shared');
4
+
5
+ function buildLines(action) {
6
+ switch (action) {
7
+ case 'create':
8
+ return [
9
+ '- 创建并绑定当前群:直接用自然语言说“给这个群创建一个 Agent,目录是 ~/repo”',
10
+ '- 创建待激活 Agent:说“创建一个 codex agent,目录是 ~/repo”,再去目标群发 `/activate`',
11
+ ];
12
+ case 'bind':
13
+ return [
14
+ '- 绑定现有 Agent:`/agent bind <名称> <目录>`',
15
+ '- 也可直接说“给这个群绑定一个 Agent,目录是 ~/repo”',
16
+ ];
17
+ case 'list':
18
+ return ['- 查看已配置 Agent:`/agent list`'];
19
+ case 'unbind':
20
+ return ['- 解绑当前群:`/agent unbind`'];
21
+ case 'edit_role':
22
+ return ['- 修改当前 Agent 角色:`/agent edit <描述>`,或直接用自然语言说“把当前 agent 角色改成 ...”'];
23
+ case 'reset':
24
+ return ['- 清空当前 Agent 的角色定义:`/agent reset`'];
25
+ case 'soul':
26
+ return [
27
+ '- 查看当前 Soul:`/agent soul`',
28
+ '- 修复 Soul 文件:`/agent soul repair`',
29
+ '- 覆盖编辑 Soul:`/agent soul edit <内容>`',
30
+ ];
31
+ case 'activate':
32
+ return ['- 在新群完成绑定:进入目标群发送 `/activate`'];
33
+ case 'wizard_clone':
34
+ return ['- 创建当前 Agent 的分身:`/agent new clone`'];
35
+ case 'wizard_team':
36
+ return ['- 创建团队工作区:`/agent new team`'];
37
+ case 'agent_doc':
38
+ return ['- Agent 配置/管理文档:先看 `~/.metame/docs/agent-guide.md`'];
39
+ default:
40
+ return [];
41
+ }
42
+ }
43
+
44
+ module.exports = function detectAgentCapability(prompt) {
45
+ const intent = classifyAgentIntent(prompt);
46
+ if (!intent) return null;
47
+
48
+ const lines = buildLines(intent.action);
49
+ if (lines.length === 0) return null;
50
+ return ['[Agent 能力提示]', ...lines].join('\n');
51
+ };
@@ -12,16 +12,18 @@
12
12
 
13
13
  const { createDocRoute } = require('./doc-router');
14
14
 
15
+ const AGENT_DOC_PATTERNS = [
16
+ /(?:agent|智能体|机器人|bot).{0,12}(文档|手册|说明|guide)/i,
17
+ /(?:怎么|如何|手册|文档|说明).{0,12}(配置|管理|使用).{0,12}(agent|智能体|机器人|bot)/i,
18
+ /(?:agent|智能体|机器人|bot).{0,12}(怎么|如何).{0,12}(配置|管理|使用)/i,
19
+ ];
20
+
15
21
  const routes = [
16
22
  createDocRoute({
17
- patterns: [
18
- /(?:创建|新建|添加|注册|绑定|配置|管理).{0,8}(?:agent|机器人|bot|智能体)/i,
19
- /(?:agent|bot|智能体).{0,8}(?:创建|新建|添加|注册|绑定|配置|管理)/i,
20
- /\b(?:create|add|register|bind|manage|setup|configure)\s+(?:an?\s+)?agent\b/i,
21
- ],
22
- title: 'Agent 管理提示',
23
+ patterns: AGENT_DOC_PATTERNS,
24
+ title: 'Agent 文档提示',
23
25
  docPath: '~/.metame/docs/agent-guide.md',
24
- summary: '创建/管理/绑定 Agent',
26
+ summary: 'Agent 配置/管理/使用说明',
25
27
  }),
26
28
  createDocRoute({
27
29
  patterns: [
@@ -34,8 +36,8 @@ const routes = [
34
36
  }),
35
37
  createDocRoute({
36
38
  patterns: [
37
- /(?:hook|intent|意图).{0,10}(?:配置|设置|开关|新增|添加|修改|怎么配|怎么设置|怎么改)/i,
38
- /(?:配置|设置|开关|新增|添加|修改).{0,10}(?:hook|intent|意图)/i,
39
+ /(?:hook|intent|意图).{0,10}(?:配置|设置|开关|新增|添加|修改|怎么配|怎么设置|怎么改|原理)/i,
40
+ /(?:配置|设置|开关|新增|添加|修改|原理).{0,10}(?:hook|intent|意图)/i,
39
41
  /intent.?engine/i,
40
42
  /意图引擎|意图模块/,
41
43
  ],
@@ -45,10 +47,20 @@ const routes = [
45
47
  }),
46
48
  ];
47
49
 
48
- module.exports = function detectDocRouter(prompt) {
50
+ function hasExplicitDocIntent(prompt) {
51
+ const text = String(prompt || '').trim();
52
+ if (!text) return false;
53
+ return AGENT_DOC_PATTERNS.some((pattern) => pattern.test(text)) ||
54
+ /(?:文档|手册|说明|guide|readme)/i.test(text);
55
+ }
56
+
57
+ function detectDocRouter(prompt) {
49
58
  const hints = routes
50
59
  .map((detect) => detect(prompt))
51
60
  .filter(Boolean);
52
61
 
53
62
  return hints.length ? hints.join('\n') : null;
54
- };
63
+ }
64
+
65
+ module.exports = detectDocRouter;
66
+ module.exports.hasExplicitDocIntent = hasExplicitDocIntent;
@@ -17,8 +17,6 @@ const RECALL_PATTERNS = [
17
17
  /之前.{0,4}(?:说过|讨论过|聊过|提到过|商量过|做过的)/,
18
18
  // "还记得/记不记得" — asking if AI remembers (exclude "你记得" which is often imperative)
19
19
  /(?:还记得|记不记得|记得吗)/,
20
- // "之前那个/上次那个" — referencing past artifacts
21
- /(?:之前|上次|前几天)那个/,
22
20
  // English recall patterns
23
21
  /\b(?:last time|previously|remember when|do you remember|earlier we)\b/i,
24
22
  ];
@@ -31,6 +29,6 @@ module.exports = function detectMemoryRecall(prompt) {
31
29
  '- 搜索记忆: `node ~/.metame/memory-search.js "关键词1" "keyword2"`',
32
30
  '- 一次传 3-4 个关键词(中文+英文+函数名)',
33
31
  '- `--facts` 只搜事实,`--sessions` 只搜会话',
34
- '- 统一召回: `require("./memory").assembleContext({ query, scope: { project, agent } })`',
32
+ '- 不要假设工作区里存在 `./memory` 模块;优先走 `memory-search.js` CLI 做召回',
35
33
  ].join('\n');
36
34
  };
@@ -4,7 +4,7 @@
4
4
  * Team Dispatch Intent Module
5
5
  *
6
6
  * Detects communication intent towards team members in the prompt.
7
- * Extracted from team-context.js same logic, pure function interface.
7
+ * Daemon-side intent module for team dispatch hints.
8
8
  *
9
9
  * @param {string} prompt - sanitized user prompt
10
10
  * @param {object} config - daemon.yaml config
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const detectDocRouter = require('./hooks/intent-doc-router');
4
+
3
5
  /**
4
6
  * Shared intent registry for all MetaMe runtime adapters.
5
7
  *
@@ -8,9 +10,9 @@
8
10
  */
9
11
 
10
12
  const DEFAULTS = Object.freeze({
13
+ agent_capability: true,
11
14
  team_dispatch: true,
12
15
  ops_assist: true,
13
- task_create: true,
14
16
  file_transfer: true,
15
17
  weixin_bridge: true,
16
18
  memory_recall: true,
@@ -20,17 +22,54 @@ const DEFAULTS = Object.freeze({
20
22
  });
21
23
 
22
24
  const INTENT_MODULES = Object.freeze({
25
+ agent_capability: {
26
+ detect: require('./hooks/intent-agent-capability'),
27
+ priority: 100,
28
+ },
23
29
  team_dispatch: require('./hooks/intent-team-dispatch'),
24
- ops_assist: require('./hooks/intent-ops-assist'),
25
- task_create: require('./hooks/intent-task-create'),
26
- file_transfer: require('./hooks/intent-file-transfer'),
27
- weixin_bridge: require('./hooks/intent-weixin-bridge'),
28
- memory_recall: require('./hooks/intent-memory-recall'),
29
- doc_router: require('./hooks/intent-doc-router'),
30
- perpetual: require('./hooks/intent-perpetual'),
31
- research: require('./hooks/intent-research'),
30
+ ops_assist: {
31
+ detect: require('./hooks/intent-ops-assist'),
32
+ priority: 80,
33
+ },
34
+ file_transfer: {
35
+ detect: require('./hooks/intent-file-transfer'),
36
+ priority: 95,
37
+ },
38
+ weixin_bridge: {
39
+ detect: require('./hooks/intent-weixin-bridge'),
40
+ priority: 90,
41
+ },
42
+ memory_recall: {
43
+ detect: require('./hooks/intent-memory-recall'),
44
+ priority: 70,
45
+ },
46
+ doc_router: {
47
+ detect: detectDocRouter,
48
+ priority: 10,
49
+ fallbackOnly: true,
50
+ },
51
+ perpetual: {
52
+ detect: require('./hooks/intent-perpetual'),
53
+ priority: 60,
54
+ },
55
+ research: {
56
+ detect: require('./hooks/intent-research'),
57
+ priority: 55,
58
+ },
32
59
  });
33
60
 
61
+ const DEFAULT_MAX_HINTS = 2;
62
+ const DEFAULT_MAX_HINT_CHARS = 1200;
63
+
64
+ function normalizeIntentModule(entry) {
65
+ if (typeof entry === 'function') return { detect: entry, priority: 50, fallbackOnly: false };
66
+ return {
67
+ detect: entry.detect,
68
+ priority: Number.isFinite(entry.priority) ? entry.priority : 50,
69
+ fallbackOnly: !!entry.fallbackOnly,
70
+ };
71
+ }
72
+
34
73
  function resolveEnabledIntents(config = {}) {
35
74
  const hooksCfg = (config.hooks && typeof config.hooks === 'object') ? config.hooks : {};
36
75
  return { ...DEFAULTS, ...hooksCfg };
@@ -42,18 +81,43 @@ function collectIntentHints(prompt, config = {}, projectKey = '') {
42
81
 
43
82
  const enabled = resolveEnabledIntents(config);
44
83
  const hints = [];
45
- for (const [key, detect] of Object.entries(INTENT_MODULES)) {
84
+ for (const [key, rawEntry] of Object.entries(INTENT_MODULES)) {
46
85
  if (enabled[key] === false) continue;
47
- const hint = detect(text, config, projectKey);
86
+ const entry = normalizeIntentModule(rawEntry);
87
+ const hint = entry.detect(text, config, projectKey);
48
88
  if (hint) hints.push({ key, hint });
49
89
  }
50
90
  return hints;
51
91
  }
52
92
 
53
93
  function buildIntentHintBlock(prompt, config = {}, projectKey = '') {
54
- return collectIntentHints(prompt, config, projectKey)
55
- .map(item => item.hint)
56
- .join('\n\n');
94
+ const maxHints = Number.isInteger(config && config.intent_max_hints) && config.intent_max_hints > 0
95
+ ? config.intent_max_hints
96
+ : DEFAULT_MAX_HINTS;
97
+ const maxChars = Number.isInteger(config && config.intent_max_hint_chars) && config.intent_max_hint_chars > 0
98
+ ? config.intent_max_hint_chars
99
+ : DEFAULT_MAX_HINT_CHARS;
100
+
101
+ let hits = collectIntentHints(prompt, config, projectKey)
102
+ .map((item) => ({ ...item, ...normalizeIntentModule(INTENT_MODULES[item.key]) }));
103
+
104
+ if (hits.some(item => !item.fallbackOnly) && !(typeof detectDocRouter.hasExplicitDocIntent === 'function' && detectDocRouter.hasExplicitDocIntent(prompt))) {
105
+ hits = hits.filter(item => !item.fallbackOnly);
106
+ }
107
+
108
+ hits.sort((a, b) => b.priority - a.priority);
109
+
110
+ const selected = [];
111
+ let usedChars = 0;
112
+ for (const item of hits) {
113
+ if (selected.length >= maxHints) break;
114
+ const nextChars = usedChars + item.hint.length;
115
+ if (selected.length > 0 && nextChars > maxChars) continue;
116
+ selected.push(item);
117
+ usedChars = nextChars;
118
+ }
119
+
120
+ return selected.map(item => item.hint).join('\n\n');
57
121
  }
58
122
 
59
123
  module.exports = {