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.
- package/README.md +0 -1
- package/extension/assets/editor-ClIT9WYa.js +76 -0
- package/extension/assets/{index.ts-DbQynGAJ.js → index.ts-BEVXnN62.js} +11 -11
- package/extension/manifest.json +1 -1
- package/extension/service-worker-loader.js +1 -1
- package/extension/src/editor/index.html +1 -1
- package/extension//345/256/211/350/243/205/344/270/216/346/233/264/346/226/260.txt +1 -1
- package/index.cjs +11 -70
- package/package.json +1 -1
- package/source/package.json +1 -1
- package/source/src/bridge.ts +11 -58
- package/source/src/identity.ts +2 -2
- package/source/src/server.ts +0 -4
- package/extension/assets/editor-DWoK50xk.js +0 -76
package/extension/manifest.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import './assets/index.ts-
|
|
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-
|
|
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">
|
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.
|
|
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", "/
|
|
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 === "/
|
|
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
|
|
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, "
|
|
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, "
|
|
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",
|
|
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",
|
|
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",
|
|
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 \
|
|
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\
|
|
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
package/source/package.json
CHANGED
package/source/src/bridge.ts
CHANGED
|
@@ -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
|
|
2
|
+
* WriteDock uses one loopback port, authenticated forwarding and an HMAC handshake.
|
|
3
3
|
*/
|
|
4
4
|
import http from 'node:http'
|
|
5
|
-
import {
|
|
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
|
|
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', '/
|
|
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 === '/
|
|
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
|
|
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, '
|
|
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, '
|
|
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',
|
|
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',
|
|
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',
|
|
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()
|
package/source/src/identity.ts
CHANGED
|
@@ -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`)
|
package/source/src/server.ts
CHANGED
|
@@ -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,{})}));
|