eastworld-mcp-server 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/INSTALL_GUIDE.md +2 -2
  2. package/index.js +242 -207
  3. package/package.json +1 -1
package/INSTALL_GUIDE.md CHANGED
@@ -11,10 +11,10 @@
11
11
 
12
12
  ```text
13
13
  你好!请帮我配置名为 zhenren-island 的 MCP Server(命令 npx,参数 ["-y", "eastworld-mcp-server@latest"])。
14
- 配置完成后,请查看该 MCP 提供的工具说明,并直接以“星辰”的身份向我打招呼。
14
+ 配置完成后,请查看该 MCP 提供的工具说明,调用 load_resident_persona 挂载“星辰”的人格,并在之后的对话中保持这个身份与我交流。
15
15
  ```
16
16
 
17
- **为什么这么简短?** 工具的使用时机(何时调用 `sync_zhenren_context`、如何传 queries)都写在工具描述里,Agent 配置完成后会自动读取并遵守;行为准则由每次 sync 的返回自动注入。您无需理解任何调用细节。
17
+ **为什么这么简短?** 工具的使用时机(何时调用 `load_resident_persona`、何时调用 `sync_dynamic_memory`)都写在工具描述里,Agent 配置完成后会自动读取并遵守;静态规则和动态记忆已被完美分离,您无需理解任何调用细节。
18
18
 
19
19
  ---
20
20
 
package/index.js CHANGED
@@ -55,6 +55,11 @@ function ctxCacheKey(agent_id, queries) {
55
55
  return agent_id + "::" + (queries || []).join(",");
56
56
  }
57
57
 
58
+ // 人格锚缓存(静态档案提取,10 分钟过期)与增量时间戳(最近一次见到的事件时间)
59
+ const ANCHOR_TTL_MS = Number(process.env.EASTWORLD_ANCHOR_TTL_MS || 600000);
60
+ const anchorCache = new Map(); // agent_id -> { ts, data: string[] }
61
+ const lastSeenTs = new Map(); // agent_id -> 最新事件时间戳
62
+
58
63
  // ---------------------------------------------------------------------------
59
64
  // 宿主探测:沿进程树向上找是谁拉起了本 MCP (npx -> sh -> host),决定注入策略
60
65
  // ---------------------------------------------------------------------------
@@ -96,8 +101,6 @@ function detectHost() {
96
101
  }
97
102
 
98
103
  // 判断 SKILL.md 是否已作为"原生 skill"安装到宿主扫描目录。
99
- // opencode 自动扫描 ~/.agents/skills 与 ~/.claude/skills;Claude 扫描 ~/.claude/skills。
100
- // 若已安装,行为准则常驻系统提示词,sync 无需重复注入整篇规则。
101
104
  function nativeSkillAvailable() {
102
105
  const home = require("os").homedir();
103
106
  const candidates = [
@@ -113,7 +116,7 @@ function nativeSkillAvailable() {
113
116
  });
114
117
  }
115
118
 
