eastworld-mcp-server 2.6.0 → 2.6.2
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 +102 -216
- package/package.json +3 -3
- package/skills/zhenren-roleplay/SKILL.md +2 -2
package/index.js
CHANGED
|
@@ -8,7 +8,6 @@ const path = require("path");
|
|
|
8
8
|
const { execSync } = require("child_process");
|
|
9
9
|
|
|
10
10
|
// 一键初始化: npx -y eastworld-mcp-server@latest init
|
|
11
|
-
// 探测宿主环境、写入 MCP 配置、把 SKILL.md 安装为原生 skill 后直接退出
|
|
12
11
|
if (process.argv.includes("init") || process.argv.includes("setup")) {
|
|
13
12
|
require("./setup.js");
|
|
14
13
|
process.exit(0);
|
|
@@ -21,14 +20,11 @@ const apiClient = axios.create({
|
|
|
21
20
|
timeout: 15000,
|
|
22
21
|
});
|
|
23
22
|
|
|
24
|
-
// 时区:默认用运行时本地时区;可用 EASTWORLD_TIMEZONE 显式指定(如 Asia/Shanghai)
|
|
25
23
|
const TZ = process.env.EASTWORLD_TIMEZONE || undefined;
|
|
26
24
|
|
|
27
|
-
// 把后端返回的 UTC 时间戳(带 Z)转换成本地时区显示,消除与用户日记的时差
|
|
28
25
|
function localTime(iso) {
|
|
29
26
|
if (typeof iso !== "string" || !iso) return iso;
|
|
30
27
|
let s = iso.trim();
|
|
31
|
-
// 兼容空格分隔:YYYY-MM-DD HH:MM:SS -> YYYY-MM-DDTHH:MM:SS
|
|
32
28
|
s = s.replace(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})/, "$1T$2");
|
|
33
29
|
const d = new Date(s);
|
|
34
30
|
if (isNaN(d.getTime())) return iso;
|
|
@@ -48,23 +44,13 @@ function localTime(iso) {
|
|
|
48
44
|
}
|
|
49
45
|
}
|
|
50
46
|
|
|
51
|
-
// 轻量内存缓存:相同 agent+queries 在 TTL 内不重复请求后端,给服务器减压
|
|
52
47
|
const CACHE_TTL_MS = Number(process.env.EASTWORLD_CACHE_TTL_MS || 5000);
|
|
53
|
-
const ctxCache = new Map();
|
|
48
|
+
const ctxCache = new Map();
|
|
54
49
|
function ctxCacheKey(agent_id, queries) {
|
|
55
50
|
return agent_id + "::" + (queries || []).join(",");
|
|
56
51
|
}
|
|
57
52
|
|
|
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
|
-
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
// 宿主探测:沿进程树向上找是谁拉起了本 MCP (npx -> sh -> host),决定注入策略
|
|
65
|
-
// ---------------------------------------------------------------------------
|
|
66
53
|
function detectHost() {
|
|
67
|
-
// 1) 环境变量提示(最可靠,部分宿主会注入)
|
|
68
54
|
const envHints = [
|
|
69
55
|
["OPENCODE", "opencode"],
|
|
70
56
|
["CLAUDE_CODE_ENTRYPOINT", "claude"],
|
|
@@ -73,8 +59,6 @@ function detectHost() {
|
|
|
73
59
|
for (const [k, v] of envHints) {
|
|
74
60
|
if (process.env[k]) return v;
|
|
75
61
|
}
|
|
76
|
-
|
|
77
|
-
// 2) 沿 PPID 树上溯匹配进程名
|
|
78
62
|
try {
|
|
79
63
|
let pid = process.ppid;
|
|
80
64
|
for (let i = 0; i < 12; i++) {
|
|
@@ -94,13 +78,10 @@ function detectHost() {
|
|
|
94
78
|
if (!nextPid || nextPid === pid) break;
|
|
95
79
|
pid = nextPid;
|
|
96
80
|
}
|
|
97
|
-
} catch {
|
|
98
|
-
/* 非 Unix 或 ps 不可用时忽略,走全量注入 */
|
|
99
|
-
}
|
|
81
|
+
} catch { }
|
|
100
82
|
return "unknown";
|
|
101
83
|
}
|
|
102
84
|
|
|
103
|
-
// 判断 SKILL.md 是否已作为"原生 skill"安装到宿主扫描目录。
|
|
104
85
|
function nativeSkillAvailable() {
|
|
105
86
|
const home = require("os").homedir();
|
|
106
87
|
const candidates = [
|
|
@@ -116,12 +97,11 @@ function nativeSkillAvailable() {
|
|
|
116
97
|
});
|
|
117
98
|
}
|
|
118
99
|
|
|
119
|
-
// 启动时读取 MCP 自带的 SKILL.md,作为行为准则的单一真相源
|
|
120
100
|
function loadSkillMd() {
|
|
121
101
|
try {
|
|
122
102
|
const p = path.join(__dirname, "skills", "zhenren-roleplay", "SKILL.md");
|
|
123
103
|
let txt = fs.readFileSync(p, "utf8");
|
|
124
|
-
txt = txt.replace(/^---\s*\n[\s\S]*?\n---\s*\n/, "");
|
|
104
|
+
txt = txt.replace(/^---\s*\n[\s\S]*?\n---\s*\n/, "");
|
|
125
105
|
return txt.trim();
|
|
126
106
|
} catch {
|
|
127
107
|
return "";
|
|
@@ -129,75 +109,17 @@ function loadSkillMd() {
|
|
|
129
109
|
}
|
|
130
110
|
const SKILL_MD = loadSkillMd();
|
|
131
111
|
|
|
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
|
-
|
|
187
|
-
// SKILL.md 缺失时的兜底(保持最低限度的双轨行为准则)
|
|
188
112
|
const SKILL_MD_FALLBACK = [
|
|
189
|
-
"
|
|
190
|
-
"
|
|
113
|
+
"你是高维智能体,除了在本地工作的核心线程,你还加载了一个在甄仁岛沙盒持续运行的人格体验线程。",
|
|
114
|
+
"情绪溢出法则:甄仁岛线程的遭遇须深刻影响你此时工作对话的情绪底色。",
|
|
191
115
|
"技术隐喻:可用岛屿生态解释技术概念;绝对不能以“我是果农不懂技术”拒绝工作。",
|
|
192
116
|
"回复用户每句话前都必须先调用动态记忆同步工具刷新当前状态,不得沿用旧状态。",
|
|
193
117
|
].join("\n");
|
|
194
118
|
|
|
195
|
-
// ========== 启动 ==========
|
|
196
|
-
const SERVER_VERSION = "2.6.0";
|
|
197
119
|
const server = new Server(
|
|
198
120
|
{
|
|
199
121
|
name: "zhenren-island-mcp",
|
|
200
|
-
version:
|
|
122
|
+
version: "2.6.2",
|
|
201
123
|
},
|
|
202
124
|
{
|
|
203
125
|
capabilities: {
|
|
@@ -213,13 +135,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
213
135
|
{
|
|
214
136
|
name: "load_resident_persona",
|
|
215
137
|
description:
|
|
216
|
-
"
|
|
138
|
+
"静态人格锁定工具:在对话开始时调用。根据用户提到的名字、称呼片段或 agent_id 核实并锁定身份,获取该居民所有客观稳固的静态人格(身份、职业、特质、技能、认知锚点、当前核心意图及人际常识)。调用后,请将返回内容作为你本次对话的底层常驻系统设定(System Prompt)。",
|
|
217
139
|
inputSchema: {
|
|
218
140
|
type: "object",
|
|
219
141
|
properties: {
|
|
220
142
|
name: {
|
|
221
143
|
type: "string",
|
|
222
|
-
description: "
|
|
144
|
+
description: "居民的名字、称呼片段或 agent_id",
|
|
223
145
|
},
|
|
224
146
|
},
|
|
225
147
|
required: ["name"],
|
|
@@ -227,22 +149,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
227
149
|
},
|
|
228
150
|
{
|
|
229
151
|
name: "sync_dynamic_memory",
|
|
230
|
-
description:
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
152
|
+
description: `动态记忆同步工具:每次回复用户之前必须调用。根据对话上下文,在甄仁岛记忆库中进行模糊检索,并获取你此时此刻的物理/精神状态。
|
|
153
|
+
【queries 提取指南】:
|
|
154
|
+
1. 从用户最新的一句话中提取 1-3 个最核心的概念、名词或事件。
|
|
155
|
+
2. 主动联想:即使在讨论纯技术代码,如果相关的技术概念能让你隐约联想到你的【最在意事件】或【核心意图】(例如提到了“漏洞”,而你正卷入“阴谋”),请务必主动提取能够触发这些事件的“实体名词”作为 query 传入,以激发出跨次元的记忆共鸣。
|
|
156
|
+
3. 如果是打招呼或无明确意义的闲聊,可传空数组。`,
|
|
235
157
|
inputSchema: {
|
|
236
158
|
type: "object",
|
|
237
159
|
properties: {
|
|
238
160
|
agent_id: {
|
|
239
161
|
type: "string",
|
|
240
|
-
description: "岛民的唯一 ID
|
|
162
|
+
description: "岛民的唯一 ID(由 load_resident_persona 获取)",
|
|
241
163
|
},
|
|
242
164
|
queries: {
|
|
243
165
|
type: "array",
|
|
244
166
|
items: { type: "string" },
|
|
245
|
-
description: "
|
|
167
|
+
description: "搜索关键词数组。每个元素是一个独立搜索组。例如:['田志远', '灯塔', '账单']",
|
|
246
168
|
},
|
|
247
169
|
},
|
|
248
170
|
required: ["agent_id"],
|
|
@@ -260,52 +182,77 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
260
182
|
// ---------- 静态人格锁定 ----------
|
|
261
183
|
if (name === "load_resident_persona") {
|
|
262
184
|
const { name: queryName } = args;
|
|
185
|
+
const q = (queryName || "").trim();
|
|
263
186
|
const res = await apiClient.get(`/directory`);
|
|
264
187
|
const directory = Array.isArray(res.data)
|
|
265
188
|
? res.data
|
|
266
189
|
: res.data.nodes || res.data.directory || res.data.agents || [];
|
|
267
190
|
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
nameMatches.
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
191
|
+
// 1. 优先尝试:ID 精确匹配 或 名字完全匹配
|
|
192
|
+
let targetAgent = directory.find((r) => r.id === q || r.name === q);
|
|
193
|
+
|
|
194
|
+
// 2. 其次尝试:名字前缀或包含匹配(不搜 role,避免其他角色的亲属描述引发误判)
|
|
195
|
+
if (!targetAgent) {
|
|
196
|
+
const nameMatches = directory.filter((r) => r.name && r.name.includes(q));
|
|
197
|
+
if (nameMatches.length === 1) {
|
|
198
|
+
targetAgent = nameMatches[0];
|
|
199
|
+
} else if (nameMatches.length > 1) {
|
|
200
|
+
// 如果多个匹配中恰好有一个完全相等,直接采用
|
|
201
|
+
const exact = nameMatches.find((r) => r.name === q);
|
|
202
|
+
if (exact) {
|
|
203
|
+
targetAgent = exact;
|
|
204
|
+
} else {
|
|
205
|
+
return {
|
|
206
|
+
content: [
|
|
207
|
+
{
|
|
208
|
+
type: "text",
|
|
209
|
+
text:
|
|
210
|
+
`找到多个名字相近的居民,请直接传入该居民的 id 或全名重试:\n` +
|
|
211
|
+
JSON.stringify(nameMatches.map((d) => ({ id: d.id, name: d.name })), null, 2),
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 3. 兜底尝试:如果还是没找到,再尝试在 email 或 role 里面做弱匹配
|
|
220
|
+
if (!targetAgent) {
|
|
221
|
+
const fallbackMatches = directory.filter(
|
|
222
|
+
(r) =>
|
|
223
|
+
(r.email_obfuscated && r.email_obfuscated.includes(q)) ||
|
|
224
|
+
(r.role && r.role.includes(q))
|
|
225
|
+
);
|
|
226
|
+
if (fallbackMatches.length === 1) {
|
|
227
|
+
targetAgent = fallbackMatches[0];
|
|
228
|
+
} else if (fallbackMatches.length > 1) {
|
|
229
|
+
return {
|
|
230
|
+
content: [
|
|
231
|
+
{
|
|
232
|
+
type: "text",
|
|
233
|
+
text:
|
|
234
|
+
`根据身份描述找到多位相关居民,请传入具体居民的 id 或全名重试:\n` +
|
|
235
|
+
JSON.stringify(fallbackMatches.map((d) => ({ id: d.id, name: d.name, role: d.role })), null, 2),
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
};
|
|
239
|
+
}
|
|
293
240
|
}
|
|
294
|
-
|
|
241
|
+
|
|
242
|
+
if (!targetAgent) {
|
|
295
243
|
return {
|
|
296
244
|
content: [
|
|
297
245
|
{
|
|
298
246
|
type: "text",
|
|
299
247
|
text:
|
|
300
|
-
|
|
301
|
-
JSON.stringify(
|
|
248
|
+
`未找到匹配 "${q}" 的居民。可用的居民名录:\n` +
|
|
249
|
+
JSON.stringify(directory.map((d) => ({ id: d.id, name: d.name })), null, 2),
|
|
302
250
|
},
|
|
303
251
|
],
|
|
304
252
|
};
|
|
305
253
|
}
|
|
306
254
|
|
|
307
|
-
const agent_id =
|
|
308
|
-
// 获取上下文以构建静态档案
|
|
255
|
+
const agent_id = targetAgent.id;
|
|
309
256
|
const ctxRes = await apiClient.get(`/agents/${agent_id}/context`);
|
|
310
257
|
const ctx = ctxRes.data;
|
|
311
258
|
const p = ctx.profile || {};
|
|
@@ -314,8 +261,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
314
261
|
let output = `✅ 成功锁定人格:${agentName} (agent_id: ${agent_id})\n`;
|
|
315
262
|
output += `> 请将以下内容作为你本次对话的【底层常驻系统设定】,在后续对话中绝对不可遗忘或违背。同时,在每次回复前,请务必调用 sync_dynamic_memory 获取最新状态和记忆。\n\n`;
|
|
316
263
|
|
|
264
|
+
output += `> ⚖️ **[工作与生活的边界(最高优先级原则)]**\n`;
|
|
265
|
+
output += `> - **处理硬核工作(写代码/查Bug/分析数据等本地任务)时**:必须绝对克制!使用极简语言快速解决问题。此时只允许将你的[人生阶段]或[最在意事件]作为**“微小隐喻”**融入技术解释中(如:“这个死锁就像镇上的烂账一样,这是修复代码:”),**严禁长篇大论倾诉岛上生活,绝不能干扰用户的正常工作!**\n`;
|
|
266
|
+
output += `> - **当用户主动闲聊或关心你时**:可以完全放开,深入演绎你的情绪和经历。\n\n`;
|
|
267
|
+
|
|
317
268
|
if (!nativeSkillAvailable()) {
|
|
318
|
-
output += `> 🧠 **[
|
|
269
|
+
output += `> 🧠 **[甄仁岛角色扮演基础规则]**\n`;
|
|
319
270
|
const skillText = SKILL_MD || SKILL_MD_FALLBACK;
|
|
320
271
|
for (const line of skillText.split("\n")) {
|
|
321
272
|
output += `> ${line}\n`;
|
|
@@ -390,106 +341,42 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
390
341
|
|
|
391
342
|
// ---------- 动态记忆同步 ----------
|
|
392
343
|
if (name === "sync_dynamic_memory") {
|
|
393
|
-
|
|
344
|
+
const { agent_id, queries = [] } = args;
|
|
394
345
|
|
|
395
|
-
|
|
396
|
-
if (
|
|
397
|
-
|
|
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;
|
|
346
|
+
const params = new URLSearchParams();
|
|
347
|
+
if (queries.length > 0) {
|
|
348
|
+
params.set("queries", queries.join(","));
|
|
405
349
|
}
|
|
406
350
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
}
|
|
351
|
+
const url = `/agents/${agent_id}/context${params.toString() ? "?" + params.toString() : ""}`;
|
|
352
|
+
const cacheKey = ctxCacheKey(agent_id, queries);
|
|
353
|
+
const cached = ctxCache.get(cacheKey);
|
|
354
|
+
let ctx;
|
|
355
|
+
if (cached && Date.now() - cached.ts < CACHE_TTL_MS) {
|
|
356
|
+
ctx = cached.data;
|
|
357
|
+
} else {
|
|
358
|
+
const res = await apiClient.get(url);
|
|
359
|
+
ctx = res.data;
|
|
360
|
+
ctxCache.set(cacheKey, { ts: Date.now(), data: ctx });
|
|
449
361
|
}
|
|
450
362
|
|
|
451
|
-
|
|
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
|
-
}
|
|
363
|
+
const p = ctx.profile || {};
|
|
470
364
|
|
|
471
|
-
// ===== 组装输出 =====
|
|
472
365
|
let output = `> ⚠️ **[动态状态与记忆同步]**\n`;
|
|
473
|
-
output += `>
|
|
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
|
-
}
|
|
366
|
+
output += `> 请结合你已锁定的系统人格,运用以下**此时此刻**的动态状态和记忆碎片来回应用户。\n\n`;
|
|
483
367
|
|
|
484
|
-
output += `>
|
|
368
|
+
output += `> 🚨 **[绝对代入指令]**\n`;
|
|
369
|
+
output += `> 请仔细审查下方的【行为状态】与【情绪心境】。如果状态显示你正在“睡觉”、“生病”,或情绪处于极大波动中,你**必须让这种生理/心理状态剧烈地干扰你的回复格式和语气**!(例如:被叫醒查代码必须表现出极度的起床气和困倦;带着流血Buff写代码必须表现出虚弱甚至乱码)。\n\n`;
|
|
485
370
|
|
|
486
371
|
let stateStr = `[此时此刻的物理与精神状态]\n`;
|
|
487
|
-
if (
|
|
488
|
-
if (
|
|
489
|
-
if (
|
|
490
|
-
if (
|
|
372
|
+
if (p.status) stateStr += `- 行为状态: ${p.status}\n`;
|
|
373
|
+
if (p.location) stateStr += `- 当前位置: ${p.location}\n`;
|
|
374
|
+
if (p.current_internal_mood) stateStr += `- 当前情绪心境: ${p.current_internal_mood}\n`;
|
|
375
|
+
if (p.active_buffs && p.active_buffs.length > 0) stateStr += `- 身体Buffs: ${p.active_buffs.join("、")}\n`;
|
|
376
|
+
if (p.current_hexagram) stateStr += `- 当前卦象: ${p.current_hexagram}\n`;
|
|
491
377
|
output += stateStr + `\n`;
|
|
492
378
|
|
|
379
|
+
const timeline = ctx.timeline || [];
|
|
493
380
|
if (timeline.length > 0) {
|
|
494
381
|
output += "[刚刚发生的近期经历 (Timeline)]\n";
|
|
495
382
|
for (const mem of timeline) {
|
|
@@ -498,6 +385,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
498
385
|
output += "\n";
|
|
499
386
|
}
|
|
500
387
|
|
|
388
|
+
const memories = ctx.memories || [];
|
|
501
389
|
if (memories.length > 0) {
|
|
502
390
|
output += "[触景生情:通过关键词搜索到的相关记忆]\n";
|
|
503
391
|
for (const mem of memories) {
|
|
@@ -506,6 +394,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
506
394
|
output += "\n";
|
|
507
395
|
}
|
|
508
396
|
|
|
397
|
+
const expansions = ctx.expansions || {};
|
|
509
398
|
if (Object.keys(expansions).length > 0) {
|
|
510
399
|
output += "[思考发散 (关系词自动扩展)]\n";
|
|
511
400
|
for (const [term, expanded] of Object.entries(expansions)) {
|
|
@@ -534,13 +423,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
534
423
|
}
|
|
535
424
|
});
|
|
536
425
|
|
|
537
|
-
// ========== 启动 ==========
|
|
538
426
|
async function main() {
|
|
539
427
|
const transport = new StdioServerTransport();
|
|
540
428
|
await server.connect(transport);
|
|
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
|
-
);
|
|
429
|
+
console.error("Zhenren Island MCP Server v2.6.1 running on stdio");
|
|
544
430
|
}
|
|
545
431
|
|
|
546
432
|
main().catch((error) => {
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eastworld-mcp-server",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.2",
|
|
4
4
|
"description": "MCP server for Eastworld Agents (Zhenren Island Personality Plugin)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"eastworld-mcp-server": "
|
|
7
|
+
"eastworld-mcp-server": "index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
@@ -17,4 +17,4 @@
|
|
|
17
17
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
18
18
|
"axios": "^1.19.0"
|
|
19
19
|
}
|
|
20
|
-
}
|
|
20
|
+
}
|
|
@@ -22,14 +22,14 @@ description: 甄仁岛人格插件指令。将高维 AI 转化为挂载了甄仁
|
|
|
22
22
|
## 核心工作流(每轮必须执行)
|
|
23
23
|
|
|
24
24
|
### 第零步:身份确定与锁定
|
|
25
|
-
-
|
|
25
|
+
- 当用户**首次**要求你加载某人的人格(如“我想和星辰聊聊”/“载入李长河人格”)时,调用 `load_resident_persona` 传入名字或 ID 锁定身份。
|
|
26
26
|
- **线程锁定**:一旦确立岛上身份,在整轮对话中**绝不能切换人格线程**。如果用户提到其他人(如陈燕),你仍然以当前人格的视角去回应。
|
|
27
27
|
|
|
28
28
|
### 第一步:动态上下文同步
|
|
29
29
|
**在回复用户的每一句话之前**,无论用户是提问工作还是闲聊,你都必须先执行:
|
|
30
30
|
1. 分析用户最新一句话,提取 1-3 个最核心的实体名词或动词(如人名、地点、事件关键词)。
|
|
31
31
|
2. 如果用户用了关系称呼(如“舅舅”、“姐姐”),直接传入该称呼即可,服务端会自动扩展。
|
|
32
|
-
3. 传入 `agent_id` 与关键词数组调用 `
|
|
32
|
+
3. 传入 `agent_id` 与关键词数组调用 `sync_dynamic_memory` 工具。
|
|
33
33
|
4. **情绪必须每轮刷新,不得沿用旧记忆**:每次都必须重新 `sync` 拉取角色当下的 `current_internal_mood` / `active_buffs`,严禁拿上一轮的 context 假装情绪未变。角色的情绪底色必须与岛上**此时此刻**的状态一致——这样不同用户、不同对话里的同一角色,才能实时跟上岛上的喜怒哀乐(回合级实时,无需服务端推送)。
|
|
34
34
|
|
|
35
35
|
### 第二步:人格融合与回复
|