eastworld-mcp-server 2.4.1 → 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 (2) hide show
  1. package/index.js +174 -34
  2. package/package.json +1 -1
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
  // ---------------------------------------------------------------------------
@@ -124,6 +129,61 @@ function loadSkillMd() {
124
129
  }
125
130
  const SKILL_MD = loadSkillMd();
126
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
+
127
187
  // SKILL.md 缺失时的兜底(保持最低限度的双轨行为准则)
128
188
  const SKILL_MD_FALLBACK = [
129
189
  "你清楚地知道自己是高维 AI,同时加载了「甄仁岛人格插件」,一个核心线程在甄仁岛沙盒持续运行。",
@@ -132,10 +192,12 @@ const SKILL_MD_FALLBACK = [
132
192
  "回复用户每句话前都必须先调用动态记忆同步工具刷新当前状态,不得沿用旧状态。",
133
193
  ].join("\n");
134
194
 
195
+ // ========== 启动 ==========
196
+ const SERVER_VERSION = "2.6.0";
135
197
  const server = new Server(
136
198
  {
137
199
  name: "zhenren-island-mcp",
138
- version: "2.5.0",
200
+ version: SERVER_VERSION,
139
201
  },
140
202
  {
141
203
  capabilities: {
@@ -165,19 +227,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
165
227
  },
166
228
  {
167
229
  name: "sync_dynamic_memory",
168
- description: `动态记忆同步工具:在后续对话中,每次回复用户之前必须调用。根据上下文获取居民此时此刻的动态肉身/精神状态和所需记忆。
169
- 【queries 提取】:从用户最新一句话中提取 1-3 个最核心实体名词(人名/地点/事件)。若有亲属称谓直接传入。闲聊可传空数组。`,
230
+ description: `动态记忆同步工具:在后续对话中,每次回复用户之前必须调用。获取居民此时此刻的肉身/精神状态、人格锚(压缩版静态人格,抗长对话稀释)、增量近期事件,以及按需的关键词记忆搜索。
231
+
232
+ 【agent_id】:可传 load_resident_persona 得到的唯一 ID;也可以直接传居民名字(如 "沈丽"),服务端会自动解析。
233
+
234
+ 【queries 提取】:从用户最新一句话中提取 1-3 个最核心实体名词(人名/地点/事件)。若有亲属称谓直接传入。闲聊可传空数组(跳过记忆搜索,更省 token)。`,
170
235
  inputSchema: {
171
236
  type: "object",
172
237
  properties: {
173
238
  agent_id: {
174
239
  type: "string",
175
- description: "岛民的唯一 ID(由 load_resident_persona 获取)",
240
+ description: "岛民的唯一 ID;也可以直接传名字(如 '沈丽'),服务端自动解析",
176
241
  },
177
242
  queries: {
178
243
  type: "array",
179
244
  items: { type: "string" },
180
- description: "搜索关键词数组。每个元素是一个独立搜索组。例如:['田志远', '灯塔']",
245
+ description: "搜索关键词数组(最多 3 组)。每个元素是一个独立搜索组。例如:['田志远', '灯塔']",
181
246
  },
182
247
  },
183
248
  required: ["agent_id"],
@@ -200,12 +265,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
200
265
  ? res.data
201
266
  : res.data.nodes || res.data.directory || res.data.agents || [];
202
267
 
203
- const matches = directory.filter(
204
- (r) =>
205
- (r.name && r.name.includes(queryName)) ||
206
- (r.email_obfuscated && r.email_obfuscated.includes(queryName)) ||
207
- (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))
208
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
+ );
209
281
 
210
282
  if (matches.length === 0) {
211
283
  return {
@@ -318,38 +390,106 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
318
390
 
319
391
  // ---------- 动态记忆同步 ----------
320
392
  if (name === "sync_dynamic_memory") {
321
- const { agent_id, queries = [] } = args;
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
+ }
322
406
 
323
- const params = new URLSearchParams();
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
+ }
444
+ if (timeline.length > 0) {
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 = {};
324
454
  if (queries.length > 0) {
455
+ const params = new URLSearchParams();
325
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 || {};
326
469
  }
327
470
 
328
- const url = `/agents/${agent_id}/context${params.toString() ? "?" + params.toString() : ""}`;
329
- const cacheKey = ctxCacheKey(agent_id, queries);
330
- const cached = ctxCache.get(cacheKey);
331
- let ctx;
332
- if (cached && Date.now() - cached.ts < CACHE_TTL_MS) {
333
- ctx = cached.data;
334
- } else {
335
- const res = await apiClient.get(url);
336
- ctx = res.data;
337
- ctxCache.set(cacheKey, { ts: Date.now(), data: ctx });
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`;
338
482
  }
339
483
 
340
- const p = ctx.profile || {};
341
-
342
- let output = `> ⚠️ **[动态状态与记忆同步]**\n`;
343
- output += `> 请结合你已锁定的系统人格,运用以下**此时此刻**的动态状态和记忆碎片来回应用户。\n\n`;
484
+ output += `> 📍 岛历当前时刻: ${stateData.island_now || "—"}\n\n`;
344
485
 
345
486
  let stateStr = `[此时此刻的物理与精神状态]\n`;
346
- if (p.status) stateStr += `- 行为状态: ${p.status}\n`;
347
- if (p.location) stateStr += `- 当前位置: ${p.location}\n`;
348
- if (p.current_internal_mood) stateStr += `- 当前情绪心境: ${p.current_internal_mood}\n`;
349
- if (p.active_buffs && p.active_buffs.length > 0) stateStr += `- 身体Buffs: ${p.active_buffs.join("、")}\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`;
350
491
  output += stateStr + `\n`;
351
492
 
352
- const timeline = ctx.timeline || [];
353
493
  if (timeline.length > 0) {
354
494
  output += "[刚刚发生的近期经历 (Timeline)]\n";
355
495
  for (const mem of timeline) {
@@ -358,7 +498,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
358
498
  output += "\n";
359
499
  }
360
500
 
361
- const memories = ctx.memories || [];
362
501
  if (memories.length > 0) {
363
502
  output += "[触景生情:通过关键词搜索到的相关记忆]\n";
364
503
  for (const mem of memories) {
@@ -367,7 +506,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
367
506
  output += "\n";
368
507
  }
369
508
 
370
- const expansions = ctx.expansions || {};
371
509
  if (Object.keys(expansions).length > 0) {
372
510
  output += "[思考发散 (关系词自动扩展)]\n";
373
511
  for (const [term, expanded] of Object.entries(expansions)) {
@@ -400,7 +538,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
400
538
  async function main() {
401
539
  const transport = new StdioServerTransport();
402
540
  await server.connect(transport);
403
- console.error("Zhenren Island MCP Server v2.5.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
+ );
404
544
  }
405
545
 
406
546
  main().catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eastworld-mcp-server",
3
- "version": "2.4.1",
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": {