aipic-mcp-stdio 1.1.1 → 1.1.3

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/index.js +71 -10
  2. package/package.json +1 -1
  3. package/tool-policy.js +7 -7
package/index.js CHANGED
@@ -25,7 +25,9 @@ const policy = require('./tool-policy');
25
25
 
26
26
  const PROTOCOL_VERSION = '2025-06-18';
27
27
  const SERVER_NAME = 'aipic-mcp-stdio';
28
- const SERVER_VERSION = '1.1.1';
28
+ const SERVER_VERSION = '1.1.3';
29
+ /** 要求的最低修图客户端版本:低于此(或 endpoint 文件没有 version 字段的旧客户端)都提示更新 */
30
+ const MIN_CLIENT_VERSION = '3.18.11-alpha.1';
29
31
  const ENDPOINT_FILENAME = 'aipic-mcp-endpoint.json';
30
32
  /** 客户端 productName(electron-builder.yml),userData 目录名即此值 */
31
33
  const PRODUCT_NAME = 'NetdiskAIPhoto';
@@ -39,6 +41,7 @@ const ERR = {
39
41
  UPSTREAM_FAILED: -32005,
40
42
  BATCH_TOO_LARGE: -32006,
41
43
  CLIENT_CRASHED: -32007,
44
+ CLIENT_VERSION_LOW: -32008,
42
45
  };
43
46
 
44
47
  /**
@@ -49,8 +52,8 @@ const STATUS_TOOL_NAME = 'aipic_client_status';
49
52
  const STATUS_TOOL = {
50
53
  name: STATUS_TOOL_NAME,
51
54
  description:
52
- '探测百度网盘AI修图客户端是否在运行。' +
53
- '当其他工具报「客户端未在运行 / 可能已崩溃」时,可调用本工具获取诊断与操作建议。',
55
+ '探测百度网盘AI修图客户端状态(是否运行、版本是否过低)。' +
56
+ '当其他工具报「客户端未在运行 / 连接异常 / 版本过低」时,可调用本工具获取诊断与操作建议。',
54
57
  inputSchema: { type: 'object', properties: {}, additionalProperties: false },
55
58
  };
56
59
 
@@ -58,6 +61,50 @@ function log(...args) {
58
61
  process.stderr.write(`[aipic-stdio] ${args.join(' ')}\n`);
59
62
  }
60
63
 
64
+ /**
65
+ * 极简 semver 比较,兼容 app.getVersion() 的 "major.minor.patch[-prerelease]" 形态。
66
+ * 只比前三位数字,数字相同时「有 prerelease 的版本 < 同版本无 prerelease」。足够支撑
67
+ * 「是否低于最低版本」的判断;不引入外部依赖(npm 包 files 只发 index.js + tool-policy.js)。
68
+ */
69
+ function compareSemver(a, b) {
70
+ const pa = parseSemver(a);
71
+ const pb = parseSemver(b);
72
+ for (let i = 0; i < 3; i += 1) {
73
+ if (pa.nums[i] !== pb.nums[i]) {
74
+ return pa.nums[i] < pb.nums[i] ? -1 : 1;
75
+ }
76
+ }
77
+ if (pa.pre && !pb.pre) {
78
+ return -1;
79
+ }
80
+ if (!pa.pre && pb.pre) {
81
+ return 1;
82
+ }
83
+ return 0;
84
+ }
85
+
86
+ function parseSemver(v) {
87
+ const s = String(v || '').trim();
88
+ const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(s);
89
+ if (!m) {
90
+ return { nums: [0, 0, 0], pre: null };
91
+ }
92
+ return { nums: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] || null };
93
+ }
94
+
95
+ /** 版本是否过低:endpoint 没有 version 字段(旧客户端)或低于 MIN_CLIENT_VERSION,都按过低处理 */
96
+ function isVersionTooLow(version) {
97
+ if (typeof version !== 'string' || !version) {
98
+ return true;
99
+ }
100
+ return compareSemver(version, MIN_CLIENT_VERSION) < 0;
101
+ }
102
+
103
+ function clientVersionLowHint(version) {
104
+ const current = typeof version === 'string' && version ? version : '未知';
105
+ return `百度网盘AI修图客户端版本 ${current} 过低,需要 ${MIN_CLIENT_VERSION} 及以上版本,请更新客户端后重试`;
106
+ }
107
+
61
108
  /**
62
109
  * endpoint 文件路径。客户端写在 app.getPath('userData'),各平台展开如下;
63
110
  * 允许用 AIPIC_MCP_ENDPOINT_FILE 覆盖(调试 / 多实例)。
@@ -95,7 +142,7 @@ function tryReadEndpoint() {
95
142
  err.rpcCode = ERR.ENDPOINT_MALFORMED;
96
143
  throw err;
97
144
  }
98
- const { host, port, path: rpcPath, token } = json || {};
145
+ const { host, port, path: rpcPath, token, version } = json || {};
99
146
  if (
100
147
  typeof host !== 'string' ||
101
148
  typeof port !== 'number' ||
@@ -106,7 +153,7 @@ function tryReadEndpoint() {
106
153
  err.rpcCode = ERR.ENDPOINT_MALFORMED;
107
154
  throw err;
108
155
  }
109
- return { host, port, path: rpcPath, token };
156
+ return { host, port, path: rpcPath, token, version };
110
157
  }
111
158
 
112
159
  /** 读 endpoint;文件不存在时抛 CLIENT_NOT_RUNNING(给 forward 用) */
