writedock-mcp 0.4.6 → 0.4.8

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.6",
7
+ "version": "0.4.8",
8
8
  "minimum_chrome_version": "116",
9
9
  "icons": {
10
10
  "16": "assets/icon-16.png",
@@ -1 +1 @@
1
- import './assets/index.ts-DbQynGAJ.js';
1
+ import './assets/index.ts-CrE-nH9x.js';
@@ -6,7 +6,7 @@
6
6
  <meta name="color-scheme" content="light" />
7
7
  <title>写作台 · WriteDock</title>
8
8
  <link rel="icon" type="image/png" href="/assets/icon-48.png" />
9
- <script type="module" crossorigin src="/assets/editor-DWoK50xk.js"></script>
9
+ <script type="module" crossorigin src="/assets/editor-DSj8NlXD.js"></script>
10
10
  <link rel="modulepreload" crossorigin href="/assets/purify.es-1yEQDPgc.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/react-vendor-BrXlnJmF.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/task-utils-DwzGcJkm.js">
@@ -1,7 +1,8 @@
1
- WriteDock 安装与更新 v0.4.6
1
+ WriteDock 安装与更新 v0.4.8
2
2
 
3
- 首次使用(电脑需要 Node.js 22+)
4
- 1. 把这句话发给你的 agent:帮我安装 WriteDock:运行 npx -y writedock-mcp@latest setup,然后告诉我怎么重启你。
3
+ 首次使用
4
+ 1. 把这句话发给你的 agent:帮我安装 WriteDock:先检查电脑上有没有 Node.js 22 或更高版本,没有就帮我安装,需要我操作时告诉我;然后运行 npx -y writedock-mcp@latest setup,最后告诉我怎么重启你。
5
+ 如果 agent 装不上 Node.js:到 nodejs.org 下载 LTS 版,一路点下一步,重启 agent 后再发一次这句话。
5
6
  2. 按提示重启 agent,并在 Chrome / Edge 扩展管理页打开开发者模式,加载提示中的文件夹。这一步只需做一次。
6
7
  3. 打开扩展,点“连接”。
7
8
  4. 在同一个浏览器里登录要发布的平台。
package/index.cjs CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ if (Number(process.versions.node.split('.')[0]) < 22) { console.error('WriteDock 需要 Node.js 22 或更高版本,当前是 ' + process.version + '。请安装 https://nodejs.org 的 LTS 版,然后重启 agent。'); process.exit(1) }
2
3
  "use strict";
3
4
  var __create = Object.create;
4
5
  var __defProp = Object.defineProperty;
@@ -29739,7 +29740,7 @@ var MediaStore = class {
29739
29740
  // package.json
29740
29741
  var package_default = {
29741
29742
  name: "@writedock/mcp-server",
29742
- version: "0.4.6",
29743
+ version: "0.4.8",
29743
29744
  private: true,
29744
29745
  type: "module",
29745
29746
  description: "WriteDock local MCP server for the browser extension",
@@ -29953,7 +29954,6 @@ var LocalBridge = class {
29953
29954
  primary = false;
29954
29955
  extensionId;
29955
29956
  seenNonces = /* @__PURE__ */ new Map();
29956
- pairing;
29957
29957
  extensionToken;
29958
29958
  media;
29959
29959
  pending = /* @__PURE__ */ new Map();
@@ -29982,14 +29982,13 @@ var LocalBridge = class {
29982
29982
  server.on("upgrade", (req, socket, head) => {
29983
29983
  const origin = req.headers.origin;
29984
29984
  const extensionOrigin = typeof origin === "string" && /^chrome-extension:\/\/[a-p]{32}$/.test(origin);
29985
- if (!this.validHost(req.headers.host) || !["/extension", "/pair", "/connect"].includes(req.url || "") || origin && !extensionOrigin || req.url === "/pair" && !extensionOrigin || req.url === "/connect" && origin !== EXTENSION_ORIGIN) {
29985
+ if (!this.validHost(req.headers.host) || !["/extension", "/connect"].includes(req.url || "") || origin && !extensionOrigin || req.url === "/connect" && origin !== EXTENSION_ORIGIN) {
29986
29986
  socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
29987
29987
  socket.destroy();
29988
29988
  return;
29989
29989
  }
29990
29990
  sockets.handleUpgrade(req, socket, head, (ws) => {
29991
- if (req.url === "/pair") this.acceptPairing(ws);
29992
- else if (req.url === "/connect") void this.acceptConnect(ws);
29991
+ if (req.url === "/connect") void this.acceptConnect(ws);
29993
29992
  else this.accept(ws);
29994
29993
  });
29995
29994
  });
@@ -30011,16 +30010,6 @@ var LocalBridge = class {
30011
30010
  validHost(host) {
30012
30011
  return host === `127.0.0.1:${this.port}` || host === `localhost:${this.port}`;
30013
30012
  }
30014
- async beginPairing() {
30015
- const state = await this.status();
30016
- if (!this.primary && state.pairing_available !== true) throw new Error("\u672C\u673A\u7AEF\u53E3\u4ECD\u7531\u65E7\u7248 MCP \u5360\u7528\u3002\u8BF7\u91CD\u542F\u4ECD\u4F7F\u7528\u65E7\u7248\u672C\u7684 agent/MCP \u8FDB\u7A0B\uFF0C\u4FDD\u7559\u539F\u914D\u7F6E\u540E\u518D\u751F\u6210\u914D\u5BF9\u7801\u3002");
30017
- if (!this.primary) return this.httpCall("/pairing", {});
30018
- const secret = (0, import_node_crypto2.randomBytes)(8).toString("hex").toUpperCase().match(/.{4}/g).join("-");
30019
- const code = `WD1-${this.port}-${secret}`;
30020
- const expiresAt = Date.now() + 18e4;
30021
- this.pairing = { code, expiresAt, attempts: 0 };
30022
- return { pairing_code: code, expires_at: new Date(expiresAt).toISOString(), expires_in_seconds: 180, instructions: "\u5C06\u6B64\u4E00\u6B21\u6027\u914D\u5BF9\u7801\u7C98\u8D34\u5230\u6269\u5C55\u7684\u300CMCP \u8FDE\u63A5 \u2192 \u8F93\u5165\u4E00\u6B21\u6027\u914D\u5BF9\u7801\u300D\u3002\u6709\u6548\u671F 3 \u5206\u949F\uFF0C\u4F7F\u7528\u4E00\u6B21\u540E\u5931\u6548\u3002\u65E0\u9700\u91CD\u65B0\u5B89\u88C5 MCP \u6216\u4FEE\u6539 agent \u914D\u7F6E\u3002\u786E\u8BA4\u7528\u6237\u8981\u8FDE\u63A5\u5F53\u524D\u6269\u5C55\u540E\u518D\u751F\u6210\u3002" };
30023
- }
30024
30013
  /** Replaces the extension credential and drops whatever was using the old one. */
30025
30014
  async replaceExtensionToken(reason) {
30026
30015
  const token = (0, import_node_crypto2.randomBytes)(32).toString("hex");
@@ -30034,7 +30023,7 @@ var LocalBridge = class {
30034
30023
  value.reject(new Error(reason));
30035
30024
  }
30036
30025
  this.pending.clear();
30037
- previousClient?.close(1e3, "Extension pairing replaced");
30026
+ previousClient?.close(1e3, "Extension connection replaced");
30038
30027
  return token;
30039
30028
  }
30040
30029
  /** One-click connection: only this extension's fixed origin can reach here, and the user has just clicked 连接. */
@@ -30047,55 +30036,18 @@ var LocalBridge = class {
30047
30036
  ws.close(1011, "Connect failed");
30048
30037
  }
30049
30038
  }
30050
- acceptPairing(ws) {
30051
- const pairing = this.pairing;
30052
- if (!pairing || Date.now() > pairing.expiresAt || pairing.attempts >= 8) {
30053
- ws.close(1008, "Pairing code expired");
30054
- return;
30055
- }
30056
- pairing.attempts++;
30057
- const nonce = (0, import_node_crypto2.randomBytes)(32).toString("hex");
30058
- const timer = setTimeout(() => ws.close(1008, "Pairing timed out"), 1e4);
30059
- ws.send(JSON.stringify({ type: "pairing_challenge", nonce, proof: proof(pairing.code, "pairing-server", nonce) }));
30060
- let consumed = false;
30061
- ws.on("message", (raw) => {
30062
- void (async () => {
30063
- if (consumed) return;
30064
- consumed = true;
30065
- const body = raw.toString();
30066
- if (body.length > 2048) throw new Error("Invalid pairing message");
30067
- const message = JSON.parse(body);
30068
- if (this.pairing !== pairing || Date.now() > pairing.expiresAt || message.type !== "pairing_hello" || typeof message.proof !== "string" || !equal(message.proof, proof(pairing.code, "pairing-extension", nonce))) throw new Error("Pairing failed");
30069
- this.pairing = void 0;
30070
- const token = await this.replaceExtensionToken("\u6269\u5C55\u914D\u5BF9\u5DF2\u66F4\u6362\uFF1B\u8BF7\u67E5\u8BE2\u539F\u540C\u6B65\u4EFB\u52A1\uFF0C\u4E0D\u8981\u81EA\u52A8\u91CD\u65B0\u63D0\u4EA4\u3002");
30071
- const key2 = (0, import_node_crypto2.createHmac)("sha256", pairing.code).update(`writedock-pairing-key:${nonce}`).digest();
30072
- const iv = (0, import_node_crypto2.randomBytes)(12);
30073
- const cipher = (0, import_node_crypto2.createCipheriv)("aes-256-gcm", key2, iv);
30074
- cipher.setAAD(Buffer.from("writedock-pairing-v1"));
30075
- const ciphertext = Buffer.concat([cipher.update(JSON.stringify({ token, port: this.port, protocol: "writedock/1" })), cipher.final(), cipher.getAuthTag()]);
30076
- clearTimeout(timer);
30077
- if (ws.readyState === import_websocket.default.OPEN) ws.send(JSON.stringify({ type: "pairing_result", iv: iv.toString("base64"), ciphertext: ciphertext.toString("base64") }));
30078
- ws.close(1e3);
30079
- })().catch(() => {
30080
- ws.close(1008, "Pairing failed");
30081
- });
30082
- });
30083
- ws.on("close", () => clearTimeout(timer));
30084
- ws.on("error", () => {
30085
- });
30086
- }
30087
30039
  accept(ws) {
30088
30040
  const nonce = (0, import_node_crypto2.randomBytes)(32).toString("hex");
30089
30041
  const token = this.extensionToken;
30090
30042
  let authenticated = false;
30091
- const timer = setTimeout(() => ws.close(1008, "Pairing timed out"), 8e3);
30043
+ const timer = setTimeout(() => ws.close(1008, "Handshake timed out"), 8e3);
30092
30044
  ws.send(JSON.stringify({ type: "challenge", nonce, proof: proof(token, "server", nonce) }));
30093
30045
  ws.on("message", (raw) => {
30094
30046
  try {
30095
30047
  const message = JSON.parse(raw.toString());
30096
30048
  if (!authenticated) {
30097
30049
  if (token !== this.extensionToken || message.type !== "hello" || typeof message.proof !== "string" || !equal(message.proof, proof(token, "extension", nonce))) {
30098
- ws.close(1008, "Pairing failed");
30050
+ ws.close(1008, "Handshake failed");
30099
30051
  return;
30100
30052
  }
30101
30053
  if (this.client?.readyState === import_websocket.default.OPEN) {
@@ -30188,11 +30140,7 @@ var LocalBridge = class {
30188
30140
  this.seenNonces.set(nonce, now + 3e4);
30189
30141
  responseNonce = nonce;
30190
30142
  if (req.method === "GET" && req.url === "/status") {
30191
- send(200, { connected: this.client?.readyState === import_websocket.default.OPEN, extensionId: this.extensionId, protocol: "writedock/1", pairing_available: true, ...mcpReleaseInfo() });
30192
- return;
30193
- }
30194
- if (req.method === "POST" && req.url === "/pairing") {
30195
- send(200, await this.beginPairing());
30143
+ send(200, { connected: this.client?.readyState === import_websocket.default.OPEN, extensionId: this.extensionId, protocol: "writedock/1", ...mcpReleaseInfo() });
30196
30144
  return;
30197
30145
  }
30198
30146
  if (req.method === "POST" && req.url === "/assets/register") {
@@ -30231,14 +30179,14 @@ var LocalBridge = class {
30231
30179
  }
30232
30180
  async status() {
30233
30181
  await this.start();
30234
- if (this.primary) return { connected: this.client?.readyState === import_websocket.default.OPEN, extensionId: this.extensionId, protocol: "writedock/1", pairing_available: true, ...mcpReleaseInfo() };
30182
+ if (this.primary) return { connected: this.client?.readyState === import_websocket.default.OPEN, extensionId: this.extensionId, protocol: "writedock/1", ...mcpReleaseInfo() };
30235
30183
  try {
30236
30184
  return await this.httpCall("/status");
30237
30185
  } catch (error2) {
30238
30186
  if (!(error2 instanceof TypeError)) throw error2;
30239
30187
  this.starting = void 0;
30240
30188
  await this.start();
30241
- if (this.primary) return { connected: false, protocol: "writedock/1", pairing_available: true, ...mcpReleaseInfo() };
30189
+ if (this.primary) return { connected: false, protocol: "writedock/1", ...mcpReleaseInfo() };
30242
30190
  throw error2;
30243
30191
  }
30244
30192
  }
@@ -30258,7 +30206,7 @@ var LocalBridge = class {
30258
30206
  return await this.httpCall("/assets/register", { path, kind });
30259
30207
  }
30260
30208
  async requestConnected(method, params) {
30261
- if (this.client?.readyState !== import_websocket.default.OPEN) throw new Error("\u6269\u5C55\u672A\u8FDE\u63A5\u3002\u8BF7\u8BA9\u7528\u6237\u6253\u5F00\u6D4F\u89C8\u5668\u7684 WriteDock \u6269\u5C55\uFF0C\u5728\u300C\u8FDE\u63A5\u300D\u9875\u70B9\u201C\u8FDE\u63A5\u201D\u3002\u65E0\u9700\u91CD\u88C5 MCP\u3002");
30209
+ if (this.client?.readyState !== import_websocket.default.OPEN) throw new Error("\u6269\u5C55\u672A\u8FDE\u63A5\uFF0C\u8FD9\u6B21\u6CA1\u6709\u53D1\u5E03\u3002\u8BF7\u8BA9\u7528\u6237\u6253\u5F00 Chrome \u6216 Edge\uFF0C\u8FDE\u63A5\u8FC7\u7684\u6269\u5C55\u901A\u5E38\u4F1A\u81EA\u52A8\u8FDE\u4E0A\uFF0C\u7136\u540E\u518D\u8BD5\uFF1B\u5982\u679C\u8FD8\u662F\u4E0D\u884C\uFF0C\u6253\u5F00 WriteDock \u6269\u5C55\u70B9\u201C\u8FDE\u63A5\u201D\u3002\u65E0\u9700\u91CD\u88C5 MCP\u3002");
30262
30210
  const id = (0, import_node_crypto2.randomUUID)();
30263
30211
  return new Promise((resolve4, reject) => {
30264
30212
  const timer = setTimeout(() => {
@@ -30275,7 +30223,6 @@ var LocalBridge = class {
30275
30223
  });
30276
30224
  }
30277
30225
  async close() {
30278
- this.pairing = void 0;
30279
30226
  clearInterval(this.heartbeat);
30280
30227
  for (const value of this.pending.values()) {
30281
30228
  clearTimeout(value.timer);
@@ -50382,7 +50329,7 @@ var postSource = {
50382
50329
  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"),
50383
50330
  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")
50384
50331
  };
50385
- 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";
50332
+ 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";
50386
50333
  function createMcpServer(bridge) {
50387
50334
  const server = new McpServer({ name: "writedock", version: package_default.version }, {
50388
50335
  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'
@@ -50396,11 +50343,6 @@ function createMcpServer(bridge) {
50396
50343
  inputSchema: {},
50397
50344
  annotations: { ...readOnly, openWorldHint: true }
50398
50345
  }, () => wrap(() => bridge.status()));
50399
- server.registerTool("begin_pairing", {
50400
- description: "\u901A\u5E38\u4E0D\u9700\u8981\uFF1A\u672A\u8FDE\u63A5\u65F6\uFF0C\u8BA9\u7528\u6237\u6253\u5F00 WriteDock \u6269\u5C55\u7684\u300C\u8FDE\u63A5\u300D\u9875\u70B9\u201C\u8FDE\u63A5\u201D\u5373\u53EF\u3002\u53EA\u6709\u7528\u6237\u4F7F\u7528\u65E7\u7248\u6269\u5C55\u65F6\u624D\u751F\u6210 3 \u5206\u949F\u6709\u6548\u7684\u4E00\u6B21\u6027\u914D\u5BF9\u7801\u3002",
50401
- inputSchema: {},
50402
- annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
50403
- }, () => wrap(() => bridge.beginPairing()));
50404
50346
  server.registerTool("list_platforms", {
50405
50347
  description: "\u5217\u51FA\u6269\u5C55\u5B9E\u9645\u652F\u6301\u7684\u5E73\u53F0\u3001\u767B\u5F55\u72B6\u6001\u3001publishModes\u3001\u5BF9\u5E94 tools \u53CA modeOptions \u9650\u5236\uFF1B\u5C0F\u7EA2\u4E66 article \u5BF9\u5E94 publish_article\uFF0Cimage/video \u5BF9\u5E94 publish_post\u3002\u4E0D\u4F1A\u8FD4\u56DE Cookie\u3002",
50406
50348
  inputSchema: { force_refresh: external_exports.boolean().optional() },
@@ -50459,9 +50401,9 @@ function createMcpServer(bridge) {
50459
50401
  };
50460
50402
  server.registerTool("publish", {
50461
50403
  description: agentWorkflow + "\u9996\u9009\u7684\u53D1\u5E03\u5DE5\u5177\uFF1A\u4E00\u6B21\u8C03\u7528\u53D1\u5E03\u5230\u7528\u6237\u6307\u5B9A\u7684\u591A\u4E2A\u5E73\u53F0\u3002\u53EA\u6709\u7528\u6237\u660E\u786E\u8981\u6C42\u53D1\u5E03\u5E76\u6307\u5B9A\u5E73\u53F0\u65F6\u624D\u8C03\u7528\u3002\u6309\u5E73\u53F0\u4F9D\u6B21\u6267\u884C\uFF0C\u67D0\u4E2A\u5E73\u53F0\u672A\u767B\u5F55\u3001\u4E0D\u80FD\u63A5\u6536\u5185\u5BB9\u6216\u5931\u8D25\uFF0C\u53EA\u8DF3\u8FC7\u8BE5\u5E73\u53F0\uFF0C\u5176\u4ED6\u5E73\u53F0\u7EE7\u7EED\u3002\u9047\u5230\u9A8C\u8BC1\u7801\u65F6\u5E73\u53F0\u8FDB\u5165 waiting_verification\uFF0C\u7ACB\u5373\u628A\u539F\u56E0\u548C\u94FE\u63A5\u544A\u8BC9\u7528\u6237\uFF0C\u7531\u7528\u6237\u624B\u52A8\u5B8C\u6210\uFF0C\u7EE7\u7EED\u67E5\u8BE2\u5373\u53EF\uFF0C\u4E0D\u8981\u91CD\u53D1\u3002\u4EFB\u52A1\u7ED3\u675F\u540E\u6309 get_sync_task \u7684 summary \u9010\u5E73\u53F0\u544A\u8BC9\u7528\u6237\uFF1A\u54EA\u4E9B\u6210\u529F\uFF0C\u54EA\u4E9B\u672A\u767B\u5F55\uFF08not_logged_in\uFF09\u7B49\u3002\u7ACB\u5373\u8FD4\u56DE task_id\uFF0C\u518D\u7528 get_sync_task \u67E5\u8BE2\uFF1Bresults \u662F\u9010\u5E73\u53F0\u7ED3\u679C\uFF0Cwarnings \u8BF4\u660E\u4E3A\u5E73\u53F0\u505A\u7684\u8C03\u6574\u3002\u5FC5\u987B\u4F7F\u7528\u7A33\u5B9A request_id\uFF0C\u76F8\u540C ID \u4E0D\u4F1A\u91CD\u590D\u63D0\u4EA4\u3002\u90E8\u5206\u5E73\u53F0\u5931\u8D25\u65F6\uFF0C\u53EA\u9488\u5BF9\u5931\u8D25\u5E73\u53F0\u7528\u65B0 request_id \u91CD\u65B0\u63D0\u4EA4\uFF0Cunknown \u7684\u5E73\u53F0\u4E0D\u8981\u91CD\u53D1\u3002\u65E7\u7248\u6269\u5C55\u4E0D\u652F\u6301\u65F6\u4F1A\u62A5\u9519\uFF0C\u4E0D\u4F1A\u53D1\u5E03\u3002",
50462
- inputSchema: { ...unifiedInput, request_id: requestId },
50404
+ inputSchema: { ...unifiedInput, request_id: requestId, discard_old_drafts: external_exports.array(external_exports.enum(["douyin"])).optional().describe('\u53EA\u6709\u7528\u6237\u660E\u786E\u540C\u610F\u653E\u5F03\u5E73\u53F0\u4E0A\u6B21\u6CA1\u53D1\u5B8C\u7684\u5185\u5BB9\u65F6\u624D\u4F20\uFF0C\u5982 ["douyin"]\u3002\u653E\u5F03\u540E\u65E0\u6CD5\u6062\u590D\u3002') },
50463
50405
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
50464
- }, (args) => wrap(async () => bridge.request("publish", { requestId: args.request_id, ...await unifiedParams(args) })));
50406
+ }, (args) => wrap(async () => bridge.request("publish", { requestId: args.request_id, ...args.discard_old_drafts ? { discardOldDrafts: args.discard_old_drafts } : {}, ...await unifiedParams(args) })));
50465
50407
  server.registerTool("rehearse_publish", {
50466
50408
  description: "\u4E0E publish \u53C2\u6570\u76F8\u540C\u7684\u6F14\u7EC3\uFF1A\u9010\u5E73\u53F0\u6253\u5F00\u539F\u751F\u9875\u9762\uFF0C\u586B\u597D\u5185\u5BB9\u3001\u4E0A\u4F20\u7D20\u6750\uFF0C\u8D70\u5230\u70B9\u51FB\u53D1\u5E03\u4E4B\u524D\u505C\u4E0B\uFF0C\u4E0D\u63D0\u4EA4\u53D1\u5E03\u3002\u65E0\u9700 request_id\uFF0C\u4E0D\u5F71\u54CD\u4E4B\u540E\u7684\u771F\u5B9E\u53D1\u5E03\u3002\u56FE\u7247\u53EF\u80FD\u5DF2\u4E0A\u4F20\u5230\u5E73\u53F0\uFF0C\u90E8\u5206\u5E73\u53F0\u4F1A\u7559\u4E0B\u4E2D\u95F4\u8349\u7A3F\uFF0C\u516C\u4F17\u53F7\u3001\u641C\u72D0\u53F7\u4E0D\u53C2\u4E0E\u3002\u7ED3\u679C publicationState=rehearsed \u4E0D\u4EE3\u8868\u5DF2\u53D1\u5E03\uFF0C\u4E0D\u5F97\u544A\u8BC9\u7528\u6237\u5185\u5BB9\u5DF2\u53D1\u51FA\uFF0C\u4E5F\u4E0D\u8981\u81EA\u884C\u63A5\u7740\u53D1\u5E03\u3002",
50467
50409
  inputSchema: unifiedInput,
@@ -50529,7 +50471,7 @@ async function loadIdentity(port, legacyToken) {
50529
50471
  const filename = (0, import_node_path4.join)(directory, `identity-${port}.json`);
50530
50472
  const read = async () => {
50531
50473
  const value = JSON.parse(await (0, import_promises4.readFile)(filename, "utf8"));
50532
- if (value.version !== 1 || !validToken(value.token) || !validToken(value.extensionToken)) throw new Error("MCP \u914D\u5BF9\u8D44\u6599\u683C\u5F0F\u4E0D\u6B63\u786E\uFF1B\u8BF7\u4FDD\u7559\u539F\u6587\u4EF6\u5E76\u68C0\u67E5\u5907\u4EFD\uFF0C\u4E0D\u4F1A\u81EA\u52A8\u8986\u76D6");
50474
+ if (value.version !== 1 || !validToken(value.token) || !validToken(value.extensionToken)) throw new Error("MCP \u8FDE\u63A5\u8D44\u6599\u683C\u5F0F\u4E0D\u6B63\u786E\uFF1B\u8BF7\u4FDD\u7559\u539F\u6587\u4EF6\u5E76\u68C0\u67E5\u5907\u4EFD\uFF0C\u4E0D\u4F1A\u81EA\u52A8\u8986\u76D6");
50533
50475
  return value;
50534
50476
  };
50535
50477
  let identity;
@@ -50557,7 +50499,7 @@ async function loadIdentity(port, legacyToken) {
50557
50499
  token: identity.token,
50558
50500
  getExtensionToken: async () => (await read()).extensionToken,
50559
50501
  setExtensionToken: async (extensionToken) => {
50560
- if (!validToken(extensionToken)) throw new Error("\u6269\u5C55\u914D\u5BF9\u5BC6\u94A5\u683C\u5F0F\u4E0D\u6B63\u786E");
50502
+ if (!validToken(extensionToken)) throw new Error("\u6269\u5C55\u8FDE\u63A5\u5BC6\u94A5\u683C\u5F0F\u4E0D\u6B63\u786E");
50561
50503
  const current = await read();
50562
50504
  if (current.token !== identity.token) throw new Error("MCP \u8EAB\u4EFD\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u542F\u52A8\u670D\u52A1");
50563
50505
  const temporary = (0, import_node_path4.join)(directory, `.identity-${(0, import_node_crypto3.randomUUID)()}.tmp`);
@@ -50577,11 +50519,11 @@ var import_node_child_process = require("child_process");
50577
50519
  var import_promises5 = require("fs/promises");
50578
50520
  var import_node_os3 = require("os");
50579
50521
  var import_node_path5 = require("path");
50580
- var COMMAND = "npx";
50581
- var ARGS = ["-y", "--prefer-online", "writedock-mcp@latest"];
50522
+ var NPX_ARGS = ["-y", "--prefer-online", "writedock-mcp@latest"];
50523
+ var serverCommand = (platform) => platform === "win32" ? { command: "cmd", args: ["/c", "npx", ...NPX_ARGS] } : { command: "npx", args: NPX_ARGS };
50582
50524
  async function readIfPresent(path) {
50583
50525
  try {
50584
- return await (0, import_promises5.readFile)(path, "utf8");
50526
+ return (await (0, import_promises5.readFile)(path, "utf8")).replace(/^\uFEFF/, "");
50585
50527
  } catch (error2) {
50586
50528
  if (error2.code === "ENOENT") return void 0;
50587
50529
  throw error2;
@@ -50592,12 +50534,13 @@ async function save(path, content, previous) {
50592
50534
  if (previous !== void 0) await (0, import_promises5.copyFile)(path, `${path}.writedock-backup`);
50593
50535
  await (0, import_promises5.writeFile)(path, content);
50594
50536
  }
50595
- async function configureCodex(path) {
50537
+ async function configureCodex(path, platform = process.platform) {
50596
50538
  const previous = await readIfPresent(path);
50597
50539
  if (previous !== void 0 && /^\s*\[mcp_servers\.writedock\]/m.test(previous)) return "present";
50540
+ const { command, args } = serverCommand(platform);
50598
50541
  const entry = `[mcp_servers.writedock]
50599
- command = "${COMMAND}"
50600
- args = ${JSON.stringify(ARGS)}
50542
+ command = "${command}"
50543
+ args = ${JSON.stringify(args)}
50601
50544
  startup_timeout_sec = 120
50602
50545
  `;
50603
50546
  await save(path, previous ? `${previous.replace(/\s*$/, "")}
@@ -50605,11 +50548,11 @@ startup_timeout_sec = 120
50605
50548
  ${entry}` : entry, previous);
50606
50549
  return "added";
50607
50550
  }
50608
- async function configureJsonClient(path) {
50551
+ async function configureJsonClient(path, platform = process.platform) {
50609
50552
  const previous = await readIfPresent(path);
50610
50553
  const config2 = previous?.trim() ? JSON.parse(previous) : {};
50611
50554
  if (config2.mcpServers?.writedock) return "present";
50612
- config2.mcpServers = { ...config2.mcpServers, writedock: { command: COMMAND, args: ARGS } };
50555
+ config2.mcpServers = { ...config2.mcpServers, writedock: serverCommand(platform) };
50613
50556
  await save(path, `${JSON.stringify(config2, null, 2)}
50614
50557
  `, previous);
50615
50558
  return "added";
@@ -50625,19 +50568,20 @@ function revealFolder(path) {
50625
50568
  } catch {
50626
50569
  }
50627
50570
  }
50628
- async function runSetup(home = (0, import_node_os3.homedir)(), log = console.log) {
50571
+ async function runSetup(home = (0, import_node_os3.homedir)(), log = console.log, platform = process.platform) {
50572
+ const claudeDesktop = platform === "win32" ? (0, import_node_path5.join)(process.env.APPDATA || (0, import_node_path5.join)(home, "AppData/Roaming"), "Claude/claude_desktop_config.json") : platform === "darwin" ? (0, import_node_path5.join)(home, "Library/Application Support/Claude/claude_desktop_config.json") : (0, import_node_path5.join)(home, ".config/Claude/claude_desktop_config.json");
50629
50573
  const clients = [
50630
50574
  { name: "Codex", path: (0, import_node_path5.join)(home, ".codex/config.toml"), detect: (0, import_node_path5.join)(home, ".codex/config.toml"), configure: configureCodex },
50631
50575
  { name: "Claude Code", path: (0, import_node_path5.join)(home, ".claude.json"), detect: (0, import_node_path5.join)(home, ".claude.json"), configure: configureJsonClient },
50632
50576
  { name: "Cursor", path: (0, import_node_path5.join)(home, ".cursor/mcp.json"), detect: (0, import_node_path5.join)(home, ".cursor/mcp.json"), configure: configureJsonClient },
50633
- { name: "Claude Desktop", path: (0, import_node_path5.join)(home, "Library/Application Support/Claude/claude_desktop_config.json"), detect: (0, import_node_path5.join)(home, "Library/Application Support/Claude/claude_desktop_config.json"), configure: configureJsonClient }
50577
+ { name: "Claude Desktop", path: claudeDesktop, detect: claudeDesktop, configure: configureJsonClient }
50634
50578
  ];
50635
50579
  let found = 0;
50636
50580
  for (const client of clients) {
50637
50581
  if (!await exists(client.detect)) continue;
50638
50582
  found++;
50639
50583
  try {
50640
- const state = await client.configure(client.path);
50584
+ const state = await client.configure(client.path, platform);
50641
50585
  log(state === "added" ? `\u2713 ${client.name}\uFF1A\u5DF2\u6DFB\u52A0 WriteDock\uFF08\u539F\u914D\u7F6E\u5907\u4EFD\u4E3A .writedock-backup\uFF09` : `\u2713 ${client.name}\uFF1A\u5DF2\u6709 WriteDock \u914D\u7F6E\uFF0C\u672A\u6539\u52A8`);
50642
50586
  } catch (error2) {
50643
50587
  log(`\u2717 ${client.name}\uFF1A\u914D\u7F6E\u5931\u8D25\uFF08${error2 instanceof Error ? error2.message : String(error2)}\uFF09\uFF0C\u539F\u6587\u4EF6\u672A\u6539\u52A8`);
@@ -50645,7 +50589,7 @@ async function runSetup(home = (0, import_node_os3.homedir)(), log = console.log
50645
50589
  }
50646
50590
  if (!found) {
50647
50591
  log("\u6CA1\u6709\u627E\u5230\u5DF2\u77E5\u7684 agent \u5BA2\u6237\u7AEF\u3002\u8BF7\u5728\u4F60\u7684\u5BA2\u6237\u7AEF\u91CC\u6DFB\u52A0\u8FD9\u4E2A MCP \u670D\u52A1\uFF1A");
50648
- log(JSON.stringify({ mcpServers: { writedock: { command: COMMAND, args: ARGS } } }, null, 2));
50592
+ log(JSON.stringify({ mcpServers: { writedock: serverCommand(platform) } }, null, 2));
50649
50593
  }
50650
50594
  const bundle = bundledExtension();
50651
50595
  const loaded = bundle ? await findInstalledExtension().catch(() => void 0) : void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "writedock-mcp",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
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.6",
3
+ "version": "0.4.8",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "WriteDock local MCP server for the browser extension",
@@ -22,6 +22,25 @@ describe('setup', () => {
22
22
  expect(await readFile(join(home, '.codex/config.toml.writedock-backup'), 'utf8')).toBe(original)
23
23
  })
24
24
 
25
+ it('starts npx through cmd on Windows, and finds Claude Desktop in its Windows folder', async () => {
26
+ const appData = process.env.APPDATA
27
+ delete process.env.APPDATA
28
+ try {
29
+ await mkdir(join(home, '.codex'))
30
+ await writeFile(join(home, '.codex/config.toml'), 'model = "x"\r\n')
31
+ const desktop = join(home, 'AppData/Roaming/Claude')
32
+ await mkdir(desktop, { recursive: true })
33
+ await writeFile(join(desktop, 'claude_desktop_config.json'), '\uFEFF{"theme":"dark"}')
34
+ expect(await runSetup(home, line => log.push(line), 'win32')).toBe(2)
35
+ const codex = await readFile(join(home, '.codex/config.toml'), 'utf8')
36
+ expect(codex).toContain('command = "cmd"')
37
+ expect(codex).toContain('args = ["/c","npx","-y","--prefer-online","writedock-mcp@latest"]')
38
+ const config = JSON.parse(await readFile(join(desktop, 'claude_desktop_config.json'), 'utf8'))
39
+ expect(config.theme).toBe('dark')
40
+ expect(config.mcpServers.writedock).toEqual({ command: 'cmd', args: ['/c', 'npx', '-y', '--prefer-online', 'writedock-mcp@latest'] })
41
+ } finally { if (appData !== undefined) process.env.APPDATA = appData }
42
+ })
43
+
25
44
  it('leaves an existing entry alone, including its custom port and token', async () => {
26
45
  await mkdir(join(home, '.codex'))
27
46
  const original = '[mcp_servers.writedock]\ncommand = "node"\n\n[mcp_servers.writedock.env]\nWRITEDOCK_PORT = "9700"\n'
@@ -1,8 +1,8 @@
1
1
  /** Local bridge, based on Wechatsync's stdio → WebSocket architecture (MIT).
2
- * WriteDock uses one loopback port, authenticated forwarding and mutual HMAC pairing.
2
+ * WriteDock uses one loopback port, authenticated forwarding and an HMAC handshake.
3
3
  */
4
4
  import http from 'node:http'
5
- import { createCipheriv, createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto'
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
8
  import { mcpReleaseInfo } from './release-info'
@@ -28,7 +28,6 @@ export class LocalBridge {
28
28
  private primary = false
29
29
  private extensionId?: string
30
30
  private seenNonces = new Map<string, number>()
31
- private pairing?: { code: string; expiresAt: number; attempts: number }
32
31
  private extensionToken: string
33
32
  private media: MediaStore
34
33
  private pending = new Map<string, { resolve: (value: unknown) => void; reject: (error: Error) => void; timer: ReturnType<typeof setTimeout> }>()
@@ -47,7 +46,7 @@ export class LocalBridge {
47
46
  if ((error as NodeJS.ErrnoException).code !== 'EADDRINUSE') throw error
48
47
  this.server = undefined
49
48
  this.sockets = undefined
50
- // Only attach to a broker that proves it holds this pairing key.
49
+ // Only attach to a broker that proves it holds this key.
51
50
  await this.httpCall('/status')
52
51
  }).catch(error => { this.starting = undefined; throw error })
53
52
  return this.starting
@@ -61,10 +60,10 @@ export class LocalBridge {
61
60
  server.on('upgrade', (req, socket, head) => {
62
61
  const origin = req.headers.origin
63
62
  const extensionOrigin = typeof origin === 'string' && /^chrome-extension:\/\/[a-p]{32}$/.test(origin)
64
- if (!this.validHost(req.headers.host) || !['/extension', '/pair', '/connect'].includes(req.url || '') || (origin && !extensionOrigin) || (req.url === '/pair' && !extensionOrigin) || (req.url === '/connect' && origin !== EXTENSION_ORIGIN)) {
63
+ if (!this.validHost(req.headers.host) || !['/extension', '/connect'].includes(req.url || '') || (origin && !extensionOrigin) || (req.url === '/connect' && origin !== EXTENSION_ORIGIN)) {
65
64
  socket.write('HTTP/1.1 403 Forbidden\r\n\r\n'); socket.destroy(); return
66
65
  }
67
- sockets.handleUpgrade(req, socket, head, ws => { if (req.url === '/pair') this.acceptPairing(ws); else if (req.url === '/connect') void this.acceptConnect(ws); else this.accept(ws) })
66
+ sockets.handleUpgrade(req, socket, head, ws => { if (req.url === '/connect') void this.acceptConnect(ws); else this.accept(ws) })
68
67
  })
69
68
  await new Promise<void>((resolve, reject) => {
70
69
  server.once('error', reject)
@@ -81,17 +80,6 @@ export class LocalBridge {
81
80
 
82
81
  private validHost(host?: string) { return host === `127.0.0.1:${this.port}` || host === `localhost:${this.port}` }
83
82
 
84
- async beginPairing(): Promise<Record<string, unknown>> {
85
- const state = await this.status() // Also acquire the vacant bridge after another MCP client exits.
86
- if (!this.primary && state.pairing_available !== true) throw new Error('本机端口仍由旧版 MCP 占用。请重启仍使用旧版本的 agent/MCP 进程,保留原配置后再生成配对码。')
87
- if (!this.primary) return this.httpCall('/pairing', {})
88
- const secret = randomBytes(8).toString('hex').toUpperCase().match(/.{4}/g)!.join('-')
89
- const code = `WD1-${this.port}-${secret}`
90
- const expiresAt = Date.now() + 180_000
91
- this.pairing = { code, expiresAt, attempts: 0 }
92
- return { pairing_code: code, expires_at: new Date(expiresAt).toISOString(), expires_in_seconds: 180, instructions: '将此一次性配对码粘贴到扩展的「MCP 连接 → 输入一次性配对码」。有效期 3 分钟,使用一次后失效。无需重新安装 MCP 或修改 agent 配置。确认用户要连接当前扩展后再生成。' }
93
- }
94
-
95
83
  /** Replaces the extension credential and drops whatever was using the old one. */
96
84
  private async replaceExtensionToken(reason: string) {
97
85
  const token = randomBytes(32).toString('hex')
@@ -101,7 +89,7 @@ export class LocalBridge {
101
89
  this.client = undefined; this.extensionId = undefined
102
90
  for (const value of this.pending.values()) { clearTimeout(value.timer); value.reject(new Error(reason)) }
103
91
  this.pending.clear()
104
- previousClient?.close(1000, 'Extension pairing replaced')
92
+ previousClient?.close(1000, 'Extension connection replaced')
105
93
  return token
106
94
  }
107
95
 
@@ -114,51 +102,18 @@ export class LocalBridge {
114
102
  } catch { ws.close(1011, 'Connect failed') }
115
103
  }
116
104
 
117
- private acceptPairing(ws: WebSocket) {
118
- const pairing = this.pairing
119
- if (!pairing || Date.now() > pairing.expiresAt || pairing.attempts >= 8) { ws.close(1008, 'Pairing code expired'); return }
120
- pairing.attempts++
121
- const nonce = randomBytes(32).toString('hex')
122
- const timer = setTimeout(() => ws.close(1008, 'Pairing timed out'), 10_000)
123
- ws.send(JSON.stringify({ type: 'pairing_challenge', nonce, proof: proof(pairing.code, 'pairing-server', nonce) }))
124
- let consumed = false
125
- ws.on('message', raw => {
126
- void (async () => {
127
- if (consumed) return
128
- consumed = true
129
- const body = raw.toString()
130
- if (body.length > 2048) throw new Error('Invalid pairing message')
131
- const message = JSON.parse(body)
132
- if (this.pairing !== pairing || Date.now() > pairing.expiresAt || message.type !== 'pairing_hello' || typeof message.proof !== 'string' || !equal(message.proof, proof(pairing.code, 'pairing-extension', nonce))) throw new Error('Pairing failed')
133
- this.pairing = undefined // Consume before the first asynchronous operation.
134
- // A deliberate new pairing retires the old extension credential, not the MCP identity.
135
- const token = await this.replaceExtensionToken('扩展配对已更换;请查询原同步任务,不要自动重新提交。')
136
- const key = createHmac('sha256', pairing.code).update(`writedock-pairing-key:${nonce}`).digest()
137
- const iv = randomBytes(12)
138
- const cipher = createCipheriv('aes-256-gcm', key, iv)
139
- cipher.setAAD(Buffer.from('writedock-pairing-v1'))
140
- const ciphertext = Buffer.concat([cipher.update(JSON.stringify({ token, port: this.port, protocol: 'writedock/1' })), cipher.final(), cipher.getAuthTag()])
141
- clearTimeout(timer)
142
- if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'pairing_result', iv: iv.toString('base64'), ciphertext: ciphertext.toString('base64') }))
143
- ws.close(1000)
144
- })().catch(() => { ws.close(1008, 'Pairing failed') })
145
- })
146
- ws.on('close', () => clearTimeout(timer))
147
- ws.on('error', () => {})
148
- }
149
-
150
105
  private accept(ws: WebSocket) {
151
106
  const nonce = randomBytes(32).toString('hex')
152
107
  const token = this.extensionToken
153
108
  let authenticated = false
154
- const timer = setTimeout(() => ws.close(1008, 'Pairing timed out'), 8000)
109
+ const timer = setTimeout(() => ws.close(1008, 'Handshake timed out'), 8000)
155
110
  ws.send(JSON.stringify({ type: 'challenge', nonce, proof: proof(token, 'server', nonce) }))
156
111
  ws.on('message', raw => {
157
112
  try {
158
113
  const message = JSON.parse(raw.toString())
159
114
  if (!authenticated) {
160
115
  if (token !== this.extensionToken || message.type !== 'hello' || typeof message.proof !== 'string' || !equal(message.proof, proof(token, 'extension', nonce))) {
161
- ws.close(1008, 'Pairing failed'); return
116
+ ws.close(1008, 'Handshake failed'); return
162
117
  }
163
118
  if (this.client?.readyState === WebSocket.OPEN) { ws.close(1008, 'An extension is already connected'); return }
164
119
  authenticated = true; clearTimeout(timer)
@@ -234,9 +189,8 @@ export class LocalBridge {
234
189
  this.seenNonces.set(nonce, now + 30_000)
235
190
  responseNonce = nonce
236
191
  if (req.method === 'GET' && req.url === '/status') {
237
- send(200, { connected: this.client?.readyState === WebSocket.OPEN, extensionId: this.extensionId, protocol: 'writedock/1', pairing_available: true, ...mcpReleaseInfo() }); return
192
+ send(200, { connected: this.client?.readyState === WebSocket.OPEN, extensionId: this.extensionId, protocol: 'writedock/1', ...mcpReleaseInfo() }); return
238
193
  }
239
- if (req.method === 'POST' && req.url === '/pairing') { send(200, await this.beginPairing()); return }
240
194
  if (req.method === 'POST' && req.url === '/assets/register') {
241
195
  const input = JSON.parse(body)
242
196
  if (typeof input.path !== 'string' || !['image', 'video'].includes(input.kind)) throw new Error('素材参数不正确')
@@ -269,14 +223,14 @@ export class LocalBridge {
269
223
 
270
224
  async status() {
271
225
  await this.start()
272
- if (this.primary) return { connected: this.client?.readyState === WebSocket.OPEN, extensionId: this.extensionId, protocol: 'writedock/1', pairing_available: true, ...mcpReleaseInfo() }
226
+ if (this.primary) return { connected: this.client?.readyState === WebSocket.OPEN, extensionId: this.extensionId, protocol: 'writedock/1', ...mcpReleaseInfo() }
273
227
  try { return await this.httpCall('/status') }
274
228
  catch (error) {
275
229
  if (!(error instanceof TypeError)) throw error
276
230
  // Read-only health checks can acquire the vacant port after another MCP client exits.
277
231
  this.starting = undefined
278
232
  await this.start()
279
- if (this.primary) return { connected: false, protocol: 'writedock/1', pairing_available: true, ...mcpReleaseInfo() }
233
+ if (this.primary) return { connected: false, protocol: 'writedock/1', ...mcpReleaseInfo() }
280
234
  throw error
281
235
  }
282
236
  }
@@ -299,7 +253,7 @@ export class LocalBridge {
299
253
  }
300
254
 
301
255
  private async requestConnected(method: string, params: Record<string, unknown>): Promise<unknown> {
302
- if (this.client?.readyState !== WebSocket.OPEN) throw new Error('扩展未连接。请让用户打开浏览器的 WriteDock 扩展,在「连接」页点“连接”。无需重装 MCP。')
256
+ if (this.client?.readyState !== WebSocket.OPEN) throw new Error('扩展未连接,这次没有发布。请让用户打开 Chrome 或 Edge,连接过的扩展通常会自动连上,然后再试;如果还是不行,打开 WriteDock 扩展点“连接”。无需重装 MCP。')
303
257
  const id = randomUUID()
304
258
  return new Promise((resolve, reject) => {
305
259
  const timer = setTimeout(() => {
@@ -315,7 +269,6 @@ export class LocalBridge {
315
269
  }
316
270
 
317
271
  async close() {
318
- this.pairing = undefined
319
272
  clearInterval(this.heartbeat)
320
273
  for (const value of this.pending.values()) { clearTimeout(value.timer); value.reject(new Error('MCP 服务已关闭')) }
321
274
  this.pending.clear()
@@ -20,7 +20,7 @@ export async function loadIdentity(port: number, legacyToken?: string) {
20
20
  const filename = join(directory, `identity-${port}.json`)
21
21
  const read = async (): Promise<Identity> => {
22
22
  const value = JSON.parse(await readFile(filename, 'utf8'))
23
- if (value.version !== 1 || !validToken(value.token) || !validToken(value.extensionToken)) throw new Error('MCP 配对资料格式不正确;请保留原文件并检查备份,不会自动覆盖')
23
+ if (value.version !== 1 || !validToken(value.token) || !validToken(value.extensionToken)) throw new Error('MCP 连接资料格式不正确;请保留原文件并检查备份,不会自动覆盖')
24
24
  return value
25
25
  }
26
26
  let identity: Identity
@@ -43,7 +43,7 @@ export async function loadIdentity(port: number, legacyToken?: string) {
43
43
  token: identity.token,
44
44
  getExtensionToken: async () => (await read()).extensionToken,
45
45
  setExtensionToken: async (extensionToken: string) => {
46
- if (!validToken(extensionToken)) throw new Error('扩展配对密钥格式不正确')
46
+ if (!validToken(extensionToken)) throw new Error('扩展连接密钥格式不正确')
47
47
  const current = await read()
48
48
  if (current.token !== identity.token) throw new Error('MCP 身份已变化,请重新启动服务')
49
49
  const temporary = join(directory, `.identity-${randomUUID()}.tmp`)
@@ -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。人工接管不代表已经发布。'
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;用户不同意就保留旧内容,告诉用户这个平台没有发。'
30
30
 
31
31
  export function createMcpServer(bridge: LocalBridge) {
32
32
  const server = new McpServer({ name: 'writedock', version: mcpPackage.version }, {
@@ -40,10 +40,6 @@ export function createMcpServer(bridge: LocalBridge) {
40
40
  server.registerTool('get_connection_status', {
41
41
  description: '检查本机 MCP 与扩展的连接、运行版本和 npm 更新提示。update_available=true 时告知用户重新启动 WriteDock MCP,保留配置;发布任务进行中不要自动重启。旧版服务可能不返回版本字段;latest_version 缺失或 update_checking=true 时,不得声称已是最新版。', inputSchema: {}, annotations: { ...readOnly, openWorldHint: true },
42
42
  }, () => wrap(() => bridge.status()))
43
- server.registerTool('begin_pairing', {
44
- description: '通常不需要:未连接时,让用户打开 WriteDock 扩展的「连接」页点“连接”即可。只有用户使用旧版扩展时才生成 3 分钟有效的一次性配对码。',
45
- inputSchema: {}, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
46
- }, () => wrap(() => bridge.beginPairing()))
47
43
  server.registerTool('list_platforms', {
48
44
  description: '列出扩展实际支持的平台、登录状态、publishModes、对应 tools 及 modeOptions 限制;小红书 article 对应 publish_article,image/video 对应 publish_post。不会返回 Cookie。',
49
45
  inputSchema: { force_refresh: z.boolean().optional() }, annotations: { ...readOnly, openWorldHint: true },
@@ -96,9 +92,9 @@ export function createMcpServer(bridge: LocalBridge) {
96
92
  }
97
93
  server.registerTool('publish', {
98
94
  description: agentWorkflow + '首选的发布工具:一次调用发布到用户指定的多个平台。只有用户明确要求发布并指定平台时才调用。按平台依次执行,某个平台未登录、不能接收内容或失败,只跳过该平台,其他平台继续。遇到验证码时平台进入 waiting_verification,立即把原因和链接告诉用户,由用户手动完成,继续查询即可,不要重发。任务结束后按 get_sync_task 的 summary 逐平台告诉用户:哪些成功,哪些未登录(not_logged_in)等。立即返回 task_id,再用 get_sync_task 查询;results 是逐平台结果,warnings 说明为平台做的调整。必须使用稳定 request_id,相同 ID 不会重复提交。部分平台失败时,只针对失败平台用新 request_id 重新提交,unknown 的平台不要重发。旧版扩展不支持时会报错,不会发布。',
99
- inputSchema: { ...unifiedInput, request_id: requestId },
95
+ inputSchema: { ...unifiedInput, request_id: requestId, discard_old_drafts: z.array(z.enum(['douyin'])).optional().describe('只有用户明确同意放弃平台上次没发完的内容时才传,如 ["douyin"]。放弃后无法恢复。') },
100
96
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
101
- }, args => wrap(async () => bridge.request('publish', { requestId: args.request_id, ...await unifiedParams(args) })))
97
+ }, args => wrap(async () => bridge.request('publish', { requestId: args.request_id, ...(args.discard_old_drafts ? { discardOldDrafts: args.discard_old_drafts } : {}), ...await unifiedParams(args) })))
102
98
  server.registerTool('rehearse_publish', {
103
99
  description: '与 publish 参数相同的演练:逐平台打开原生页面,填好内容、上传素材,走到点击发布之前停下,不提交发布。无需 request_id,不影响之后的真实发布。图片可能已上传到平台,部分平台会留下中间草稿,公众号、搜狐号不参与。结果 publicationState=rehearsed 不代表已发布,不得告诉用户内容已发出,也不要自行接着发布。',
104
100
  inputSchema: unifiedInput,