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.
- package/index.js +174 -34
- 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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
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 (
|
|
347
|
-
if (
|
|
348
|
-
if (
|
|
349
|
-
if (
|
|
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(
|
|
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) => {
|