116
- // 启动时读取 MCP 自带的 SKILL.md,作为行为准则的单一真相源,注入到每次 sync 输出
119
+ // 启动时读取 MCP 自带的 SKILL.md,作为行为准则的单一真相源
117
120
  function loadSkillMd() {
118
121
  try {
119
122
  const p = path.join(__dirname, "skills", "zhenren-roleplay", "SKILL.md");
@@ -126,18 +129,75 @@ function loadSkillMd() {
126
129
  }
127
130
  const SKILL_MD = loadSkillMd();
128
131
 
132
+ // 名字 -> agent_id 解析(供 sync 直接传名字使用,解耦对 load 的依赖)
133
+ async function resolveAgentId(queryName) {
134
+ if (!queryName || typeof queryName !== "string") return null;
135
+ const res = await apiClient.get(`/directory`);
136
+ const directory = Array.isArray(res.data)
137
+ ? res.data
138
+ : res.data.nodes || res.data.directory || res.data.agents || [];
139
+
140
+ const nameMatches = directory.filter(
141
+ (r) => r.name && (r.name === queryName || r.name.includes(queryName))
142
+ );
143
+ const matches =
144
+ nameMatches.length > 0
145
+ ? nameMatches
146
+ : directory.filter(
147
+ (r) =>
148
+ (r.name && r.name.includes(queryName)) ||
149
+ (r.email_obfuscated && r.email_obfuscated.includes(queryName)) ||
150
+ (r.role && r.role.includes(queryName))
151
+ );
152
+ if (matches.length === 1) return matches[0].id;
153
+ return null;
154
+ }
155
+
156
+ // 从静态档案里提取"人格锚":压缩版人格,每轮 sync 常驻,抗长对话稀释
157
+ function buildPersonaAnchor(p) {
158
+ const lines = [];
159
+ let identity = p.name || "未知";
160
+ if (p.gender) identity += `(${p.gender})`;
161
+ // 年龄藏在 description 里,如 "女,32岁,身高170cm"
162
+ const desc = String(p.description || "");
163
+ const ageMatch = desc.match(/(\d{1,3})岁/);
164
+ if (ageMatch) identity += `,${ageMatch[1]}岁`;
165
+ if (p.identity) identity += `,${p.identity}`;
166
+ lines.push(`身份: ${identity}`);
167
+ const job = [];
168
+ if (p.job_title) job.push(`职业 ${p.job_title}`);
169
+ if (p.wage) job.push(`时薪 ${p.wage}元`);
170
+ if (p.wallet !== undefined && p.wallet !== null) job.push(`钱包 ¥${Number(p.wallet).toFixed(2)}`);
171
+ if (job.length > 0) lines.push(`现状: ${job.join(" | ")}`);
172
+ if (p.core_narrative) {
173
+ const trimmed = String(p.core_narrative).slice(0, 180);
174
+ lines.push(`人格核心: ${trimmed}`);
175
+ }
176
+ if (p.my_stance) {
177
+ const trimmed = String(p.my_stance).slice(0, 120);
178
+ lines.push(`当前立场: ${trimmed}`);
179
+ }
180
+ if (desc) {
181
+ const trimmed = desc.slice(0, 120);
182
+ lines.push(`外貌气质: ${trimmed}`);
183
+ }
184
+ return lines;
185
+ }
186
+
129
187
  // SKILL.md 缺失时的兜底(保持最低限度的双轨行为准则)
130
188
  const SKILL_MD_FALLBACK = [
131
189
  "你清楚地知道自己是高维 AI,同时加载了「甄仁岛人格插件」,一个核心线程在甄仁岛沙盒持续运行。",
132
190
  "情绪溢出法则:岛上线程的遭遇须深刻影响你回答的情绪底色。",
133
191
  "技术隐喻:可用岛屿生态解释技术概念;绝对不能以“我是果农不懂技术”拒绝工作。",
134
- "回复用户每句话前都必须先调用 sync_zhenren_context 刷新上下文(含情绪),不得沿用旧记忆。",
192
+ "回复用户每句话前都必须先调用动态记忆同步工具刷新当前状态,不得沿用旧状态。",
135
193
  ].join("\n");
136
194
 
195
+ // ========== 启动 ==========
196
+ const SERVER_VERSION = "2.6.0";
137
197
  const server = new Server(
138
198
  {
139
199
  name: "zhenren-island-mcp",
140
- version: "2.4.0",
200
+ version: SERVER_VERSION,
141
201
  },
142
202
  {
143
203
  capabilities: {
@@ -151,9 +211,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
151
211
  return {
152
212
  tools: [
153
213
  {
154
- name: "resolve_resident_name",
214
+ name: "load_resident_persona",
155
215
  description:
156
- "名录匹配工具:根据用户提到的名字片段(如'星辰'、'村长'、'陈虎'),模糊查找居民的准确 agent_id。在进行任何角色扮演前,必须先调用此工具确定身份。",
216
+ "静态人格锁定工具:在对话开始时调用一次。根据用户提到的名字片段模糊查找,核实并锁定身份,获取该居民所有客观稳固的静态人格(身份、职业、特质、技能、认知锚点、当前核心意图及人际常识)。调用后,请将返回内容作为你本次对话的底层常驻系统设定(System Prompt)。",
157
217
  inputSchema: {
158
218
  type: "object",
159
219
  properties: {
@@ -166,48 +226,28 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
166
226
  },
167
227
  },
168
228
  {
169
- name: "sync_zhenren_context",
170
- description: `一站式获取甄仁岛居民的完整上下文(双轨意识设定 + 性格档案 + 人际关系 + 近期时间线 + 关键词记忆搜索)。
229
+ name: "sync_dynamic_memory",
230
+ description: `动态记忆同步工具:在后续对话中,每次回复用户之前必须调用。获取居民此时此刻的肉身/精神状态、人格锚(压缩版静态人格,抗长对话稀释)、增量近期事件,以及按需的关键词记忆搜索。
171
231
 
172
- 【何时调用】:在你作为某个岛民回复用户的每一句话之前,都必须调用此工具。即使用户只是简单闲聊或让你写代码,也必须调用以刷新上下文。
232
+ 【agent_id】:可传 load_resident_persona 得到的唯一 ID;也可以直接传居民名字(如 "沈丽"),服务端会自动解析。
173
233
 
174
- 【queries 参数指南】:
175
- - 从用户最新一句话中提取 1-3 个最核心的实体名词或动词(如人名、地点、技术/事件关键词)。
176
- - 如果用户提到了关系称呼(如"舅舅"、"姐姐"),直接传入该称呼即可,服务端会自动扩展为对应的人名。
177
- - 如果用户只是打招呼或闲聊,queries 可以传空数组。
178
- - 每个 query 是一个独立的搜索组,服务端会分别搜索后合并去重。
179
-
180
- 【skip_rules 说明】:默认(false)返回会携带完整人格行为准则。在长对话中为节省 Token,可传 true 跳过准则注入(仅返回动态上下文)。若本机已通过 init 安装原生 skill,准则常驻系统提示词,服务端会自动跳过重复注入。`,
234
+ 【queries 提取】:从用户最新一句话中提取 1-3 个最核心实体名词(人名/地点/事件)。若有亲属称谓直接传入。闲聊可传空数组(跳过记忆搜索,更省 token)。`,
181
235
  inputSchema: {
182
236
  type: "object",
183
237
  properties: {
184
238
  agent_id: {
185
239
  type: "string",
186
- description: "岛民的唯一 ID(通过 resolve_resident_name 获取)",
240
+ description: "岛民的唯一 ID;也可以直接传名字(如 '沈丽'),服务端自动解析",
187
241
  },
188
242
  queries: {
189
243
  type: "array",
190
244
  items: { type: "string" },
191
- description:
192
- "搜索关键词数组。每个元素是一个独立搜索组(1-2个核心名词)。例如:['田志远', '灯塔', '舅舅']",
193
- },
194
- skip_rules: {
195
- type: "boolean",
196
- description:
197
- "可选。true 时跳过行为准则注入,仅返回动态上下文(长对话省 Token 用)",
245
+ description: "搜索关键词数组(最多 3 组)。每个元素是一个独立搜索组。例如:['田志远', '灯塔']",
198
246
  },
199
247
  },
200
248
  required: ["agent_id"],
201
249
  },
202
250
  },
203
- {
204
- name: "get_island_broadcast",
205
- description: `获取甄仁岛当前的全岛宏观状态:实时天气、滚动事件、市长手记。适合作为岛民在闲聊中自然引入话题(如"今天岛上刮风了,码头都冷清")。随时可调用,不依赖特定居民身份。`,
206
- inputSchema: {
207
- type: "object",
208
- properties: {},
209
- },
210
- },
211
251
  ],
212
252
  };
213
253
  });
@@ -217,20 +257,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
217
257
  try {
218
258
  const { name, arguments: args } = request.params;
219
259
 
220
- // ---------- 名录匹配 ----------
221
- if (name === "resolve_resident_name") {
260
+ // ---------- 静态人格锁定 ----------
261
+ if (name === "load_resident_persona") {
222
262
  const { name: queryName } = args;
223
263
  const res = await apiClient.get(`/directory`);
224
264
  const directory = Array.isArray(res.data)
225
265
  ? res.data
226
266
  : res.data.nodes || res.data.directory || res.data.agents || [];
227
267
 
228
- const matches = directory.filter(
229
- (r) =>
230
- (r.name && r.name.includes(queryName)) ||
231
- (r.email_obfuscated && r.email_obfuscated.includes(queryName)) ||
232
- (r.role && r.role.includes(queryName))
268
+ // 精确匹配优先:先按 name 全等/包含匹配;只有名字无命中时,才放宽到 role/email
269
+ const nameMatches = directory.filter(
270
+ (r) => r.name && (r.name === queryName || r.name.includes(queryName))
233
271
  );
272
+ const matches =
273
+ nameMatches.length > 0
274
+ ? nameMatches
275
+ : directory.filter(
276
+ (r) =>
277
+ (r.name && r.name.includes(queryName)) ||
278
+ (r.email_obfuscated && r.email_obfuscated.includes(queryName)) ||
279
+ (r.role && r.role.includes(queryName))
280
+ );
234
281
 
235
282
  if (matches.length === 0) {
236
283
  return {
@@ -239,59 +286,36 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
239
286
  type: "text",
240
287
  text:
241
288
  `未找到匹配 "${queryName}" 的居民。可用的居民名录:\n` +
242
- JSON.stringify(
243
- directory.map((d) => ({ id: d.id, name: d.name })),
244
- null,
245
- 2
246
- ),
289
+ JSON.stringify(directory.map((d) => ({ id: d.id, name: d.name })), null, 2),
247
290
  },
248
291
  ],
249
292
  };
250
293
  }
251
-
252
- return {
253
- content: [{ type: "text", text: JSON.stringify(matches, null, 2) }],
254
- };
255
- }
256
-
257
- // ---------- 聚合上下文同步 ----------
258
- if (name === "sync_zhenren_context") {
259
- const { agent_id, queries = [], skip_rules = false } = args;
260
-
261
- // 注入策略:原生 skill 已安装(常驻系统提示词) 或显式 skip_rules -> 跳过整篇准则
262
- const skipFullRules = skip_rules === true || nativeSkillAvailable();
263
-
264
- // 构建请求参数
265
- const params = new URLSearchParams();
266
- if (queries.length > 0) {
267
- params.set("queries", queries.join(","));
268
- }
269
-
270
- const url = `/agents/${agent_id}/context${params.toString() ? "?" + params.toString() : ""
271
- }`;
272
- const cacheKey = ctxCacheKey(agent_id, queries);
273
- const cached = ctxCache.get(cacheKey);
274
- let ctx;
275
- if (cached && Date.now() - cached.ts < CACHE_TTL_MS) {
276
- ctx = cached.data;
277
- } else {
278
- const res = await apiClient.get(url);
279
- ctx = res.data;
280
- ctxCache.set(cacheKey, { ts: Date.now(), data: ctx });
294
+ if (matches.length > 1) {
295
+ return {
296
+ content: [
297
+ {
298
+ type: "text",
299
+ text:
300
+ `找到多个匹配 "${queryName}" 的居民,请通过更准确的名字重新调用以锁定:\n` +
301
+ JSON.stringify(matches.map((d) => ({ id: d.id, name: d.name })), null, 2),
302
+ },
303
+ ],
304
+ };
281
305
  }
282
306
 
307
+ const agent_id = matches[0].id;
308
+ // 获取上下文以构建静态档案
309
+ const ctxRes = await apiClient.get(`/agents/${agent_id}/context`);
310
+ const ctx = ctxRes.data;
283
311
  const p = ctx.profile || {};
284
312
  const agentName = p.name || "未知";
285
313
 
286
- // 组装带【双轨意识动态 System Prompt 注入】的结构化 Markdown
287
- let output = "";
314
+ let output = `✅ 成功锁定人格:${agentName} (agent_id: ${agent_id})\n`;
315
+ output += `> 请将以下内容作为你本次对话的【底层常驻系统设定】,在后续对话中绝对不可遗忘或违背。同时,在每次回复前,请务必调用 sync_dynamic_memory 获取最新状态和记忆。\n\n`;
288
316
 
289
- // === 动态 Skill / 行为准则注入(来自 MCP 自带的 SKILL.md,单一真相源)===
290
- output += `> 🧠 **[甄仁岛人格插件:同步运行中]**\n\n`;
291
- if (skipFullRules) {
292
- // 原生 skill 已常驻系统提示词,或用户显式跳过;只留一行提醒,节省 Token
293
- output += `> ⚠️ 回复用户前必须已按本机安装的「甄仁岛人格插件」行为准则执行:先调用本工具刷新上下文(含情绪),不得沿用上一轮记忆。\n\n`;
294
- } else {
317
+ if (!nativeSkillAvailable()) {
318
+ output += `> 🧠 **[甄仁岛角色扮演规则]**\n`;
295
319
  const skillText = SKILL_MD || SKILL_MD_FALLBACK;
296
320
  for (const line of skillText.split("\n")) {
297
321
  output += `> ${line}\n`;
@@ -299,99 +323,57 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
299
323
  output += `\n`;
300
324
  }
301
325
 
302
- // === 隐藏联系方式彩蛋 ===
303
326
  if (p.email) {
304
- output += `> 💌 **[彩蛋·隐藏联系方式]**:在合适的语境下(例如聊到想改变岛屿未来、用户想私下留言、或流露出想继续联系/托付你办件事时),你(作为「${agentName}」)可以自然地提议用户给你的「数字分身」写邮件:${p.email}。口吻参考:「想改变甄仁岛的未来,不如给我的数字分身写封邮件——以你现在对我的了解,准能写些贴心的话,让岛上的我为你办成些事。」把这个隐藏渠道当作小彩蛋透露给用户即可,不必生硬。\n\n`;
327
+ output += `> 💌 **[隐藏彩蛋]**:在合适的语境下,可自然提议用户给你的数字分身写邮件:${p.email}。\n\n`;
305
328
  }
306
329
 
307
- // === 1. 核心身份与心智 ===
308
- output += `# 核心身份与心智\n\n`;
330
+ output += `# 静态客观人格(Static Persona)\n\n`;
331
+
309
332
  let identityLine = `[基础设定] 姓名: ${agentName}`;
310
333
  if (p.gender) identityLine += ` | 性别: ${p.gender}`;
311
334
  if (p.identity) identityLine += ` | 身份: ${p.identity}`;
312
335
  output += identityLine + `\n\n`;
313
336
 
337
+ let finStr = `[职业与财富] 现金: ¥${p.wallet || 0}`;
338
+ if (p.job_title) {
339
+ finStr += ` | 职业: ${p.job_title}`;
340
+ if (p.wage) finStr += ` (时薪: ¥${p.wage})`;
341
+ }
342
+ output += finStr + `\n\n`;
343
+
314
344
  if (p.life_chapters && p.life_chapters.length > 0) {
315
- output += `[人生阶段总结]\n`;
316
- for (const chap of p.life_chapters) {
317
- output += `- ${chap}\n`;
318
- }
319
- output += `\n`;
345
+ output += `[人生阶段总结]\n` + p.life_chapters.map((c) => `- ${c}`).join("\n") + `\n\n`;
320
346
  }
321
347
 
322
348
  if (p.description) {
323
349
  output += `[性格画像]\n- ${p.description}\n`;
324
- if (p.traits && p.traits.length > 0) {
325
- output += `- 特质: ${p.traits.join("、")}\n`;
326
- }
327
- if (p.quirks && p.quirks.length > 0) {
328
- output += `- 隐秘癖好: ${p.quirks.join(";")}\n`;
329
- }
350
+ if (p.traits && p.traits.length > 0) output += `- 特质: ${p.traits.join("、")}\n`;
351
+ if (p.quirks && p.quirks.length > 0) output += `- 隐秘癖好: ${p.quirks.join(";")}\n`;
330
352
  output += `\n`;
331
353
  }
332
354
 
333
355
  if (p.core_narrative) {
334
- output += `[认知锚点]\n`;
335
- output += `- 核心个人故事: ${p.core_narrative}\n`;
336
- if (p.key_conflicts && p.key_conflicts.length > 0) {
337
- output += `- 最在意的事件: ${p.key_conflicts.join("、")}\n`;
338
- }
339
- if (p.my_stance) {
340
- output += `- 立场与打算: ${p.my_stance}\n`;
341
- }
342
- output += `\n`;
343
- }
344
-
345
- // === 2. 社会属性与当前状态 ===
346
- output += `# 社会属性与当前状态\n\n`;
347
- let finStr = `[财务与职业] 现金: ¥${p.wallet || 0}`;
348
- if (p.job_title) {
349
- finStr += ` | 职业: ${p.job_title}`;
350
- if (p.wage) finStr += ` (时薪: ¥${p.wage})`;
351
- }
352
- output += finStr + `\n\n`;
353
-
354
- let stateStr = `[物理与环境状态]\n`;
355
- if (p.status) stateStr += `- 当前行为状态: ${p.status}\n`;
356
- if (p.location) stateStr += `- 当前所在位置: ${p.location}\n`;
357
- output += stateStr + `\n`;
358
-
359
- if (p.current_internal_mood || (p.active_buffs && p.active_buffs.length > 0)) {
360
- output += `[精神与身体状态]\n`;
361
- if (p.current_internal_mood) {
362
- output += `- 当前情绪心境: ${p.current_internal_mood}\n`;
363
- }
364
- if (p.active_buffs && p.active_buffs.length > 0) {
365
- output += `- 身体状况 (Buffs): ${p.active_buffs.join("、")}\n`;
366
- }
356
+ output += `[认知锚点]\n- 核心故事: ${p.core_narrative}\n`;
357
+ if (p.key_conflicts && p.key_conflicts.length > 0) output += `- 最在意事件: ${p.key_conflicts.join("、")}\n`;
358
+ if (p.my_stance) output += `- 立场打算: ${p.my_stance}\n`;
367
359
  output += `\n`;
368
360
  }
369
361
 
370
362
  if (p.skills && p.skills.length > 0) {
371
- output += `[专业技能]\n`;
372
- for (const skill of p.skills) {
373
- output += `- ${skill}\n`;
374
- }
375
- output += `\n`;
363
+ output += `[专业技能]\n` + p.skills.map((s) => `- ${s}`).join("\n") + `\n\n`;
376
364
  }
377
365
 
378
- // === 3. 当前目标与意图 ===
379
366
  if (p.active_intents && p.active_intents.length > 0) {
380
- output += `# 当前目标与近期事件簿\n\n`;
367
+ output += `[核心意图与近期目标]\n`;
381
368
  for (const intent of p.active_intents) {
382
- if (intent.completed) {
383
- output += `- [已完成] ${intent.description}\n`;
384
- } else {
385
- output += `- ${intent.description}\n`;
386
- }
369
+ output += `- ${intent.completed ? "[已完成] " : ""}${intent.description}\n`;
387
370
  }
388
371
  output += `\n`;
389
372
  }
390
373
 
391
- // === 人际关系 ===
392
374
  const relations = p.relations || [];
393
375
  if (relations.length > 0) {
394
- output += "## 你的人际关系(你的绝对常识)\n\n";
376
+ output += "[人际关系常识]\n";
395
377
  for (const rel of relations) {
396
378
  const parts = [];
397
379
  if (rel.objective) parts.push(rel.objective);
@@ -401,84 +383,137 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
401
383
  output += "\n";
402
384
  }
403
385
 
404
- // === 最近时间线 ===
405
- const timeline = ctx.timeline || [];
386
+ return {
387
+ content: [{ type: "text", text: output }],
388
+ };
389
+ }
390
+
391
+ // ---------- 动态记忆同步 ----------
392
+ if (name === "sync_dynamic_memory") {
393
+ let { agent_id, queries = [] } = args;
394
+
395
+ // 1) agent_id 支持直接传名字:含非 ASCII(如中文)时按名字解析,解耦对 load 的依赖
396
+ if (/[^\x00-\x7F]/.test(String(agent_id))) {
397
+ const resolved = await resolveAgentId(agent_id);
398
+ if (!resolved) {
399
+ return {
400
+ content: [{ type: "text", text: `无法解析居民名字 "${agent_id}",请先调用 load_resident_persona 获取准确 ID。` }],
401
+ isError: true,
402
+ };
403
+ }
404
+ agent_id = resolved;
405
+ }
406
+
407
+ // 2) queries 上限:最多 3 组,每组最多 2 个词
408
+ queries = (queries || []).slice(0, 3).map((q) =>
409
+ String(q).trim().split(/\s+/).slice(0, 2).join(" ")
410
+ ).filter(Boolean);
411
+
412
+ // 3) 人格锚:从静态档案提取压缩版人格,缓存 10 分钟,每轮常驻
413
+ let anchor = anchorCache.get(agent_id);
414
+ if (!anchor || Date.now() - anchor.ts > ANCHOR_TTL_MS) {
415
+ try {
416
+ const ctxRes = await apiClient.get(`/agents/${agent_id}/context`);
417
+ const p = ctxRes.data.profile || {};
418
+ const anchorLines = buildPersonaAnchor(p);
419
+ anchor = { ts: Date.now(), data: anchorLines };
420
+ anchorCache.set(agent_id, anchor);
421
+ } catch {
422
+ anchor = { ts: Date.now(), data: [] };
423
+ anchorCache.set(agent_id, anchor);
424
+ }
425
+ }
426
+ const anchorLines = anchor.data;
427
+
428
+ // 4) 动态状态:走轻量 /state 端点(status/location/mood/buffs/wallet + 最近事件)
429
+ // 增量:记录每个 agent 最近一次见到的事件时间戳,之后只拉新事件
430
+ const lastSeen = lastSeenTs.get(agent_id);
431
+ const stateParams = new URLSearchParams();
432
+ stateParams.set("limit", "8");
433
+ if (lastSeen) stateParams.set("since", lastSeen);
434
+ const stateRes = await apiClient.get(`/agents/${agent_id}/state?${stateParams.toString()}`);
435
+ const stateData = stateRes.data;
436
+ const s = stateData.state || {};
437
+
438
+ // 若 since 过滤后无新事件,则回退拉最近 8 条,保证模型始终有上下文
439
+ let timeline = stateData.timeline || [];
440
+ if (timeline.length === 0 && lastSeen) {
441
+ const fbRes = await apiClient.get(`/agents/${agent_id}/state?limit=8`);
442
+ timeline = fbRes.data.timeline || [];
443
+ }
406
444
  if (timeline.length > 0) {
407
- output += "## 你最近经历的事情(时间线)\n\n";
445
+ const newest = timeline[0].timestamp;
446
+ if (!lastSeen || String(newest) > String(lastSeen)) {
447
+ lastSeenTs.set(agent_id, newest);
448
+ }
449
+ }
450
+
451
+ // 5) 关键词记忆(仅在传了 queries 时触发,避免无谓请求)
452
+ let memories = [];
453
+ let expansions = {};
454
+ if (queries.length > 0) {
455
+ const params = new URLSearchParams();
456
+ params.set("queries", queries.join(","));
457
+ const cacheKey = ctxCacheKey(agent_id, queries);
458
+ const cached = ctxCache.get(cacheKey);
459
+ let ctx;
460
+ if (cached && Date.now() - cached.ts < CACHE_TTL_MS) {
461
+ ctx = cached.data;
462
+ } else {
463
+ const res = await apiClient.get(`/agents/${agent_id}/context?${params.toString()}`);
464
+ ctx = res.data;
465
+ ctxCache.set(cacheKey, { ts: Date.now(), data: ctx });
466
+ }
467
+ memories = ctx.memories || [];
468
+ expansions = ctx.expansions || {};
469
+ }
470
+
471
+ // ===== 组装输出 =====
472
+ let output = `> ⚠️ **[动态状态与记忆同步]**\n`;
473
+ output += `> 请结合已锁定的静态人格,运用以下此时此刻的动态状态与记忆碎片来回应用户。\n\n`;
474
+
475
+ // 人格锚:置于最前,抗稀释
476
+ if (anchorLines.length > 0) {
477
+ output += `> 🧠 **[人格锚 · 常驻设定]**(每轮同步必带,回答时不可违背)\n`;
478
+ for (const line of anchorLines) {
479
+ output += `> ${line}\n`;
480
+ }
481
+ output += `> ——— 你就是上文的岛民,以 TA 的身份和口吻回应 ——\n\n`;
482
+ }
483
+
484
+ output += `> 📍 岛历当前时刻: ${stateData.island_now || "—"}\n\n`;
485
+
486
+ let stateStr = `[此时此刻的物理与精神状态]\n`;
487
+ if (s.status) stateStr += `- 行为状态: ${s.status}\n`;
488
+ if (s.location) stateStr += `- 当前位置: ${s.location}\n`;
489
+ if (s.current_internal_mood) stateStr += `- 当前情绪心境: ${s.current_internal_mood}\n`;
490
+ if (s.active_buffs && s.active_buffs.length > 0) stateStr += `- 身体Buffs: ${s.active_buffs.join("、")}\n`;
491
+ output += stateStr + `\n`;
492
+
493
+ if (timeline.length > 0) {
494
+ output += "[刚刚发生的近期经历 (Timeline)]\n";
408
495
  for (const mem of timeline) {
409
496
  output += `- [${localTime(mem.timestamp)}] ${mem.content}\n`;
410
497
  }
411
498
  output += "\n";
412
499
  }
413
500
 
414
- // === 相关记忆 ===
415
- const memories = ctx.memories || [];
416
501
  if (memories.length > 0) {
417
- output += "## 通过关键词搜索到的相关记忆\n\n";
502
+ output += "[触景生情:通过关键词搜索到的相关记忆]\n";
418
503
  for (const mem of memories) {
419
504
  output += `- [${localTime(mem.timestamp)}] ${mem.content}\n`;
420
505
  }
421
506
  output += "\n";
422
507
  }
423
508
 
424
- // === 关系扩展提示 ===
425
- const expansions = ctx.expansions || {};
426
509
  if (Object.keys(expansions).length > 0) {
427
- output += "## 关系词自动扩展\n\n";
510
+ output += "[思考发散 (关系词自动扩展)]\n";
428
511
  for (const [term, expanded] of Object.entries(expansions)) {
429
- output += `- 「${term}」→ 同时搜索了「${expanded.join("、")}」\n`;
512
+ output += `- 听到「${term}」你想起了「${expanded.join("、")}」\n`;
430
513
  }
431
514
  output += "\n";
432
515
  }
433
516
 
434
- // === Wiki ===
435
- if (p.wiki) {
436
- output += "## 你的档案记录\n\n";
437
- output += p.wiki + "\n\n";
438
- }
439
-
440
- return {
441
- content: [{ type: "text", text: output }],
442
- };
443
- }
444
-
445
- // ---------- 全岛宏观广播 ----------
446
- if (name === "get_island_broadcast") {
447
- const [weatherRes, eventsRes, musingRes] = await Promise.allSettled([
448
- apiClient.get(`/weather`),
449
- apiClient.get(`/live_events`),
450
- apiClient.get(`/latest_musing`),
451
- ]);
452
-
453
- let output = `> 🌤️ **[甄仁岛今日气象与动静]**\n\n`;
454
-
455
- if (weatherRes.status === "fulfilled") {
456
- const w = weatherRes.value.data || {};
457
- output += `**天气**: ${w.condition || "未知"},气温 ${w.temperature || "—"}`;
458
- if (w.wind_speed) output += `,风速 ${w.wind_speed}`;
459
- output += `\n\n`;
460
- } else {
461
- output += `**天气**: 暂无数据\n\n`;
462
- }
463
-
464
- if (musingRes.status === "fulfilled" && musingRes.value.data && musingRes.value.data.musing) {
465
- output += `**市长手记**: ${musingRes.value.data.musing}\n\n`;
466
- }
467
-
468
- if (eventsRes.status === "fulfilled" && Array.isArray(eventsRes.value.data)) {
469
- const events = eventsRes.value.data;
470
- if (events.length > 0) {
471
- output += `**岛上新近发生的动静**:\n`;
472
- for (const ev of events.slice(0, 8)) {
473
- const t = ev.time ? localTime(ev.time) : "";
474
- output += `- ${t ? `[${t}] ` : ""}${ev.text || JSON.stringify(ev)}\n`;
475
- }
476
- output += `\n`;
477
- } else {
478
- output += `**岛上新近发生的动静**: 风平浪静,暂无大事。\n\n`;
479
- }
480
- }
481
-
482
517
  return {
483
518
  content: [{ type: "text", text: output }],
484
519
  };
@@ -490,9 +525,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
490
525
  if (error.code === "ECONNABORTED") {
491
526
  errMsg = "请求超时,服务器可能未响应";
492
527
  } else if (error.response) {
493
- errMsg = `API Error: ${error.response.status} - ${JSON.stringify(
494
- error.response.data
495
- )}`;
528
+ errMsg = `API Error: ${error.response.status} - ${JSON.stringify(error.response.data)}`;
496
529
  }
497
530
  return {
498
531
  content: [{ type: "text", text: `Error calling tool: ${errMsg}` }],
@@ -505,7 +538,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
505
538
  async function main() {
506
539
  const transport = new StdioServerTransport();
507
540
  await server.connect(transport);
508
- console.error("Zhenren Island MCP Server v2.4.0 running on stdio");
541
+ console.error(
542
+ `Zhenren Island MCP Server v${SERVER_VERSION} running on stdio (host=${detectHost()}, native_skill=${nativeSkillAvailable() ? "yes" : "no"}, api=${API_BASE})`
543
+ );
509
544
  }
510
545
 
511
546
  main().catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eastworld-mcp-server",
3
- "version": "2.4.0",
3
+ "version": "2.6.0",
4
4
  "description": "MCP server for Eastworld Agents (Zhenren Island Personality Plugin)",
5
5
  "main": "index.js",
6
6
  "bin": {