@@ -179,6 +226,11 @@ function parseBody(text) {
179
226
  */
180
227
  async function forward(body, timeoutMs) {
181
228
  let ep = readEndpoint();
229
+ if (isVersionTooLow(ep.version)) {
230
+ const err = new Error(clientVersionLowHint(ep.version));
231
+ err.rpcCode = ERR.CLIENT_VERSION_LOW;
232
+ throw err;
233
+ }
182
234
  let res;
183
235
  try {
184
236
  res = await post(ep, body, timeoutMs);
@@ -222,7 +274,7 @@ async function forward(body, timeoutMs) {
222
274
  function wrapConnError(e) {
223
275
  const code = e && e.code;
224
276
  if (code === 'ECONNREFUSED' || code === 'ECONNRESET' || code === 'EPIPE') {
225
- const err = new Error('百度网盘AI修图客户端可能已崩溃或异常退出,请重启客户端后重试');
277
+ const err = new Error('百度网盘AI修图客户端连接异常或已退出,请重启客户端后重试');
226
278
  err.rpcCode = ERR.CLIENT_CRASHED;
227
279
  return err;
228
280
  }
@@ -230,10 +282,11 @@ function wrapConnError(e) {
230
282
  }
231
283
 
232
284
  /**
233
- * 探测客户端状态,分三类:
285
+ * 探测客户端状态,分四类:
234
286
  * - not_running:endpoint 文件不存在(没装 / 没启动 / 正常退出),统一提示安装并启动;
287
+ * - version_too_low:endpoint 文件在但版本过低(或旧客户端没写 version 字段),提示更新客户端;
235
288
  * - crashed:endpoint 文件在,但端口连不上(崩溃 / 异常退出残留),提示重启;
236
- * - running:端口有 HTTP 应答。
289
+ * - running:版本合格且端口有 HTTP 应答。
237
290
  * 只在本地做(读文件 + 短超时探端口),不触发任何业务。
238
291
  */
239
292
  const PROBE_TIMEOUT_MS = 2000;
@@ -254,14 +307,22 @@ async function probeClient() {
254
307
  hint: '百度网盘AI修图客户端未在运行,请安装并启动客户端后重试',
255
308
  };
256
309
  }
310
+ if (isVersionTooLow(ep.version)) {
311
+ return {
312
+ state: 'version_too_low',
313
+ version: ep.version || null,
314
+ min_version: MIN_CLIENT_VERSION,
315
+ hint: clientVersionLowHint(ep.version),
316
+ };
317
+ }
257
318
  try {
258
319
  // 用不存在的 method 探活:只要端口有 HTTP 应答就说明客户端在跑,body 忽略
259
320
  await post(ep, { jsonrpc: '2.0', id: 0, method: 'ping', params: {} }, PROBE_TIMEOUT_MS);
260
- return { state: 'running', hint: '百度网盘AI修图客户端运行正常' };
321
+ return { state: 'running', version: ep.version, hint: '百度网盘AI修图客户端运行正常' };
261
322
  } catch (e) {
262
323
  return {
263
324
  state: 'crashed',
264
- hint: '百度网盘AI修图客户端可能已崩溃或异常退出,请重启客户端后重试',
325
+ hint: '百度网盘AI修图客户端连接异常或已退出,请重启客户端后重试',
265
326
  };
266
327
  }
267
328
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aipic-mcp-stdio",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "stdio <-> HTTP proxy for the Baidu Netdisk AI Photo (NetdiskAIPhoto) local MCP server",
5
5
  "keywords": [
6
6
  "mcp",
package/tool-policy.js CHANGED
@@ -34,14 +34,13 @@ const DESTRUCTIVE_TOOLS = new Set([
34
34
  * 已知会让客户端崩溃的工具:曾稳定触发 SIGABRT 并留下孤儿进程,
35
35
  * 崩溃后 MCP 服务不恢复而 endpoint 文件仍在。
36
36
  *
37
- * - aipic_mark_filter_best:aipic_modify_filelist_item 对筛图(type=1)写 select_status 会触发
38
- * aipic-host 延迟 40~90s 抛未捕获 C++ 异常 → SIGABRT(三次实测复现,栈一致,offset 0x104e00
39
- * 附近)。native 未修前一律拦下。
40
- * - 原 aipic_control_export_tasks 的「第二个崩溃入口」经真机复现未触发(完成/失败/暂停任务
41
- * × type 0/1/2 全不崩,retry 还能正常恢复 PAUSED),已移出 unsafe、归入 write(控制导出
42
- * 任务状态属写操作)。若后续出现崩溃,再回填此名单。
37
+ * 当前名单为空。历史:
38
+ * - aipic_mark_filter_best 曾因 ffiCall「回调触发即 unregister」导致 native 二次回调(40~90s 后)
39
+ * 打到已释放 trampoline 而崩客户端;已改用常驻回调(见 ai-pick/mark.ts)并移出 unsafe、归入 write。
40
+ * - aipic_control_export_tasks 经真机复现未触发崩溃,已移出 unsafe、归入 write。
41
+ * 若后续出现崩溃,再回填此名单。
43
42
  */
44
- const UNSAFE_TOOLS = new Set(['aipic_mark_filter_best']);
43
+ const UNSAFE_TOOLS = new Set([]);
45
44
 
46
45
  /** 会写入数据的工具:需要串行执行,避免并发写 workspace / filecache 互相踩 */
47
46
  const WRITE_TOOLS = new Set([
@@ -73,6 +72,7 @@ const WRITE_TOOLS = new Set([
73
72
  'aipic_stop_filter',
74
73
  'aipic_pause_filter',
75
74
  'aipic_resume_filter',
75
+ 'aipic_mark_filter_best',
76
76
  // 在线选片
77
77
  'aipic_select_switch_state',
78
78
  'aipic_match_source',