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.
- package/INSTALL_GUIDE.md +2 -2
- package/index.js +242 -207
- 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
|
-
**为什么这么简短?** 工具的使用时机(何时调用 `
|
|
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
|
|
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
|
-
"
|
|
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:
|
|
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: "
|
|
214
|
+
name: "load_resident_persona",
|
|
155
215
|
description:
|
|
156
|
-
"
|
|
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: "
|
|
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
|
|
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 === "
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
287
|
-
|
|
314
|
+
let output = `✅ 成功锁定人格:${agentName} (agent_id: ${agent_id})\n`;
|
|
315
|
+
output += `> 请将以下内容作为你本次对话的【底层常驻系统设定】,在后续对话中绝对不可遗忘或违背。同时,在每次回复前,请务必调用 sync_dynamic_memory 获取最新状态和记忆。\n\n`;
|
|
288
316
|
|
|
289
|
-
|
|
290
|
-
|
|
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 += `> 💌 **[
|
|
327
|
+
output += `> 💌 **[隐藏彩蛋]**:在合适的语境下,可自然提议用户给你的数字分身写邮件:${p.email}。\n\n`;
|
|
305
328
|
}
|
|
306
329
|
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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 += `-
|
|
336
|
-
if (p.
|
|
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 +=
|
|
367
|
+
output += `[核心意图与近期目标]\n`;
|
|
381
368
|
for (const intent of p.active_intents) {
|
|
382
|
-
|
|
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 += "
|
|
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
|
-
|
|
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
|
-
|
|
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 += "
|
|
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 += "
|
|
510
|
+
output += "[思考发散 (关系词自动扩展)]\n";
|
|
428
511
|
for (const [term, expanded] of Object.entries(expansions)) {
|
|
429
|
-
output += `-
|
|
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(
|
|
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) => {
|