writedock-mcp 0.4.6 → 0.4.7

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.7",
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-BEVXnN62.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-ClIT9WYa.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,4 +1,4 @@
1
- WriteDock 安装与更新 v0.4.6
1
+ WriteDock 安装与更新 v0.4.7
2
2
 
3
3
  首次使用(电脑需要 Node.js 22+)
4
4
  1. 把这句话发给你的 agent:帮我安装 WriteDock:运行 npx -y writedock-mcp@latest setup,然后告诉我怎么重启你。
package/index.cjs CHANGED
@@ -29739,7 +29739,7 @@ var MediaStore = class {
29739
29739
  // package.json
29740
29740
  var package_default = {
29741
29741
  name: "@writedock/mcp-server",
29742
- version: "0.4.6",
29742
+ version: "0.4.7",
29743
29743
  private: true,
29744
29744
  type: "module",
29745
29745
  description: "WriteDock local MCP server for the browser extension",
@@ -29953,7 +29953,6 @@ var LocalBridge = class {
29953
29953
  primary = false;
29954
29954
  extensionId;
29955
29955
  seenNonces = /* @__PURE__ */ new Map();
29956
- pairing;
29957
29956
  extensionToken;
29958
29957
  media;
29959
29958
  pending = /* @__PURE__ */ new Map();
@@ -29982,14 +29981,13 @@ var LocalBridge = class {
29982
29981
  server.on("upgrade", (req, socket, head) => {
29983
29982
  const origin = req.headers.origin;
29984
29983
  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) {
29984
+ if (!this.validHost(req.headers.host) || !["/extension", "/connect"].includes(req.url || "") || origin && !extensionOrigin || req.url === "/connect" && origin !== EXTENSION_ORIGIN) {
29986
29985
  socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
29987
29986
  socket.destroy();
29988
29987
  return;
29989
29988
  }
29990
29989
  sockets.handleUpgrade(req, socket, head, (ws) => {
29991
- if (req.url === "/pair") this.acceptPairing(ws);
29992
- else if (req.url === "/connect") void this.acceptConnect(ws);
29990
+ if (req.url === "/connect") void this.acceptConnect(ws);
29993
29991
  else this.accept(ws);
29994
29992
  });
29995
29993
  });
@@ -30011,16 +30009,6 @@ var LocalBridge = class {
30011
30009
  validHost(host) {
30012
30010
  return host === `127.0.0.1:${this.port}` || host === `localhost:${this.port}`;
30013
30011
  }
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
30012
  /** Replaces the extension credential and drops whatever was using the old one. */
30025
30013
  async replaceExtensionToken(reason) {
30026
30014
  const token = (0, import_node_crypto2.randomBytes)(32).toString("hex");
@@ -30034,7 +30022,7 @@ var LocalBridge = class {
30034
30022
  value.reject(new Error(reason));
30035
30023
  }
30036
30024
  this.pending.clear();
30037
- previousClient?.close(1e3, "Extension pairing replaced");
30025
+ previousClient?.close(1e3, "Extension connection replaced");
30038
30026
  return token;
30039
30027
  }
30040
30028
  /** One-click connection: only this extension's fixed origin can reach here, and the user has just clicked 连接. */
@@ -30047,55 +30035,18 @@ var LocalBridge = class {
30047
30035
  ws.close(1011, "Connect failed");
30048
30036
  }
30049
30037
  }
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
30038
  accept(ws) {
30088
30039
  const nonce = (0, import_node_crypto2.randomBytes)(32).toString("hex");
30089
30040
  const token = this.extensionToken;
30090
30041
  let authenticated = false;
30091
- const timer = setTimeout(() => ws.close(1008, "Pairing timed out"), 8e3);
30042
+ const timer = setTimeout(() => ws.close(1008, "Handshake timed out"), 8e3);
30092
30043
  ws.send(JSON.stringify({ type: "challenge", nonce, proof: proof(token, "server", nonce) }));
30093
30044
  ws.on("message", (raw) => {
30094
30045
  try {
30095
30046
  const message = JSON.parse(raw.toString());
30096
30047
  if (!authenticated) {
30097
30048
  if (token !== this.extensionToken || message.type !== "hello" || typeof message.proof !== "string" || !equal(message.proof, proof(token, "extension", nonce))) {
30098
- ws.close(1008, "Pairing failed");
30049
+ ws.close(1008, "Handshake failed");
30099
30050
  return;
30100
30051
  }
30101
30052
  if (this.client?.readyState === import_websocket.default.OPEN) {
@@ -30188,11 +30139,7 @@ var LocalBridge = class {
30188
30139
  this.seenNonces.set(nonce, now + 3e4);
30189
30140
  responseNonce = nonce;
30190
30141
  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());
30142
+ send(200, { connected: this.client?.readyState === import_websocket.default.OPEN, extensionId: this.extensionId, protocol: "writedock/1", ...mcpReleaseInfo() });
30196
30143
  return;
30197
30144
  }
30198
30145
  if (req.method === "POST" && req.url === "/assets/register") {
@@ -30231,14 +30178,14 @@ var LocalBridge = class {
30231
30178
  }
30232
30179
  async status() {
30233
30180
  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() };
30181
+ if (this.primary) return { connected: this.client?.readyState === import_websocket.default.OPEN, extensionId: this.extensionId, protocol: "writedock/1", ...mcpReleaseInfo() };
30235
30182
  try {
30236
30183
  return await this.httpCall("/status");
30237
30184
  } catch (error2) {
30238
30185
  if (!(error2 instanceof TypeError)) throw error2;
30239
30186
  this.starting = void 0;
30240
30187
  await this.start();
30241
- if (this.primary) return { connected: false, protocol: "writedock/1", pairing_available: true, ...mcpReleaseInfo() };
30188
+ if (this.primary) return { connected: false, protocol: "writedock/1", ...mcpReleaseInfo() };
30242
30189
  throw error2;
30243
30190
  }
30244
30191
  }
@@ -30275,7 +30222,6 @@ var LocalBridge = class {
30275
30222
  });
30276
30223
  }
30277
30224
  async close() {
30278
- this.pairing = void 0;
30279
30225
  clearInterval(this.heartbeat);
30280
30226
  for (const value of this.pending.values()) {
30281
30227
  clearTimeout(value.timer);
@@ -50396,11 +50342,6 @@ function createMcpServer(bridge) {
50396
50342
  inputSchema: {},
50397
50343
  annotations: { ...readOnly, openWorldHint: true }
50398
50344
  }, () => 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
50345
  server.registerTool("list_platforms", {
50405
50346
  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
50347
  inputSchema: { force_refresh: external_exports.boolean().optional() },
@@ -50529,7 +50470,7 @@ async function loadIdentity(port, legacyToken) {
50529
50470
  const filename = (0, import_node_path4.join)(directory, `identity-${port}.json`);
50530
50471
  const read = async () => {
50531
50472
  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");
50473
+ 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
50474
  return value;
50534
50475
  };
50535
50476
  let identity;
@@ -50557,7 +50498,7 @@ async function loadIdentity(port, legacyToken) {
50557
50498
  token: identity.token,
50558
50499
  getExtensionToken: async () => (await read()).extensionToken,
50559
50500
  setExtensionToken: async (extensionToken) => {
50560
- if (!validToken(extensionToken)) throw new Error("\u6269\u5C55\u914D\u5BF9\u5BC6\u94A5\u683C\u5F0F\u4E0D\u6B63\u786E");
50501
+ if (!validToken(extensionToken)) throw new Error("\u6269\u5C55\u8FDE\u63A5\u5BC6\u94A5\u683C\u5F0F\u4E0D\u6B63\u786E");
50561
50502
  const current = await read();
50562
50503
  if (current.token !== identity.token) throw new Error("MCP \u8EAB\u4EFD\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u542F\u52A8\u670D\u52A1");
50563
50504
  const temporary = (0, import_node_path4.join)(directory, `.identity-${(0, import_node_crypto3.randomUUID)()}.tmp`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "writedock-mcp",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
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.7",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "WriteDock local MCP server for the browser extension",
@@ -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
  }
@@ -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`)
@@ -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 },
@@ -1,76 +0,0 @@
1
- import{p as he}from"./purify.es-1yEQDPgc.js";import{r as i,j as e,c as ue}from"./react-vendor-BrXlnJmF.js";import{S as H,s as V,e as xe}from"./task-utils-DwzGcJkm.js";/**
2
- * @license lucide-react v0.303.0 - ISC
3
- *
4
- * This source code is licensed under the ISC license.
5
- * See the LICENSE file in the root directory of this source tree.
6
- */var pe={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};/**
7
- * @license lucide-react v0.303.0 - ISC
8
- *
9
- * This source code is licensed under the ISC license.
10
- * See the LICENSE file in the root directory of this source tree.
11
- */const me=t=>t.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase().trim(),j=(t,o)=>{const l=i.forwardRef(({color:E="currentColor",size:d=24,strokeWidth:N=2,absoluteStrokeWidth:m,className:g="",children:x,...C},c)=>i.createElement("svg",{ref:c,...pe,width:d,height:d,stroke:E,strokeWidth:m?Number(N)*24/Number(d):N,className:["lucide",`lucide-${me(t)}`,g].join(" "),...C},[...o.map(([f,u])=>i.createElement(f,u)),...Array.isArray(x)?x:[x]]));return l.displayName=`${t}`,l};/**
12
- * @license lucide-react v0.303.0 - ISC
13
- *
14
- * This source code is licensed under the ISC license.
15
- * See the LICENSE file in the root directory of this source tree.
16
- */const Q=j("Check",[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]]);/**
17
- * @license lucide-react v0.303.0 - ISC
18
- *
19
- * This source code is licensed under the ISC license.
20
- * See the LICENSE file in the root directory of this source tree.
21
- */const O=j("Copy",[["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2",key:"17jyea"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",key:"zix9uf"}]]);/**
22
- * @license lucide-react v0.303.0 - ISC
23
- *
24
- * This source code is licensed under the ISC license.
25
- * See the LICENSE file in the root directory of this source tree.
26
- */const q=j("ExternalLink",[["path",{d:"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6",key:"a6xqqp"}],["polyline",{points:"15 3 21 3 21 9",key:"mznyad"}],["line",{x1:"10",x2:"21",y1:"14",y2:"3",key:"18c3s4"}]]);/**
27
- * @license lucide-react v0.303.0 - ISC
28
- *
29
- * This source code is licensed under the ISC license.
30
- * See the LICENSE file in the root directory of this source tree.
31
- */const je=j("Eye",[["path",{d:"M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z",key:"rwhkz3"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]]);/**
32
- * @license lucide-react v0.303.0 - ISC
33
- *
34
- * This source code is licensed under the ISC license.
35
- * See the LICENSE file in the root directory of this source tree.
36
- */const U=j("FileText",[["path",{d:"M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z",key:"1nnpy2"}],["polyline",{points:"14 2 14 8 20 8",key:"1ew0cm"}],["line",{x1:"16",x2:"8",y1:"13",y2:"13",key:"14keom"}],["line",{x1:"16",x2:"8",y1:"17",y2:"17",key:"17nazh"}],["line",{x1:"10",x2:"8",y1:"9",y2:"9",key:"1a5vjj"}]]);/**
37
- * @license lucide-react v0.303.0 - ISC
38
- *
39
- * This source code is licensed under the ISC license.
40
- * See the LICENSE file in the root directory of this source tree.
41
- */const G=j("History",[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"1357e3"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}],["path",{d:"M12 7v5l4 2",key:"1fdv2h"}]]);/**
42
- * @license lucide-react v0.303.0 - ISC
43
- *
44
- * This source code is licensed under the ISC license.
45
- * See the LICENSE file in the root directory of this source tree.
46
- */const fe=j("Info",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 16v-4",key:"1dtifu"}],["path",{d:"M12 8h.01",key:"e9boi3"}]]);/**
47
- * @license lucide-react v0.303.0 - ISC
48
- *
49
- * This source code is licensed under the ISC license.
50
- * See the LICENSE file in the root directory of this source tree.
51
- */const W=j("Link2",[["path",{d:"M9 17H7A5 5 0 0 1 7 7h2",key:"8i5ue5"}],["path",{d:"M15 7h2a5 5 0 1 1 0 10h-2",key:"1b9ql8"}],["line",{x1:"8",x2:"16",y1:"12",y2:"12",key:"1jonct"}]]);/**
52
- * @license lucide-react v0.303.0 - ISC
53
- *
54
- * This source code is licensed under the ISC license.
55
- * See the LICENSE file in the root directory of this source tree.
56
- */const K=j("Loader2",[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56",key:"13zald"}]]);/**
57
- * @license lucide-react v0.303.0 - ISC
58
- *
59
- * This source code is licensed under the ISC license.
60
- * See the LICENSE file in the root directory of this source tree.
61
- */const Y=j("RefreshCw",[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]]);/**
62
- * @license lucide-react v0.303.0 - ISC
63
- *
64
- * This source code is licensed under the ISC license.
65
- * See the LICENSE file in the root directory of this source tree.
66
- */const ye=j("Trash2",[["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6",key:"4alrt4"}],["path",{d:"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2",key:"v07s0e"}],["line",{x1:"10",x2:"10",y1:"11",y2:"17",key:"1uufr5"}],["line",{x1:"14",x2:"14",y1:"11",y2:"17",key:"xtxkd"}]]);/**
67
- * @license lucide-react v0.303.0 - ISC
68
- *
69
- * This source code is licensed under the ISC license.
70
- * See the LICENSE file in the root directory of this source tree.
71
- */const ve=j("Users",[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",key:"1yyitq"}],["circle",{cx:"9",cy:"7",r:"4",key:"nufk8"}],["path",{d:"M22 21v-2a4 4 0 0 0-3-3.87",key:"kshegd"}],["path",{d:"M16 3.13a4 4 0 0 1 0 7.75",key:"1da9ce"}]]);/**
72
- * @license lucide-react v0.303.0 - ISC
73
- *
74
- * This source code is licensed under the ISC license.
75
- * See the LICENSE file in the root directory of this source tree.
76
- */const ee=j("X",[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]]);async function ke(){return(await chrome.storage.local.get(H.draftIndex))[H.draftIndex]||[]}async function Ne(t){const o=`writedockDraft:${t}`;return(await chrome.storage.local.get(o))[o]||null}async function ge(t){return T("DELETE_DRAFT",{draftId:t})}async function T(t,o={}){const l=await chrome.runtime.sendMessage({type:t,...o});if(l!=null&&l.error)throw new Error(l.error);if(!l||!("data"in l))throw new Error("扩展后台未响应,请在扩展管理页重新加载 WriteDock");return l.data}function we({bridge:t,busy:o,syncing:l,update:E,pair:d,connect:N,updateExtension:m,copy:g}){const[x,C]=i.useState(String(t.port)),[c,f]=i.useState(t.serverPath),[u,b]=i.useState("");i.useEffect(()=>C(String(t.port)),[t.port]),i.useEffect(()=>f(t.serverPath),[t.serverPath]);const w="帮我安装 WriteDock:运行 npx -y writedock-mcp@latest setup,然后告诉我怎么重启你。",y={WRITEDOCK_PORT:String(t.port)},S=()=>JSON.stringify({mcpServers:{writedock:{command:"node",args:[t.serverPath],env:y}}},null,2),I=[t.mcpVersion?`服务版本 v${t.mcpVersion}`:"",t.connected&&t.mcpLatestVersion?`最新 v${t.mcpLatestVersion}`:""].filter(Boolean).join(" · ");return e.jsxs("div",{className:"connection-page",children:[t.connected?e.jsxs("section",{className:"panel",children:[e.jsx("div",{className:"panel-heading",children:e.jsxs("div",{children:[e.jsxs("h2",{children:[e.jsx(Q,{size:16})," 已连接"]}),e.jsxs("p",{children:["告诉你的 agent 要写什么、发到哪些平台就行。",I&&` ${I}`]})]})}),t.mcpUpdateAvailable&&e.jsx("p",{className:"help-text",role:"status",children:"有新版本。等发布结束后重启你的 agent 就会更新,不用重新连接。"})]}):t.mcpFound?e.jsxs("section",{className:"panel",children:[e.jsx("h2",{children:"发现本机的 WriteDock 服务"}),e.jsxs("p",{className:"help-text",children:[t.mcpFoundVersion?`服务版本 v${t.mcpFoundVersion}。`:"","点一下就能连上,以后会自动连接。"]}),e.jsx("div",{className:"settings-actions",children:e.jsx("button",{className:"primary-button",disabled:o||l,onClick:()=>void N(),children:o?"正在连接…":"连接"})})]}):e.jsxs("section",{className:"panel",children:[e.jsx("h2",{children:"把这句话发给你的 agent"}),e.jsx("p",{className:"help-text",children:"它会帮你装好服务。需要电脑上有 Node.js 22 或更高版本。"}),e.jsx("pre",{tabIndex:0,className:"setup-sentence",children:w}),e.jsx("div",{className:"settings-actions",children:e.jsxs("button",{className:"primary-button",onClick:()=>void g(w),children:[e.jsx(O,{size:14}),"复制这句话"]})}),e.jsx("p",{className:"help-text",children:"agent 重启后,这里会出现“连接”按钮。"})]}),t.connected&&t.extensionUpdateVersion&&e.jsxs("section",{className:"panel",children:[e.jsxs("h2",{children:["扩展有新版本 v",t.extensionUpdateVersion]}),e.jsx("p",{className:"help-text",children:"点一下就能更新,连接和稿件都会保留。"}),e.jsx("div",{className:"settings-actions",children:e.jsx("button",{className:"primary-button",disabled:o||l,onClick:()=>void m(),children:o?"正在更新…":"更新扩展"})}),l&&e.jsx("p",{className:"help-text",children:"发布结束后才能更新。"})]}),e.jsxs("section",{className:"panel compact",children:[e.jsx("h2",{children:"怎么用"}),e.jsxs("ol",{children:[e.jsx("li",{children:"在「平台账号」里确认要发布的平台已登录。"}),e.jsx("li",{children:"告诉 agent 写什么、发到哪些平台。"}),e.jsx("li",{children:"遇到验证码时,agent 会告诉你去哪个页面完成。"})]})]}),e.jsxs("details",{className:"panel developer-settings",children:[e.jsx("summary",{children:"高级设置"}),e.jsx("p",{className:"help-text",children:"通常不需要动这里。"}),e.jsxs("label",{children:["启用自动连接",e.jsx("button",{role:"switch","aria-label":"启用 MCP 连接","aria-checked":t.enabled,className:`switch ${t.enabled?"on":""}`,disabled:o||l,onClick:()=>E({enabled:!t.enabled}),children:e.jsx("span",{})})]}),e.jsxs("form",{onSubmit:v=>{v.preventDefault(),!o&&!l&&u.trim()&&d(u).then(h=>{h&&b("")})},children:[e.jsxs("label",{children:["一次性配对码(仅旧版服务需要)",e.jsx("input",{"aria-label":"一次性配对码",value:u,onChange:v=>b(v.target.value),autoComplete:"off",spellCheck:!1,placeholder:"WD1-9647-XXXX-XXXX-XXXX-XXXX",disabled:o||l})]}),e.jsx("div",{className:"settings-actions",children:e.jsx("button",{className:"secondary-button",type:"submit",disabled:o||l||!u.trim(),children:o?"正在配对…":"配对并连接"})})]}),e.jsxs("div",{className:"settings-grid",children:[e.jsxs("label",{children:["连接端口",e.jsx("input",{"aria-label":"连接端口",inputMode:"numeric",value:x,onChange:v=>C(v.target.value)})]}),e.jsxs("label",{children:["当前浏览器配对密钥",e.jsxs("div",{className:"input-action",children:[e.jsx("input",{"aria-label":"连接密钥",type:"password",readOnly:!0,value:t.token}),e.jsx("button",{className:"icon-button","aria-label":"复制连接密钥",onClick:()=>g(t.token),children:e.jsx(O,{size:14})})]})]})]}),e.jsx("div",{className:"settings-actions",children:e.jsx("button",{className:"secondary-button",disabled:o||l||x===String(t.port),onClick:()=>E({port:Number(x)}),children:"保存端口"})}),e.jsxs("label",{className:"path-setting",children:["本机开发服务路径",e.jsx("input",{"aria-label":"本机开发服务路径",placeholder:"已构建的 packages/mcp-server/dist/index.cjs 路径",value:c,onChange:v=>f(v.target.value)})]}),e.jsxs("div",{className:"settings-actions",children:[e.jsx("button",{className:"secondary-button",disabled:o||c===t.serverPath,onClick:()=>E({serverPath:c}),children:"保存开发路径"}),e.jsx("button",{className:"secondary-button",disabled:!t.serverPath||c!==t.serverPath,onClick:()=>g(S()),children:"复制本机开发配置"})]})]})]})}const Z=t=>new Intl.DateTimeFormat("zh-CN",{month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",hour12:!1}).format(t),L=t=>t.trim()||"未命名稿件",be={syncing:"正在保存",completed:"已保存草稿",partial:"部分成功",failed:"失败",interrupted:"中断了",unknown:"请去平台确认",needs_user_action:"需要你处理",rehearsed:"演练完成"},se={syncing:"正在发布",completed:"已发布",partial:"部分成功",failed:"发布失败",interrupted:"中断了",unknown:"请去平台确认",needs_user_action:"需要你处理",rehearsed:"演练完成"},Ee={...se,syncing:"正在演练",completed:"演练完成",partial:"部分通过",failed:"演练失败"},Ce={pending:"排队中",checking_auth:"检查登录",uploading_images:"上传图片",transferring_media:"读取素材",uploading_media:"上传素材",preparing_article:"准备内容",publishing:"发布中",verifying:"等待平台确认",saving:"保存草稿",completed:"完成",failed:"失败",unknown:"请去平台确认",needs_user_action:"需要你处理",waiting_verification:"等你完成验证"},Te=t=>(t.videos||[]).reduce((o,l)=>o.replace(l.marker,`视频:${xe(l.asset.name)}`),t.html),Se=t=>t.publicationState==="rehearsed"?"演练通过,未发布":t.errorCode==="not_logged_in"?"未登录,已跳过":t.publicationState==="needs_user_action"?"需要你处理":t.publicationState==="unknown"?"请去平台确认":t.success?t.draftOnly===!1?"已发布":"已保存草稿":"失败";function J({title:t,children:o,close:l,wide:E=!1}){const d=i.useRef(null),N=i.useRef(l);return N.current=l,i.useEffect(()=>{var x,C;const m=document.activeElement;(C=(x=d.current)==null?void 0:x.querySelector("button"))==null||C.focus();const g=c=>{var w;if(c.key==="Escape"){c.preventDefault(),N.current();return}if(c.key!=="Tab")return;const f=Array.from(((w=d.current)==null?void 0:w.querySelectorAll('button:not(:disabled), a[href], input, [tabindex="0"]'))||[]),u=f[0],b=f.at(-1);c.shiftKey&&document.activeElement===u?(c.preventDefault(),b==null||b.focus()):!c.shiftKey&&document.activeElement===b&&(c.preventDefault(),u==null||u.focus())};return document.addEventListener("keydown",g),()=>{document.removeEventListener("keydown",g),m==null||m.focus()}},[]),e.jsx("div",{className:"modal-backdrop",onMouseDown:m=>{m.target===m.currentTarget&&l()},children:e.jsxs("section",{className:`modal ${E?"wide":""}`,ref:d,role:"dialog","aria-modal":"true","aria-label":t,children:[e.jsxs("header",{children:[e.jsx("h2",{children:t}),e.jsx("button",{className:"icon-button","aria-label":"关闭弹窗",onClick:l,children:e.jsx(ee,{size:18})})]}),o]})})}function _e(){const[t,o]=i.useState("tasks"),[l,E]=i.useState([]),[d,N]=i.useState(null),[m,g]=i.useState([]),[x,C]=i.useState([]),[c,f]=i.useState(null),[u,b]=i.useState(!1),[w,y]=i.useState(!1),[S,I]=i.useState(!1),[v,h]=i.useState(""),[D,P]=i.useState(null),[X,z]=i.useState(null),R=i.useRef(!1),_=i.useCallback(async()=>{const[s,r,n]=await Promise.all([ke(),T("GET_SYNC_STATE"),T("GET_SYNC_HISTORY")]);E(s),N(r),g(n)},[]),A=i.useCallback(async()=>{const s=await T("GET_BRIDGE_STATE");return f(s),s},[]),$=i.useCallback(async(s=!1)=>{b(!0);try{C(await T("CHECK_ALL_AUTH",{forceRefresh:s}))}catch(r){h(r instanceof Error?r.message:"登录检测失败")}finally{b(!1)}},[]);i.useEffect(()=>{let s=!1;Promise.all([_(),A()]).then(([,a])=>{s||(a.connected||o("connection"),I(!0),$())}).catch(a=>h(String(a.message||a)));const r=a=>(a.type==="TASK_UPDATED"&&a.task&&(N(a.task),a.task.status!=="syncing"&&_().catch(()=>{})),["DRAFT_SAVED","DRAFT_DELETED"].includes(a.type)&&(a.type==="DRAFT_DELETED"&&(P(p=>(p==null?void 0:p.id)===a.draftId?null:p),N(p=>(p==null?void 0:p.draftId)===a.draftId?null:p),g(p=>p.filter(k=>k.draftId!==a.draftId))),_().catch(()=>{})),a.type==="BRIDGE_STATUS_UPDATED"&&A().catch(()=>{}),!1);chrome.runtime.onMessage.addListener(r);const n=()=>{R.current&&(R.current=!1,$(!0))};return window.addEventListener("focus",n),()=>{s=!0,chrome.runtime.onMessage.removeListener(r),window.removeEventListener("focus",n)}},[_,A,$]),i.useEffect(()=>{if(t!=="connection"||c!=null&&c.connected)return;const s=setInterval(()=>{A().catch(()=>{})},3e3);return()=>clearInterval(s)},[t,c==null?void 0:c.connected,A]),i.useEffect(()=>{if(!v)return;const s=setTimeout(()=>h(""),7e3);return()=>clearTimeout(s)},[v]);const te=async s=>{y(!0);try{f(await T("SET_BRIDGE_CONFIG",{payload:s}))}catch(r){h(r instanceof Error?r.message:"连接设置保存失败")}finally{y(!1)}},ae=async()=>{y(!0);try{return f(await T("CONNECT_BRIDGE")),!0}catch(s){return h(s instanceof Error?s.message:"连接失败"),!1}finally{y(!1)}},ne=async()=>{y(!0);try{await T("UPDATE_EXTENSION"),h("更新完成,正在重新加载")}catch(s){h(s instanceof Error?s.message:"更新失败")}finally{y(!1)}},ce=async s=>{y(!0);try{return f(await T("PAIR_BRIDGE",{code:s})),h("正在连接"),!0}catch(r){return h(r instanceof Error?r.message:"配对失败"),!1}finally{y(!1)}},F=(s,r=!1)=>{const n=V(s);n&&(async()=>{if(r){const a=new URL(n),k=(await chrome.tabs.query({})).find(de=>{try{const M=new URL(de.url||"");return M.origin===a.origin&&M.pathname===a.pathname&&(a.searchParams.has("article_id")?M.searchParams.get("article_id")===a.searchParams.get("article_id"):a.hash?M.hash===a.hash:M.href===a.href)}catch{return!1}});if(k!=null&&k.id){await chrome.tabs.update(k.id,{active:!0}),await chrome.windows.update(k.windowId,{focused:!0});return}}await chrome.tabs.create({url:n})})().catch(a=>h(a instanceof Error?a.message:"无法打开稿件页"))},re=async s=>{try{await navigator.clipboard.writeText(s),h("已复制")}catch{h("复制失败,请选中文字后复制")}},B=d?[d,...m.filter(s=>s.id!==d.id)]:m,ie=s=>{var r;return((r=x.find(n=>n.id===s))==null?void 0:r.name)||s},le=x.filter(s=>s.isAuthenticated).length,oe=s=>e.jsxs("section",{className:"task-card","aria-label":`任务:${s.title}`,children:[e.jsxs("div",{className:"task-title",children:[e.jsxs("div",{children:[e.jsx("h3",{children:L(s.title)}),e.jsx("span",{children:Z(s.startedAt)})]}),e.jsxs("span",{className:`task-status ${s.status}`,children:[s.status==="syncing"&&e.jsx(K,{size:13,className:"spin"}),(s.action==="rehearsal"?Ee:s.action==="publish"?se:be)[s.status]]})]}),e.jsx("div",{className:"task-result-list",children:s.platforms.map(r=>{var p;const n=s.results.find(k=>k.platform===r),a=s.progress[r];return e.jsxs("div",{className:"task-result",children:[e.jsxs("div",{children:[e.jsx("strong",{children:ie(r)}),e.jsxs("span",{className:(n==null?void 0:n.publicationState)==="needs_user_action"||(a==null?void 0:a.stage)==="waiting_verification"?"manual-text":n!=null&&n.success||(n==null?void 0:n.publicationState)==="rehearsed"?"success-text":n?"error-text":"",children:[n?Se(n):Ce[(a==null?void 0:a.stage)||"pending"],(a==null?void 0:a.stage)==="uploading_images"&&a.total?` ${a.current}/${a.total}`:"",(a==null?void 0:a.stage)==="transferring_media"&&a.total?` ${((a.current||0)/1048576).toFixed(1)} / ${(a.total/1048576).toFixed(1)} MB`:""]}),n&&(n.success||n.draftOnly===!1)&&V(((p=n.userAction)==null?void 0:p.url)||n.postUrl)&&e.jsxs("button",{className:"link-button",onClick:()=>{var k;return F(((k=n.userAction)==null?void 0:k.url)||n.postUrl,n.requiresUserAction===!0)},children:[n.requiresUserAction?"去页面处理":n.publicationState==="unknown"?"去平台确认":n.publicationState==="rehearsed"?"查看页面":n.draftOnly===!1?"查看":"打开草稿",e.jsx(q,{size:12})]})]}),(n==null?void 0:n.error)&&e.jsx("p",{className:"error-text",children:n.error}),(n==null?void 0:n.message)&&!n.success&&e.jsx("p",{children:n.message}),!n&&(a==null?void 0:a.message)&&e.jsx("p",{children:a.message})]},r)})}),s.error&&e.jsx("p",{className:"error-text task-error",children:s.error}),e.jsx("footer",{children:s.action==="rehearsal"?`演练 ${s.results.filter(r=>r.publicationState==="rehearsed").length} / ${s.platforms.length} 个平台通过`:`${s.results.filter(r=>r.success).length} / ${s.platforms.length} 个平台成功`})]},s.id);return e.jsxs("div",{className:"console-shell",children:[e.jsxs("header",{className:"app-header",children:[e.jsxs("div",{className:"brand",children:[e.jsx("img",{src:"/assets/icon-48.png",width:"26",height:"26",alt:""}),e.jsx("strong",{children:"WriteDock"})]}),e.jsxs("button",{className:`connection-indicator ${c!=null&&c.connected?"online":""}`,onClick:()=>o("connection"),children:[e.jsx("span",{}),c!=null&&c.connected?"已连接":"未连接"]})]}),e.jsxs("aside",{className:"navigation",children:[e.jsx("nav",{"aria-label":"控制台导航",children:[["tasks",G,"发布记录"],["accounts",ve,"平台登录"],["articles",U,"稿件"],["connection",W,"连接 agent"]].map(([s,r,n])=>e.jsxs("button",{className:t===s?"selected":"",onClick:()=>o(s),children:[e.jsx(r,{size:17}),n,s==="tasks"&&(d==null?void 0:d.status)==="syncing"&&e.jsx("span",{className:"nav-dot"}),s==="connection"&&((c==null?void 0:c.mcpUpdateAvailable)||(c==null?void 0:c.extensionUpdateVersion))&&e.jsx("span",{className:"nav-dot",title:"有新版本"})]},s))}),e.jsxs("footer",{children:[e.jsx(fe,{size:13}),e.jsxs("span",{children:["v",chrome.runtime.getManifest().version]})]})]}),e.jsxs("main",{className:"console-main",children:[e.jsxs("div",{className:"page-heading",children:[e.jsxs("div",{children:[e.jsx("h1",{children:{articles:"稿件",tasks:"发布记录",accounts:"平台登录",connection:"连接 agent"}[t]}),e.jsx("p",{children:{articles:"agent 写好的内容,可以在这里预览。",tasks:"每次发布到各平台的结果。",accounts:"在这个浏览器里登录要发布的平台。",connection:"连上后,你的 agent 就能把内容发到这个浏览器。"}[t]})]}),t==="articles"&&e.jsxs("button",{className:"secondary-button",onClick:()=>void _().catch(s=>h(s.message)),children:[e.jsx(Y,{size:14}),"刷新"]}),t==="accounts"&&e.jsxs("button",{className:"secondary-button",disabled:u,onClick:()=>$(!0),children:[e.jsx(Y,{size:14,className:u?"spin":""}),"重新检查"]})]}),!S&&e.jsxs("div",{className:"empty-state",children:[e.jsx(K,{className:"spin",size:24}),e.jsx("p",{children:"正在加载…"})]}),S&&t==="articles"&&e.jsxs(e.Fragment,{children:[!(c!=null&&c.connected)&&e.jsxs("div",{className:"connection-banner",children:[e.jsx(W,{size:17}),e.jsx("span",{children:c!=null&&c.mcpFound?"发现本机的 WriteDock 服务,点一下就能连接。":"还没有连接到你的 agent。"}),e.jsx("button",{className:"link-button",onClick:()=>o("connection"),children:"去连接"})]}),l.length===0?e.jsxs("div",{className:"empty-state bordered",children:[e.jsx(U,{size:30}),e.jsx("h2",{children:"还没有稿件"}),e.jsx("p",{children:"告诉你的 agent 要写什么,内容会出现在这里。"})]}):e.jsx("div",{className:"article-list",children:l.map(s=>e.jsxs("article",{className:"article-row",children:[e.jsx(U,{size:19}),e.jsxs("div",{children:[e.jsx("h2",{children:L(s.title)}),e.jsx("p",{children:Z(s.updatedAt)})]}),e.jsxs("button",{className:"secondary-button","aria-label":`预览稿件:${L(s.title)}`,onClick:()=>void Ne(s.id).then(r=>r&&P(r)),children:[e.jsx(je,{size:14}),"预览"]}),e.jsx("button",{className:"icon-button","aria-label":`删除稿件:${L(s.title)}`,disabled:(d==null?void 0:d.status)==="syncing"&&d.draftId===s.id,onClick:()=>z(s),children:e.jsx(ye,{size:15})})]},s.id))}),e.jsx("p",{className:"footnote",children:"稿件只保存在这台电脑上。"})]}),S&&t==="tasks"&&(B.length?e.jsx("div",{className:"task-list",children:B.map(oe)}):e.jsxs("div",{className:"empty-state bordered",children:[e.jsx(G,{size:30}),e.jsx("h2",{children:"还没有发布记录"}),e.jsx("p",{children:"让 agent 发布后,这里会显示每个平台的结果。"})]})),S&&t==="accounts"&&e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"section-summary",children:["已登录 ",le," / ",x.length," 个平台",e.jsx("span",{children:u?"正在检查…":""})]}),e.jsx("div",{className:"account-grid",children:x.map(s=>e.jsxs("section",{className:"account-card",children:[e.jsxs("div",{children:[e.jsx("h2",{children:s.name}),e.jsx("span",{className:s.isAuthenticated?"success-text":"",children:s.isAuthenticated?s.username||"已登录":u&&!s.checked?"检查中…":s.error?"暂时查不到":"未登录"})]}),e.jsx("div",{children:e.jsxs("button",{className:"secondary-button","aria-label":`打开${s.name}`,onClick:()=>{R.current=!0,F(s.homepage)},children:[s.isAuthenticated?"打开":"去登录",e.jsx(q,{size:12})]})})]},s.id))}),e.jsx("p",{className:"footnote",children:"登录后回到这里会自动更新。显示“暂时查不到”时,可以打开平台确认后点“重新检查”。"})]}),S&&t==="connection"&&c&&e.jsx(we,{bridge:c,busy:w,syncing:(d==null?void 0:d.status)==="syncing",update:te,pair:ce,connect:ae,updateExtension:ne,copy:re})]}),v&&e.jsxs("div",{className:"toast",role:"status",children:[e.jsx(Q,{size:16}),e.jsx("span",{children:v}),e.jsx("button",{className:"icon-button","aria-label":"关闭提示",onClick:()=>h(""),children:e.jsx(ee,{size:15})})]}),D&&e.jsx(J,{title:"稿件预览",close:()=>P(null),wide:!0,children:e.jsxs("div",{className:"preview-content",children:[D.cover&&e.jsx("img",{className:"preview-cover",src:D.cover,alt:"文章封面"}),e.jsx("h1",{children:L(D.title)}),e.jsx("div",{className:"article-content",dangerouslySetInnerHTML:{__html:he.sanitize(Te(D))}})]})}),X&&e.jsx(J,{title:"删除这篇稿件?",close:()=>{w||z(null)},children:e.jsxs("div",{className:"modal-content",children:[e.jsx("p",{children:"只删除这台电脑上的稿件和记录,已发到平台的内容不受影响。"}),e.jsxs("div",{className:"modal-actions",children:[e.jsx("button",{className:"secondary-button",disabled:w,onClick:()=>z(null),children:"保留稿件"}),e.jsx("button",{className:"danger-button",disabled:w,onClick:async()=>{y(!0);try{await ge(X.id),z(null),await _()}catch(s){h(s instanceof Error?s.message:"删除失败")}finally{y(!1)}},children:w?"正在删除…":"删除稿件"})]})]})})]})}ue(document.getElementById("root")).render(e.jsx(i.StrictMode,{children:e.jsx(_e,{})}));