writedock-mcp 0.4.12 → 0.4.14

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.
@@ -4,7 +4,7 @@
4
4
  "name": "写作台 WriteDock",
5
5
  "short_name": "写作台",
6
6
  "description": "连接自己的 AI agent,通过 MCP 发布小红书长文、图文和视频,微博图文、视频及长文章,知乎、百家号、B站专栏文章与抖音图文视频。",
7
- "version": "0.4.12",
7
+ "version": "0.4.14",
8
8
  "minimum_chrome_version": "116",
9
9
  "icons": {
10
10
  "16": "assets/icon-16.png",
@@ -1 +1 @@
1
- import './assets/index.ts-CSY6FJ6h.js';
1
+ import './assets/index.ts-CfQ_yCaZ.js';
@@ -1,4 +1,4 @@
1
- WriteDock 安装与更新 v0.4.12
1
+ WriteDock 安装与更新 v0.4.14
2
2
 
3
3
  首次使用
4
4
  1. 把这句话发给你的 agent:帮我安装 WriteDock:先检查电脑上有没有 Node.js 22 或更高版本,没有就帮我安装,需要我操作时告诉我;然后运行 npx -y writedock-mcp@latest setup,最后告诉我怎么重启你。
package/index.cjs CHANGED
@@ -29740,7 +29740,7 @@ var MediaStore = class {
29740
29740
  // package.json
29741
29741
  var package_default = {
29742
29742
  name: "@writedock/mcp-server",
29743
- version: "0.4.12",
29743
+ version: "0.4.14",
29744
29744
  private: true,
29745
29745
  type: "module",
29746
29746
  description: "WriteDock local MCP server for the browser extension",
@@ -29924,6 +29924,14 @@ function mcpReleaseInfo() {
29924
29924
  update_checking: !!pending
29925
29925
  };
29926
29926
  }
29927
+ function updateNotice(input) {
29928
+ const notices = [];
29929
+ if (input.bundle && (!input.extension || newerVersion(input.bundle, input.extension))) {
29930
+ notices.push(`WriteDock \u6269\u5C55\u6709\u65B0\u7248\u672C v${input.bundle}${input.extension ? `\uFF08\u5F53\u524D v${input.extension}\uFF09` : ""}\uFF0C\u9700\u8981\u7528\u6237\u81EA\u5DF1\u70B9\u4E00\u4E0B\u624D\u4F1A\u66F4\u65B0\uFF1A\u70B9\u6D4F\u89C8\u5668\u53F3\u4E0A\u89D2\u7684\u62FC\u56FE\u56FE\u6807\uFF0C\u70B9 WriteDock\uFF0C\u8FDB\u5165\u201C\u8FDE\u63A5 agent\u201D\uFF0C\u70B9\u201C\u66F4\u65B0\u6269\u5C55\u201D\u3002\u8FDE\u63A5\u548C\u7A3F\u4EF6\u90FD\u4F1A\u4FDD\u7559\u3002`);
29931
+ }
29932
+ if (input.mcpLatest && input.mcp && newerVersion(input.mcpLatest, input.mcp)) notices.push(`WriteDock \u670D\u52A1\u6709\u65B0\u7248\u672C v${input.mcpLatest}\uFF1A\u91CD\u542F agent \u5C31\u4F1A\u66F4\u65B0\u3002`);
29933
+ return notices.length ? `${notices.join("")}\u8BF7\u5728\u6C47\u62A5\u7ED3\u679C\u65F6\u628A\u8FD9\u4E9B\u6B65\u9AA4\u539F\u6837\u544A\u8BC9\u7528\u6237\u4E00\u6B21\u3002\u8FD9\u4E0D\u5F71\u54CD\u672C\u6B21\u7ED3\u679C\uFF0C\u4E0D\u8981\u7B49\u7528\u6237\u66F4\u65B0\uFF0C\u4E5F\u4E0D\u8981\u56E0\u6B64\u91CD\u53D1\u3002` : void 0;
29934
+ }
29927
29935
 
29928
29936
  // src/bridge.ts
29929
29937
  var LIMIT = 32 * 1024 * 1024;
@@ -29953,6 +29961,7 @@ var LocalBridge = class {
29953
29961
  starting;
29954
29962
  primary = false;
29955
29963
  extensionId;
29964
+ extensionVersion;
29956
29965
  seenNonces = /* @__PURE__ */ new Map();
29957
29966
  extensionToken;
29958
29967
  lastWork = 0;
@@ -30013,6 +30022,16 @@ var LocalBridge = class {
30013
30022
  get isPrimary() {
30014
30023
  return this.primary;
30015
30024
  }
30025
+ notice() {
30026
+ if (this.client?.readyState !== import_websocket.default.OPEN) return void 0;
30027
+ const release = mcpReleaseInfo();
30028
+ return updateNotice({ bundle: release.extension_bundle_version, extension: this.extensionVersion, mcpLatest: release.latest_version, mcp: release.mcp_version });
30029
+ }
30030
+ connectionFields() {
30031
+ const connected = this.client?.readyState === import_websocket.default.OPEN, notice = this.notice();
30032
+ const { extension_bundle_version: _bundled, ...release } = mcpReleaseInfo();
30033
+ return { connected, extensionId: this.extensionId, ...this.extensionVersion ? { extension_version: this.extensionVersion } : {}, ...notice ? { update_notice: notice } : {}, protocol: "writedock/1", ...release };
30034
+ }
30016
30035
  /** How long nothing has needed this service. An open extension connection counts as needed. */
30017
30036
  idleFor() {
30018
30037
  if (this.client?.readyState === import_websocket.default.OPEN) this.lastSeen = Date.now();
@@ -30037,6 +30056,7 @@ var LocalBridge = class {
30037
30056
  const previousClient = this.client;
30038
30057
  this.client = void 0;
30039
30058
  this.extensionId = void 0;
30059
+ this.extensionVersion = void 0;
30040
30060
  for (const value of this.pending.values()) {
30041
30061
  clearTimeout(value.timer);
30042
30062
  value.reject(new Error(reason));
@@ -30077,6 +30097,7 @@ var LocalBridge = class {
30077
30097
  clearTimeout(timer);
30078
30098
  this.client = ws;
30079
30099
  this.extensionId = String(message.extensionId || "");
30100
+ this.extensionVersion = typeof message.extensionVersion === "string" && message.extensionVersion.length <= 40 ? message.extensionVersion : void 0;
30080
30101
  ws.send(JSON.stringify({ type: "ready", ...mcpReleaseInfo() }));
30081
30102
  return;
30082
30103
  }
@@ -30103,6 +30124,7 @@ var LocalBridge = class {
30103
30124
  if (this.client !== ws) return;
30104
30125
  this.client = void 0;
30105
30126
  this.extensionId = void 0;
30127
+ this.extensionVersion = void 0;
30106
30128
  for (const value of this.pending.values()) {
30107
30129
  clearTimeout(value.timer);
30108
30130
  value.reject(new Error("\u6269\u5C55\u8FDE\u63A5\u5DF2\u65AD\u5F00\u3002\u82E5\u5DF2\u63D0\u4EA4\u540C\u6B65\uFF0C\u8BF7\u6309\u4EFB\u52A1 ID \u67E5\u8BE2\u7ED3\u679C\uFF0C\u4E0D\u8981\u66F4\u6362 request_id \u81EA\u52A8\u91CD\u53D1\u3002"));
@@ -30167,7 +30189,7 @@ var LocalBridge = class {
30167
30189
  }
30168
30190
  this.lastSeen = Date.now();
30169
30191
  if (req.method === "GET" && req.url === "/status") {
30170
- send(200, { connected: this.client?.readyState === import_websocket.default.OPEN, extensionId: this.extensionId, protocol: "writedock/1", ...mcpReleaseInfo() });
30192
+ send(200, this.connectionFields());
30171
30193
  return;
30172
30194
  }
30173
30195
  this.lastWork = Date.now();
@@ -30207,19 +30229,35 @@ var LocalBridge = class {
30207
30229
  }
30208
30230
  async status() {
30209
30231
  await this.start();
30210
- if (this.primary) return { connected: this.client?.readyState === import_websocket.default.OPEN, extensionId: this.extensionId, protocol: "writedock/1", ...mcpReleaseInfo() };
30232
+ if (this.primary) return this.connectionFields();
30211
30233
  try {
30212
30234
  return await this.httpCall("/status");
30213
30235
  } catch (error2) {
30214
30236
  if (!(error2 instanceof TypeError)) throw error2;
30215
30237
  this.starting = void 0;
30216
30238
  await this.start();
30217
- if (this.primary) return { connected: false, protocol: "writedock/1", ...mcpReleaseInfo() };
30239
+ if (this.primary) return this.connectionFields();
30218
30240
  return this.httpCall("/status");
30219
30241
  }
30220
30242
  }
30243
+ /**
30244
+ * The service may have only just started, for example on the first publication after a reboot, and the extension
30245
+ * then needs a moment to connect. Nothing has been sent yet, so waiting here cannot submit anything twice.
30246
+ */
30247
+ async waitForExtension() {
30248
+ const end = Date.now() + (this.options.extensionWaitMs ?? 45e3);
30249
+ while (Date.now() < end) {
30250
+ try {
30251
+ if (this.primary ? this.client?.readyState === import_websocket.default.OPEN : (await this.httpCall("/status")).connected === true) return;
30252
+ } catch {
30253
+ return;
30254
+ }
30255
+ await new Promise((resolve4) => setTimeout(resolve4, 500));
30256
+ }
30257
+ }
30221
30258
  async request(method, params = {}) {
30222
30259
  await this.start();
30260
+ await this.waitForExtension();
30223
30261
  if (this.primary) return this.requestConnected(method, params);
30224
30262
  try {
30225
30263
  return (await this.httpCall("/request", { method, params })).result;
@@ -30251,6 +30289,9 @@ var LocalBridge = class {
30251
30289
  this.pending.delete(id);
30252
30290
  reject(error2);
30253
30291
  });
30292
+ }).then((result) => {
30293
+ const notice = result && typeof result === "object" && result.finished === true ? this.notice() : void 0;
30294
+ return notice ? { ...result, update_notice: notice } : result;
30254
30295
  });
30255
30296
  }
30256
30297
  async close() {
@@ -50360,7 +50401,7 @@ var postSource = {
50360
50401
  weibo_video_creation: external_exports.enum(["original", "secondary", "repost"]).optional().describe("\u5FAE\u535A\u72EC\u7ACB\u89C6\u9891\u5FC5\u586B\uFF1Aoriginal \u539F\u521B\u3001secondary \u4E8C\u521B\u3001repost \u8F6C\u8F7D\u3002\u5FC5\u987B\u6839\u636E\u7528\u6237\u5B9E\u9645\u5185\u5BB9\u660E\u786E\u9009\u62E9\uFF0C\u4E0D\u5F97\u9ED8\u8BA4\u539F\u521B\u3002\u5176\u4ED6\u5E73\u53F0\u4E0D\u4F7F\u7528\u6B64\u5B57\u6BB5\uFF1B\u6765\u6E90\u3001\u5185\u5BB9\u58F0\u660E\u6216\u534F\u8BAE\u9700\u8865\u5145\u65F6\u8F6C\u4EBA\u5DE5\u5904\u7406"),
50361
50402
  base_directory: external_exports.string().optional().describe("\u4F7F\u7528\u76F8\u5BF9\u7D20\u6750\u8DEF\u5F84\u65F6\uFF0C\u660E\u786E\u6307\u5B9A\u7D20\u6750\u76EE\u5F55")
50362
50403
  };
50363
- var agentWorkflow = "Agent \u53EA\u901A\u8FC7 WriteDock MCP \u6295\u7A3F\u548C\u67E5\u8BE2\u56DE\u6267\uFF0C\u4E0D\u4F9D\u8D56\u4E5F\u4E0D\u8981\u8C03\u7528\u6D4F\u89C8\u5668\u3001Playwright \u6216\u5176\u4ED6\u7F51\u9875\u5DE5\u5177\u3002\u6309\u7ED3\u679C\u7684 next_action \u64CD\u4F5C\uFF1Apoll_task \u7528\u539F task_id/request_id \u67E5\u8BE2 get_sync_task\uFF1Breport_result \u76F4\u63A5\u5411\u7528\u6237\u6C47\u62A5\u9010\u5E73\u53F0\u72B6\u6001\u548C\u94FE\u63A5\u3002submitted/published \u5DF2\u6709\u6269\u5C55\u786E\u8BA4\uFF0C\u4E0D\u518D\u6253\u5F00\u7F51\u9875\u9A8C\u8BC1\uFF1B\u9A8C\u8BC1\u7801\u3001\u56FE\u5F62\u9A8C\u8BC1\u3001\u534F\u8BAE\u3001\u53D1\u5E03\u8BBE\u7F6E\u6216 unknown/interrupted \u90FD\u5C06\u539F\u56E0\u548C\u94FE\u63A5\u4EA4\u7ED9\u7528\u6237\u81EA\u884C\u70B9\u51FB\u5904\u7406\uFF0C\u4E0D\u81EA\u52A8\u91CD\u53D1\u3001\u4E0D\u6362 request_id\u3002\u4EBA\u5DE5\u63A5\u7BA1\u4E0D\u4EE3\u8868\u5DF2\u7ECF\u53D1\u5E03\u3002\u552F\u4E00\u4F8B\u5916\u662F user_actions \u7684 type=old_draft\uFF1A\u5E73\u53F0\u6709\u4E00\u4EFD\u7528\u6237\u4E0A\u6B21\u6CA1\u53D1\u5B8C\u7684\u5185\u5BB9\uFF0C\u8FD9\u7BC7\u5C1A\u672A\u63D0\u4EA4\u3002\u5FC5\u987B\u5148\u95EE\u7528\u6237\u662F\u5426\u653E\u5F03\u90A3\u4EFD\u65E7\u5185\u5BB9\uFF0C\u4E0D\u5F97\u66FF\u7528\u6237\u51B3\u5B9A\uFF1B\u7528\u6237\u660E\u786E\u540C\u610F\u540E\uFF0C\u624D\u7528\u65B0\u7684 request_id \u53EA\u5BF9\u8BE5\u5E73\u53F0\u518D\u8C03\u7528 publish\uFF0C\u5E76\u4F20 discard_old_drafts\uFF1B\u7528\u6237\u4E0D\u540C\u610F\u5C31\u4FDD\u7559\u65E7\u5185\u5BB9\uFF0C\u544A\u8BC9\u7528\u6237\u8FD9\u4E2A\u5E73\u53F0\u6CA1\u6709\u53D1\u3002";
50404
+ var agentWorkflow = "Agent \u53EA\u901A\u8FC7 WriteDock MCP \u6295\u7A3F\u548C\u67E5\u8BE2\u56DE\u6267\uFF0C\u4E0D\u4F9D\u8D56\u4E5F\u4E0D\u8981\u8C03\u7528\u6D4F\u89C8\u5668\u3001Playwright \u6216\u5176\u4ED6\u7F51\u9875\u5DE5\u5177\u3002\u6309\u7ED3\u679C\u7684 next_action \u64CD\u4F5C\uFF1Apoll_task \u7528\u539F task_id/request_id \u67E5\u8BE2 get_sync_task\uFF1Breport_result \u76F4\u63A5\u5411\u7528\u6237\u6C47\u62A5\u9010\u5E73\u53F0\u72B6\u6001\u548C\u94FE\u63A5\u3002submitted/published \u5DF2\u6709\u6269\u5C55\u786E\u8BA4\uFF0C\u4E0D\u518D\u6253\u5F00\u7F51\u9875\u9A8C\u8BC1\uFF1B\u9A8C\u8BC1\u7801\u3001\u56FE\u5F62\u9A8C\u8BC1\u3001\u534F\u8BAE\u3001\u53D1\u5E03\u8BBE\u7F6E\u6216 unknown/interrupted \u90FD\u5C06\u539F\u56E0\u548C\u94FE\u63A5\u4EA4\u7ED9\u7528\u6237\u81EA\u884C\u70B9\u51FB\u5904\u7406\uFF0C\u4E0D\u81EA\u52A8\u91CD\u53D1\u3001\u4E0D\u6362 request_id\u3002\u4EBA\u5DE5\u63A5\u7BA1\u4E0D\u4EE3\u8868\u5DF2\u7ECF\u53D1\u5E03\u3002\u7ED3\u679C\u91CC\u6709 update_notice \u65F6\uFF0C\u5728\u6C47\u62A5\u7ED3\u679C\u672B\u5C3E\u987A\u5E26\u544A\u8BC9\u7528\u6237\u4E00\u6B21\u3002\u552F\u4E00\u4F8B\u5916\u662F user_actions \u7684 type=old_draft\uFF1A\u5E73\u53F0\u6709\u4E00\u4EFD\u7528\u6237\u4E0A\u6B21\u6CA1\u53D1\u5B8C\u7684\u5185\u5BB9\uFF0C\u8FD9\u7BC7\u5C1A\u672A\u63D0\u4EA4\u3002\u5FC5\u987B\u5148\u95EE\u7528\u6237\u662F\u5426\u653E\u5F03\u90A3\u4EFD\u65E7\u5185\u5BB9\uFF0C\u4E0D\u5F97\u66FF\u7528\u6237\u51B3\u5B9A\uFF1B\u7528\u6237\u660E\u786E\u540C\u610F\u540E\uFF0C\u624D\u7528\u65B0\u7684 request_id \u53EA\u5BF9\u8BE5\u5E73\u53F0\u518D\u8C03\u7528 publish\uFF0C\u5E76\u4F20 discard_old_drafts\uFF1B\u7528\u6237\u4E0D\u540C\u610F\u5C31\u4FDD\u7559\u65E7\u5185\u5BB9\uFF0C\u544A\u8BC9\u7528\u6237\u8FD9\u4E2A\u5E73\u53F0\u6CA1\u6709\u53D1\u3002";
50364
50405
  function createMcpServer(bridge) {
50365
50406
  const server = new McpServer({ name: "writedock", version: package_default.version }, {
50366
50407
  instructions: agentWorkflow + 'WriteDock \u901A\u8FC7\u672C\u673A\u6269\u5C55\u6267\u884C\u6295\u7A3F\u3002\u5148\u68C0\u67E5\u8FDE\u63A5\u548C\u5E73\u53F0\u767B\u5F55\u3002\u5B9E\u9645\u5E73\u53F0\u80FD\u529B\u4EE5 list_platforms \u8FD4\u56DE\u7684 action\u3001publishModes\u3001tools\u3001modeOptions\u3001confirmation\u3001capabilities\u3001coverRequired \u4E3A\u51C6\uFF0C\u6269\u5C55\u66F4\u65B0\u540E\u4F18\u5148\u6309\u8BE5\u5143\u4FE1\u606F\u9009\u62E9\u5DE5\u5177\u3002\u77E5\u4E4E\u3001\u767E\u5BB6\u53F7\u3001\u5FAE\u535A\u957F\u6587\u7AE0\u3001B\u7AD9\u4E13\u680F\u3001\u5C0F\u7EA2\u4E66\u957F\u6587\u4F7F\u7528 publish_article\uFF0CAgent \u76F4\u63A5\u63D0\u4F9B Markdown/HTML \u6216\u6587\u4EF6\u8DEF\u5F84\u5373\u53EF\u53D1\u5E03\uFF0C\u65E0\u9700\u7528\u6237\u5728\u6269\u5C55\u7F16\u8F91\u3002\u767E\u5BB6\u53F7\u5FC5\u987B\u63D0\u4F9B cover \u5C01\u9762\uFF1B\u77E5\u4E4E\u53E6\u652F\u6301 <video src="\u672C\u5730MP4\u8DEF\u5F84"></video> \u6587\u7AE0\u5185\u89C6\u9891\uFF0C\u72EC\u7ACB\u5C01\u9762\u6682\u672A\u63A5\u5165\u3002prepare_article \u4EC5\u4F9B\u53EF\u9009\u672C\u673A\u9884\u89C8\uFF0C\u4E0D\u662F\u53D1\u5E03\u7684\u524D\u7F6E\u8981\u6C42\u3002\u5C0F\u7EA2\u4E66 ID \u4E3A xiaohongshu\uFF0C\u4E09\u79CD\u6A21\u5F0F\u5171\u7528\u6B64\u5E73\u53F0\uFF1A\u5199\u957F\u6587\u7528 publish_article\uFF0C\u652F\u6301\u6B63\u6587\u56FE\u7247\u3001\u539F\u751F\u81EA\u52A8\u6392\u7248\u540E\u53D1\u5E03\uFF0C\u6807\u9898\u6700\u591A 64 \u5B57\u3001\u6B63\u6587\u6700\u591A 10000 \u5B57\uFF0C\u4E0D\u63A5\u72EC\u7ACB cover \u6216\u6587\u7AE0\u5185\u89C6\u9891\uFF1B\u666E\u901A\u56FE\u6587\u7528 publish_post(kind=image)\uFF0C\u72EC\u7ACB\u89C6\u9891\u7528 publish_post(kind=video)\uFF0C\u672A\u4F20 cover_path \u65F6\u4FDD\u7559\u5E73\u53F0\u81EA\u52A8\u751F\u6210\u7684\u7B2C\u4E00\u5E27\u5C01\u9762\u3002\u5C0F\u7EA2\u4E66\u6700\u7EC8\u5747\u987B\u5339\u914D POST /web_api/sns/v2/note \u7684 success=true\u3001result=0 \u548C\u7B14\u8BB0 ID \u56DE\u6267\u624D\u786E\u8BA4 submitted\u3002\u6296\u97F3\u5E73\u53F0 ID \u4E3A douyin\uFF0C\u56FE\u6587\u548C\u89C6\u9891\u540C\u6837\u7528 publish_post(kind=image|video)\uFF0C\u53EF\u9009 cover_path\uFF1B\u56FE\u6587\u8BDD\u9898\u6700\u591A 4 \u4E2A\u3001\u89C6\u9891\u6700\u591A 5 \u4E2A\uFF0C\u5F53\u524D\u53EA\u63A5\u5373\u65F6\u53D1\u5E03\u3002\u6296\u97F3 confirmation=native_page_message\uFF0C\u4EE5\u672C\u6B21\u64CD\u4F5C\u65B0\u589E\u7684\u539F\u751F\u6210\u529F\u63D0\u793A\u786E\u8BA4 submitted\uFF0C\u672A\u6838\u5BF9\u6700\u7EC8 HTTP \u4E1A\u52A1\u56DE\u6267\uFF1B\u4EC5\u8DF3\u8F6C\u4F5C\u54C1\u7BA1\u7406\u65F6\u4ECD\u4E3A unknown\uFF0CAgent \u4E0D\u81EA\u52A8\u91CD\u53D1\u3002\u5C0F\u7EA2\u4E66\u3001\u6296\u97F3\u3001\u5FAE\u535A\u666E\u901A\u56FE\u6587\u4E0E\u72EC\u7ACB\u89C6\u9891\u4F7F\u7528 prepare_post / publish_post\uFF0C\u5FAE\u535A\u5E73\u53F0 ID \u4ECD\u4E3A weibo\uFF0C\u4E0D\u53E6\u5EFA\u5E73\u53F0\u3002\u5FAE\u535A\u56FE\u6587\u6807\u9898\u653E\u5165\u6B63\u6587\u9996\u884C\uFF0C\u6700\u591A 18 \u5F20\u56FE\u7247\uFF1B\u72EC\u7ACB\u89C6\u9891\u6807\u9898\u6700\u591A 30 \u4E2A\u6C49\u5B57\uFF0860 \u4E2A\u534A\u89D2\u5B57\u7B26\uFF09\uFF0Cweibo_video_creation \u5FC5\u586B\uFF0C\u987B\u4F9D\u636E\u7528\u6237\u5B9E\u9645\u5185\u5BB9\u4F20 original\u3001secondary \u6216 repost\uFF0C\u4E0D\u80FD\u9ED8\u8BA4\u539F\u521B\u3002\u5FAE\u535A\u540C\u4E00\u6B21\u53EA\u80FD\u9009\u56FE\u6587\u6216\u89C6\u9891\uFF0C\u957F\u6587\u7AE0\u7EE7\u7EED\u7528 publish_article\u3002\u53EA\u6709\u7528\u6237\u660E\u786E\u8981\u6C42\u53D1\u5E03\u5E76\u6307\u5B9A\u5E73\u53F0\u65F6\u624D\u8C03\u7528\u53D1\u5E03\u5DE5\u5177\u3002\u5FAE\u4FE1\u516C\u4F17\u53F7\u3001\u641C\u72D0\u53F7\u76EE\u524D\u7528 sync_article \u4FDD\u5B58\u6587\u7AE0\u8349\u7A3F\u3002B\u7AD9\u4E13\u680F ID \u4E3A bilibili\uFF0C\u4F7F\u7528 publish_article \u76F4\u63A5\u53D1\u5E03\u6587\u5B57\u548C\u6B63\u6587\u56FE\u7247\uFF0C\u4E0D\u63A5\u89C6\u9891\u3001\u97F3\u9891\u3001\u9644\u52A0\u5185\u5BB9\u5361\u7247\u6216\u72EC\u7ACB\u5C01\u9762\uFF1B\u901A\u8FC7\u65B0\u7248\u539F\u751F\u7F16\u8F91\u5668\u4E0A\u4F20\u5E76\u53D1\u5E03\uFF0Ccode=-352 \u65F6\u505C\u6B62\u81EA\u52A8\u63D0\u4EA4\u5E76\u8F6C\u4EBA\u5DE5\u9A8C\u8BC1\u3002\u9047\u5230 needs_user_action \u6216 requires_user_action \u65F6\uFF0C\u628A user_actions \u4E2D\u7684\u539F\u56E0\u548C\u7A3F\u4EF6\u94FE\u63A5\u544A\u8BC9\u7528\u6237\uFF0C\u4F18\u5148\u8BA9\u7528\u6237\u5207\u6362\u5230\u6269\u5C55\u5DF2\u6253\u5F00\u7684\u9875\u9762\u5904\u7406\u9A8C\u8BC1\u7801\u3001\u5C01\u9762\u3001\u5185\u5BB9\u58F0\u660E\u3001\u6765\u6E90\u3001\u539F\u521B\u534F\u8BAE\u6216\u53D1\u5E03\u8BBE\u7F6E\uFF0C\u5E76\u624B\u52A8\u53D1\u5E03\uFF1B\u4E0D\u5F97\u81EA\u52A8\u89E3\u51B3\u9A8C\u8BC1\u7801\u3001\u91CD\u65B0\u63D0\u4EA4\u6216\u6362 request_id\u3002\u8BE5\u72B6\u6001\u53EA\u8BB0\u5F55\u4EBA\u5DE5\u63A5\u7BA1\uFF0C\u4E0D\u4EE3\u8868\u7528\u6237\u4E4B\u540E\u7684\u64CD\u4F5C\u7ED3\u679C\u5DF2\u88AB\u786E\u8BA4\u3002\u6269\u5C55\u66F4\u65B0\u590D\u7528\u5DF2\u6709 MCP\uFF1B\u672A\u8FDE\u63A5\u65F6\uFF0C\u8BA9\u7528\u6237\u6253\u5F00 WriteDock \u6269\u5C55\u7684\u300C\u8FDE\u63A5\u300D\u9875\u70B9\u201C\u8FDE\u63A5\u201D\u3002\u63D0\u4EA4\u8FD4\u56DE\u4EFB\u52A1 ID\uFF0C\u968F\u540E\u7528 get_sync_task \u67E5\u8BE2\u7ED3\u679C\u3002\u4FDD\u6301\u540C\u4E00\u64CD\u4F5C\u7684 request_id \u4E0D\u53D8\u3002publicationState=unknown \u6216\u4EFB\u52A1\u4E2D\u65AD\u65F6\uFF0C\u5411\u7528\u6237\u7ED9\u51FA\u94FE\u63A5\u8BA9\u5176\u81EA\u884C\u6838\u5BF9\u5E73\u53F0\uFF0CAgent \u4E0D\u6253\u5F00\u7F51\u9875\uFF0C\u4E0D\u8981\u81EA\u52A8\u751F\u6210\u65B0 request_id \u91CD\u53D1\u3002\u76F4\u63A5\u53D1\u5E03\u6210\u529F\u8868\u793A\u5E73\u53F0\u5DF2\u63A5\u53D7\u63D0\u4EA4\uFF0C\u662F\u5426\u516C\u5F00\u5C55\u793A\u4EE5\u5E73\u53F0\u4E3A\u51C6\u3002'
@@ -50370,7 +50411,7 @@ function createMcpServer(bridge) {
50370
50411
  })).catch((error2) => ({ content: [{ type: "text", text: error2 instanceof Error ? error2.message : String(error2) }], isError: true }));
50371
50412
  const readOnly = { readOnlyHint: true, destructiveHint: false, openWorldHint: false };
50372
50413
  server.registerTool("get_connection_status", {
50373
- description: "\u68C0\u67E5\u672C\u673A MCP \u4E0E\u6269\u5C55\u7684\u8FDE\u63A5\u3001\u8FD0\u884C\u7248\u672C\u548C npm \u66F4\u65B0\u63D0\u793A\u3002update_available=true \u65F6\u544A\u77E5\u7528\u6237\u91CD\u65B0\u542F\u52A8 WriteDock MCP\uFF0C\u4FDD\u7559\u914D\u7F6E\uFF1B\u53D1\u5E03\u4EFB\u52A1\u8FDB\u884C\u4E2D\u4E0D\u8981\u81EA\u52A8\u91CD\u542F\u3002\u65E7\u7248\u670D\u52A1\u53EF\u80FD\u4E0D\u8FD4\u56DE\u7248\u672C\u5B57\u6BB5\uFF1Blatest_version \u7F3A\u5931\u6216 update_checking=true \u65F6\uFF0C\u4E0D\u5F97\u58F0\u79F0\u5DF2\u662F\u6700\u65B0\u7248\u3002",
50414
+ description: "\u68C0\u67E5\u672C\u673A MCP \u4E0E\u6269\u5C55\u7684\u8FDE\u63A5\u3001\u8FD0\u884C\u7248\u672C\u548C npm \u66F4\u65B0\u63D0\u793A\u3002update_available=true \u65F6\u544A\u77E5\u7528\u6237\u91CD\u65B0\u542F\u52A8 WriteDock MCP\uFF0C\u4FDD\u7559\u914D\u7F6E\uFF1B\u53D1\u5E03\u4EFB\u52A1\u8FDB\u884C\u4E2D\u4E0D\u8981\u81EA\u52A8\u91CD\u542F\u3002\u6709 update_notice \u65F6\u8F6C\u544A\u7528\u6237\u3002extension_version \u662F\u7528\u6237\u6B63\u5728\u7528\u7684\u6269\u5C55\u7248\u672C\uFF1B\u6CA1\u6709\u8FD9\u4E2A\u5B57\u6BB5\u8868\u793A\u7248\u672C\u672A\u77E5\uFF0C\u4E0D\u5F97\u58F0\u79F0\u6269\u5C55\u4E0E MCP \u7248\u672C\u4E00\u81F4\u3002\u914D\u7F6E\u91CC\u7684 writedock-mcp@latest \u662F\u6709\u610F\u7684\uFF0C\u7528\u6237\u9760\u5B83\u6536\u5230\u66F4\u65B0\uFF0C\u4E0D\u8981\u5EFA\u8BAE\u56FA\u5B9A\u7248\u672C\uFF0C\u4E5F\u4E0D\u8981\u4FEE\u6539\u8FD9\u6761\u914D\u7F6E\u3002\u65E7\u7248\u670D\u52A1\u53EF\u80FD\u4E0D\u8FD4\u56DE\u7248\u672C\u5B57\u6BB5\uFF1Blatest_version \u7F3A\u5931\u6216 update_checking=true \u65F6\uFF0C\u4E0D\u5F97\u58F0\u79F0\u5DF2\u662F\u6700\u65B0\u7248\u3002",
50374
50415
  inputSchema: {},
50375
50416
  annotations: { ...readOnly, openWorldHint: true }
50376
50417
  }, () => wrap(() => bridge.status()));
@@ -50725,7 +50766,7 @@ async function main() {
50725
50766
  const port = Number(process.env.WRITEDOCK_PORT || 9647);
50726
50767
  const identity = await loadIdentity(port, process.env.WRITEDOCK_TOKEN);
50727
50768
  const background = !process.env.WRITEDOCK_NO_SERVICE;
50728
- const bridge = new LocalBridge({ token: identity.token, port, credentials: identity, ...background ? { ensureService: () => ensureService(bridge, port) } : {} });
50769
+ const bridge = new LocalBridge({ token: identity.token, port, credentials: identity, ...background ? { ensureService: () => ensureService(bridge, port) } : {}, ...process.env.WRITEDOCK_EXTENSION_WAIT_MS ? { extensionWaitMs: Number(process.env.WRITEDOCK_EXTENSION_WAIT_MS) } : {} });
50729
50770
  await bridge.start();
50730
50771
  if (background) watchService(bridge, port);
50731
50772
  const server = createMcpServer(bridge);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "writedock-mcp",
3
- "version": "0.4.12",
3
+ "version": "0.4.14",
4
4
  "description": "WriteDock local MCP server for the browser extension",
5
5
  "type": "commonjs",
6
6
  "license": "GPL-3.0-only",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@writedock/mcp-server",
3
- "version": "0.4.12",
3
+ "version": "0.4.14",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "WriteDock local MCP server for the browser extension",
@@ -18,7 +18,7 @@ async function unusedPort() {
18
18
  return port
19
19
  }
20
20
  async function broker() {
21
- const bridge = new LocalBridge({ token, port: await unusedPort(), timeoutMs: 1000 })
21
+ const bridge = new LocalBridge({ token, port: await unusedPort(), timeoutMs: 1000, extensionWaitMs: 0 })
22
22
  brokers.push(bridge); await bridge.start(); return bridge
23
23
  }
24
24
  async function pair(bridge: LocalBridge, key = token) {
@@ -79,6 +79,29 @@ describe('authenticated local bridge', () => {
79
79
  expect(await bridge.request('getSyncHistory')).toEqual({ method: 'getSyncHistory' })
80
80
  })
81
81
 
82
+ it('waits for an extension that connects shortly after, and sends the request only once', async () => {
83
+ const bridge = new LocalBridge({ token, port: await unusedPort(), timeoutMs: 1000, extensionWaitMs: 5000 })
84
+ brokers.push(bridge); await bridge.start()
85
+ const asked: string[] = []
86
+ const answer = bridge.request('getSyncHistory')
87
+ await new Promise(resolve => setTimeout(resolve, 600))
88
+ const ws = await pair(bridge)
89
+ ws.on('message', raw => {
90
+ const message = JSON.parse(raw.toString())
91
+ if (message.method) { asked.push(message.method); ws.send(JSON.stringify({ id: message.id, result: { ok: true } })) }
92
+ })
93
+ expect(await answer).toEqual({ ok: true })
94
+ expect(asked).toEqual(['getSyncHistory'])
95
+ })
96
+
97
+ it('still reports a missing extension after the wait, without sending anything', async () => {
98
+ const bridge = new LocalBridge({ token, port: await unusedPort(), timeoutMs: 1000, extensionWaitMs: 300 })
99
+ brokers.push(bridge); await bridge.start()
100
+ const started = Date.now()
101
+ await expect(bridge.request('publish')).rejects.toThrow('这次没有发布')
102
+ expect(Date.now() - started).toBeGreaterThanOrEqual(250)
103
+ })
104
+
82
105
  it('allows two MCP clients to share a port without replacing the extension or re-sending requests', async () => {
83
106
  const first = await broker()
84
107
  const ws = await pair(first)
@@ -88,7 +111,7 @@ describe('authenticated local bridge', () => {
88
111
  const message = JSON.parse(raw.toString())
89
112
  if (message.method) { requests++; ws.send(JSON.stringify({ id: message.id, result: 'ok' })) }
90
113
  })
91
- const second = new LocalBridge({ token, port: first.port, timeoutMs: 1000 })
114
+ const second = new LocalBridge({ token, port: first.port, timeoutMs: 1000, extensionWaitMs: 0 })
92
115
  brokers.push(second)
93
116
  await second.start()
94
117
  expect(await second.request('listPlatforms')).toBe('ok')
@@ -100,7 +123,7 @@ describe('authenticated local bridge', () => {
100
123
 
101
124
  it('lets a remaining MCP client acquire the bridge when the first process exits', async () => {
102
125
  const first = await broker()
103
- const second = new LocalBridge({ token, port: first.port, timeoutMs: 1000 })
126
+ const second = new LocalBridge({ token, port: first.port, timeoutMs: 1000, extensionWaitMs: 0 })
104
127
  brokers.push(second)
105
128
  await second.start()
106
129
  await first.close()
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { updateNotice } from '../release-info'
3
+
4
+ describe('updateNotice', () => {
5
+ it('offers a newer extension and says what to click', () => {
6
+ const text = updateNotice({ bundle: '0.4.13', extension: '0.4.12' })
7
+ expect(text).toContain('v0.4.13')
8
+ expect(text).toContain('拼图图标')
9
+ expect(text).toContain('连接 agent')
10
+ expect(text).toContain('更新扩展')
11
+ expect(text).toContain('不要因此重发')
12
+ })
13
+ it('treats an extension that reports no version as older', () => {
14
+ expect(updateNotice({ bundle: '0.4.13' })).toContain('v0.4.13')
15
+ })
16
+ it('compares versions as numbers', () => {
17
+ expect(updateNotice({ bundle: '0.4.10', extension: '0.4.9' })).toContain('v0.4.10')
18
+ expect(updateNotice({ bundle: '0.4.9', extension: '0.4.10' })).toBeUndefined()
19
+ })
20
+ it('stays quiet when everything is current or nothing can be offered', () => {
21
+ expect(updateNotice({ bundle: '0.4.13', extension: '0.4.13', mcp: '0.4.13', mcpLatest: '0.4.13' })).toBeUndefined()
22
+ expect(updateNotice({ extension: '0.4.1' })).toBeUndefined()
23
+ })
24
+ it('offers a newer service with a restart', () => {
25
+ expect(updateNotice({ mcp: '0.4.12', mcpLatest: '0.4.13' })).toContain('重启 agent')
26
+ })
27
+ })
@@ -5,7 +5,7 @@ import http from 'node:http'
5
5
  import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto'
6
6
  import { WebSocket, WebSocketServer } from 'ws'
7
7
  import { MediaStore, type MediaAsset } from './media'
8
- import { mcpReleaseInfo } from './release-info'
8
+ import { mcpReleaseInfo, updateNotice } from './release-info'
9
9
  import { EXTENSION_ORIGIN } from './extension-id'
10
10
  import { updateInstalledExtension } from './extension-update'
11
11
 
@@ -27,6 +27,7 @@ export class LocalBridge {
27
27
  private starting?: Promise<void>
28
28
  private primary = false
29
29
  private extensionId?: string
30
+ private extensionVersion?: string
30
31
  private seenNonces = new Map<string, number>()
31
32
  private extensionToken: string
32
33
  private lastWork = 0
@@ -34,7 +35,7 @@ export class LocalBridge {
34
35
  private media: MediaStore
35
36
  private pending = new Map<string, { resolve: (value: unknown) => void; reject: (error: Error) => void; timer: ReturnType<typeof setTimeout> }>()
36
37
  readonly port: number
37
- constructor(private options: { token: string; port?: number; timeoutMs?: number; credentials?: { getExtensionToken: () => Promise<string>; setExtensionToken: (token: string) => Promise<void> }; ensureService?: () => Promise<void>; onStopRequested?: () => void }) {
38
+ constructor(private options: { token: string; port?: number; timeoutMs?: number; credentials?: { getExtensionToken: () => Promise<string>; setExtensionToken: (token: string) => Promise<void> }; ensureService?: () => Promise<void>; onStopRequested?: () => void; extensionWaitMs?: number }) {
38
39
  this.port = options.port ?? 9647
39
40
  this.extensionToken = options.token
40
41
  this.media = new MediaStore(this.port)
@@ -82,6 +83,19 @@ export class LocalBridge {
82
83
 
83
84
  get isPrimary() { return this.primary }
84
85
 
86
+ private notice() {
87
+ if (this.client?.readyState !== WebSocket.OPEN) return undefined
88
+ const release = mcpReleaseInfo()
89
+ return updateNotice({ bundle: release.extension_bundle_version, extension: this.extensionVersion, mcpLatest: release.latest_version, mcp: release.mcp_version })
90
+ }
91
+
92
+ private connectionFields() {
93
+ const connected = this.client?.readyState === WebSocket.OPEN, notice = this.notice()
94
+ // Seen live: agents read the bundled version as the version the user runs, so it stays out of what they see.
95
+ const { extension_bundle_version: _bundled, ...release } = mcpReleaseInfo()
96
+ return { connected, extensionId: this.extensionId, ...(this.extensionVersion ? { extension_version: this.extensionVersion } : {}), ...(notice ? { update_notice: notice } : {}), protocol: 'writedock/1', ...release }
97
+ }
98
+
85
99
  /** How long nothing has needed this service. An open extension connection counts as needed. */
86
100
  idleFor() {
87
101
  if (this.client?.readyState === WebSocket.OPEN) this.lastSeen = Date.now()
@@ -102,7 +116,7 @@ export class LocalBridge {
102
116
  await this.options.credentials?.setExtensionToken(token)
103
117
  this.extensionToken = token
104
118
  const previousClient = this.client
105
- this.client = undefined; this.extensionId = undefined
119
+ this.client = undefined; this.extensionId = undefined; this.extensionVersion = undefined
106
120
  for (const value of this.pending.values()) { clearTimeout(value.timer); value.reject(new Error(reason)) }
107
121
  this.pending.clear()
108
122
  previousClient?.close(1000, 'Extension connection replaced')
@@ -133,7 +147,7 @@ export class LocalBridge {
133
147
  }
134
148
  if (this.client?.readyState === WebSocket.OPEN) { ws.close(1008, 'An extension is already connected'); return }
135
149
  authenticated = true; clearTimeout(timer)
136
- this.client = ws; this.extensionId = String(message.extensionId || '')
150
+ this.client = ws; this.extensionId = String(message.extensionId || ''); this.extensionVersion = typeof message.extensionVersion === 'string' && message.extensionVersion.length <= 40 ? message.extensionVersion : undefined
137
151
  ws.send(JSON.stringify({ type: 'ready', ...mcpReleaseInfo() }))
138
152
  return
139
153
  }
@@ -155,7 +169,7 @@ export class LocalBridge {
155
169
  ws.on('close', () => {
156
170
  clearTimeout(timer)
157
171
  if (this.client !== ws) return
158
- this.client = undefined; this.extensionId = undefined
172
+ this.client = undefined; this.extensionId = undefined; this.extensionVersion = undefined
159
173
  for (const value of this.pending.values()) {
160
174
  clearTimeout(value.timer)
161
175
  value.reject(new Error('扩展连接已断开。若已提交同步,请按任务 ID 查询结果,不要更换 request_id 自动重发。'))
@@ -213,7 +227,7 @@ export class LocalBridge {
213
227
  }
214
228
  this.lastSeen = Date.now() // Status checks keep the service around, but only real work stops it being replaced.
215
229
  if (req.method === 'GET' && req.url === '/status') {
216
- send(200, { connected: this.client?.readyState === WebSocket.OPEN, extensionId: this.extensionId, protocol: 'writedock/1', ...mcpReleaseInfo() }); return
230
+ send(200, this.connectionFields()); return
217
231
  }
218
232
  this.lastWork = Date.now()
219
233
  if (req.method === 'POST' && req.url === '/assets/register') {
@@ -248,20 +262,35 @@ export class LocalBridge {
248
262
 
249
263
  async status() {
250
264
  await this.start()
251
- if (this.primary) return { connected: this.client?.readyState === WebSocket.OPEN, extensionId: this.extensionId, protocol: 'writedock/1', ...mcpReleaseInfo() }
265
+ if (this.primary) return this.connectionFields()
252
266
  try { return await this.httpCall('/status') }
253
267
  catch (error) {
254
268
  if (!(error instanceof TypeError)) throw error
255
269
  // Read-only health checks can acquire the vacant port after another MCP client exits.
256
270
  this.starting = undefined
257
271
  await this.start()
258
- if (this.primary) return { connected: false, protocol: 'writedock/1', ...mcpReleaseInfo() }
272
+ if (this.primary) return this.connectionFields()
259
273
  return this.httpCall('/status') // A background service took the port in the meantime.
260
274
  }
261
275
  }
262
276
 
277
+ /**
278
+ * The service may have only just started, for example on the first publication after a reboot, and the extension
279
+ * then needs a moment to connect. Nothing has been sent yet, so waiting here cannot submit anything twice.
280
+ */
281
+ private async waitForExtension() {
282
+ const end = Date.now() + (this.options.extensionWaitMs ?? 45_000)
283
+ while (Date.now() < end) {
284
+ try {
285
+ if (this.primary ? this.client?.readyState === WebSocket.OPEN : (await this.httpCall('/status')).connected === true) return
286
+ } catch { return } // The request below reports what is wrong.
287
+ await new Promise(resolve => setTimeout(resolve, 500))
288
+ }
289
+ }
290
+
263
291
  async request<T = unknown>(method: string, params: Record<string, unknown> = {}): Promise<T> {
264
292
  await this.start()
293
+ await this.waitForExtension()
265
294
  if (this.primary) return this.requestConnected(method, params) as Promise<T>
266
295
  try { return (await this.httpCall('/request', { method, params })).result as T }
267
296
  catch (error) {
@@ -280,7 +309,7 @@ export class LocalBridge {
280
309
  private async requestConnected(method: string, params: Record<string, unknown>): Promise<unknown> {
281
310
  if (this.client?.readyState !== WebSocket.OPEN) throw new Error('扩展未连接,这次没有发布。请让用户打开 Chrome 或 Edge,连接过的扩展通常会自动连上,然后再试;如果还是不行,打开 WriteDock 扩展点“连接”。无需重装 MCP。')
282
311
  const id = randomUUID()
283
- return new Promise((resolve, reject) => {
312
+ return new Promise<unknown>((resolve, reject) => {
284
313
  const timer = setTimeout(() => {
285
314
  this.pending.delete(id)
286
315
  reject(new Error('扩展响应超时;同步任务可能仍在运行,请查询任务状态。'))
@@ -290,6 +319,10 @@ export class LocalBridge {
290
319
  if (!error) return
291
320
  clearTimeout(timer); this.pending.delete(id); reject(error)
292
321
  })
322
+ }).then(result => {
323
+ // Only on a finished task, so the agent says it once at the end and never in the middle of a publication.
324
+ const notice = result && typeof result === 'object' && (result as { finished?: unknown }).finished === true ? this.notice() : undefined
325
+ return notice ? { ...result as object, update_notice: notice } : result
293
326
  })
294
327
  }
295
328
 
@@ -12,7 +12,7 @@ async function main() {
12
12
  const identity = await loadIdentity(port, process.env.WRITEDOCK_TOKEN)
13
13
  // Set only for tests that need one process owning the port by itself.
14
14
  const background = !process.env.WRITEDOCK_NO_SERVICE
15
- const bridge: LocalBridge = new LocalBridge({ token: identity.token, port, credentials: identity, ...(background ? { ensureService: () => ensureService(bridge, port) } : {}) })
15
+ const bridge: LocalBridge = new LocalBridge({ token: identity.token, port, credentials: identity, ...(background ? { ensureService: () => ensureService(bridge, port) } : {}), ...(process.env.WRITEDOCK_EXTENSION_WAIT_MS ? { extensionWaitMs: Number(process.env.WRITEDOCK_EXTENSION_WAIT_MS) } : {}) })
16
16
  await bridge.start()
17
17
  if (background) watchService(bridge, port)
18
18
  const server = createMcpServer(bridge)
@@ -68,3 +68,16 @@ export function mcpReleaseInfo() {
68
68
  update_checking: !!pending,
69
69
  }
70
70
  }
71
+
72
+ /**
73
+ * What the agent should pass on to the user once a task has finished. An extension that reports no version
74
+ * predates reporting it, so it is older than any bundle that can compare. Without a bundle nothing can be offered.
75
+ */
76
+ export function updateNotice(input: { bundle?: string; extension?: string; mcpLatest?: string; mcp?: string }) {
77
+ const notices: string[] = []
78
+ if (input.bundle && (!input.extension || newerVersion(input.bundle, input.extension))) {
79
+ notices.push(`WriteDock 扩展有新版本 v${input.bundle}${input.extension ? `(当前 v${input.extension})` : ''},需要用户自己点一下才会更新:点浏览器右上角的拼图图标,点 WriteDock,进入“连接 agent”,点“更新扩展”。连接和稿件都会保留。`)
80
+ }
81
+ if (input.mcpLatest && input.mcp && newerVersion(input.mcpLatest, input.mcp)) notices.push(`WriteDock 服务有新版本 v${input.mcpLatest}:重启 agent 就会更新。`)
82
+ return notices.length ? `${notices.join('')}请在汇报结果时把这些步骤原样告诉用户一次。这不影响本次结果,不要等用户更新,也不要因此重发。` : undefined
83
+ }
@@ -26,7 +26,7 @@ const postSource = {
26
26
  base_directory: z.string().optional().describe('使用相对素材路径时,明确指定素材目录'),
27
27
  }
28
28
 
29
- const agentWorkflow = 'Agent 只通过 WriteDock MCP 投稿和查询回执,不依赖也不要调用浏览器、Playwright 或其他网页工具。按结果的 next_action 操作:poll_task 用原 task_id/request_id 查询 get_sync_task;report_result 直接向用户汇报逐平台状态和链接。submitted/published 已有扩展确认,不再打开网页验证;验证码、图形验证、协议、发布设置或 unknown/interrupted 都将原因和链接交给用户自行点击处理,不自动重发、不换 request_id。人工接管不代表已经发布。唯一例外是 user_actions 的 type=old_draft:平台有一份用户上次没发完的内容,这篇尚未提交。必须先问用户是否放弃那份旧内容,不得替用户决定;用户明确同意后,才用新的 request_id 只对该平台再调用 publish,并传 discard_old_drafts;用户不同意就保留旧内容,告诉用户这个平台没有发。'
29
+ const agentWorkflow = 'Agent 只通过 WriteDock MCP 投稿和查询回执,不依赖也不要调用浏览器、Playwright 或其他网页工具。按结果的 next_action 操作:poll_task 用原 task_id/request_id 查询 get_sync_task;report_result 直接向用户汇报逐平台状态和链接。submitted/published 已有扩展确认,不再打开网页验证;验证码、图形验证、协议、发布设置或 unknown/interrupted 都将原因和链接交给用户自行点击处理,不自动重发、不换 request_id。人工接管不代表已经发布。结果里有 update_notice 时,在汇报结果末尾顺带告诉用户一次。唯一例外是 user_actions 的 type=old_draft:平台有一份用户上次没发完的内容,这篇尚未提交。必须先问用户是否放弃那份旧内容,不得替用户决定;用户明确同意后,才用新的 request_id 只对该平台再调用 publish,并传 discard_old_drafts;用户不同意就保留旧内容,告诉用户这个平台没有发。'
30
30
 
31
31
  export function createMcpServer(bridge: LocalBridge) {
32
32
  const server = new McpServer({ name: 'writedock', version: mcpPackage.version }, {
@@ -38,7 +38,7 @@ export function createMcpServer(bridge: LocalBridge) {
38
38
  const readOnly = { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
39
39
 
40
40
  server.registerTool('get_connection_status', {
41
- description: '检查本机 MCP 与扩展的连接、运行版本和 npm 更新提示。update_available=true 时告知用户重新启动 WriteDock MCP,保留配置;发布任务进行中不要自动重启。旧版服务可能不返回版本字段;latest_version 缺失或 update_checking=true 时,不得声称已是最新版。', inputSchema: {}, annotations: { ...readOnly, openWorldHint: true },
41
+ description: '检查本机 MCP 与扩展的连接、运行版本和 npm 更新提示。update_available=true 时告知用户重新启动 WriteDock MCP,保留配置;发布任务进行中不要自动重启。有 update_notice 时转告用户。extension_version 是用户正在用的扩展版本;没有这个字段表示版本未知,不得声称扩展与 MCP 版本一致。配置里的 writedock-mcp@latest 是有意的,用户靠它收到更新,不要建议固定版本,也不要修改这条配置。旧版服务可能不返回版本字段;latest_version 缺失或 update_checking=true 时,不得声称已是最新版。', inputSchema: {}, annotations: { ...readOnly, openWorldHint: true },
42
42
  }, () => wrap(() => bridge.status()))
43
43
  server.registerTool('list_platforms', {
44
44
  description: '列出扩展实际支持的平台、登录状态、publishModes、对应 tools 及 modeOptions 限制;小红书 article 对应 publish_article,image/video 对应 publish_post。不会返回 Cookie。',