writedock-mcp 0.4.6 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -3
- package/extension/assets/editor-DSj8NlXD.js +76 -0
- package/extension/assets/index.ts-CrE-nH9x.js +42 -0
- 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 +4 -3
- package/index.cjs +30 -86
- package/package.json +1 -1
- package/source/package.json +1 -1
- package/source/src/__tests__/setup.test.ts +19 -0
- package/source/src/bridge.ts +12 -59
- package/source/src/identity.ts +2 -2
- package/source/src/server.ts +3 -7
- package/source/src/setup.ts +22 -12
- package/source/tsup.config.ts +2 -0
- package/extension/assets/editor-DWoK50xk.js +0 -76
- package/extension/assets/index.ts-DbQynGAJ.js +0 -42
package/extension/manifest.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import './assets/index.ts-
|
|
1
|
+
import './assets/index.ts-CrE-nH9x.js';
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<meta name="color-scheme" content="light" />
|
|
7
7
|
<title>写作台 · WriteDock</title>
|
|
8
8
|
<link rel="icon" type="image/png" href="/assets/icon-48.png" />
|
|
9
|
-
<script type="module" crossorigin src="/assets/editor-
|
|
9
|
+
<script type="module" crossorigin src="/assets/editor-DSj8NlXD.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/purify.es-1yEQDPgc.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/react-vendor-BrXlnJmF.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/task-utils-DwzGcJkm.js">
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
WriteDock 安装与更新 v0.4.
|
|
1
|
+
WriteDock 安装与更新 v0.4.8
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
1. 把这句话发给你的 agent:帮我安装 WriteDock
|
|
3
|
+
首次使用
|
|
4
|
+
1. 把这句话发给你的 agent:帮我安装 WriteDock:先检查电脑上有没有 Node.js 22 或更高版本,没有就帮我安装,需要我操作时告诉我;然后运行 npx -y writedock-mcp@latest setup,最后告诉我怎么重启你。
|
|
5
|
+
如果 agent 装不上 Node.js:到 nodejs.org 下载 LTS 版,一路点下一步,重启 agent 后再发一次这句话。
|
|
5
6
|
2. 按提示重启 agent,并在 Chrome / Edge 扩展管理页打开开发者模式,加载提示中的文件夹。这一步只需做一次。
|
|
6
7
|
3. 打开扩展,点“连接”。
|
|
7
8
|
4. 在同一个浏览器里登录要发布的平台。
|
package/index.cjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
if (Number(process.versions.node.split('.')[0]) < 22) { console.error('WriteDock 需要 Node.js 22 或更高版本,当前是 ' + process.version + '。请安装 https://nodejs.org 的 LTS 版,然后重启 agent。'); process.exit(1) }
|
|
2
3
|
"use strict";
|
|
3
4
|
var __create = Object.create;
|
|
4
5
|
var __defProp = Object.defineProperty;
|
|
@@ -29739,7 +29740,7 @@ var MediaStore = class {
|
|
|
29739
29740
|
// package.json
|
|
29740
29741
|
var package_default = {
|
|
29741
29742
|
name: "@writedock/mcp-server",
|
|
29742
|
-
version: "0.4.
|
|
29743
|
+
version: "0.4.8",
|
|
29743
29744
|
private: true,
|
|
29744
29745
|
type: "module",
|
|
29745
29746
|
description: "WriteDock local MCP server for the browser extension",
|
|
@@ -29953,7 +29954,6 @@ var LocalBridge = class {
|
|
|
29953
29954
|
primary = false;
|
|
29954
29955
|
extensionId;
|
|
29955
29956
|
seenNonces = /* @__PURE__ */ new Map();
|
|
29956
|
-
pairing;
|
|
29957
29957
|
extensionToken;
|
|
29958
29958
|
media;
|
|
29959
29959
|
pending = /* @__PURE__ */ new Map();
|
|
@@ -29982,14 +29982,13 @@ var LocalBridge = class {
|
|
|
29982
29982
|
server.on("upgrade", (req, socket, head) => {
|
|
29983
29983
|
const origin = req.headers.origin;
|
|
29984
29984
|
const extensionOrigin = typeof origin === "string" && /^chrome-extension:\/\/[a-p]{32}$/.test(origin);
|
|
29985
|
-
if (!this.validHost(req.headers.host) || !["/extension", "/
|
|
29985
|
+
if (!this.validHost(req.headers.host) || !["/extension", "/connect"].includes(req.url || "") || origin && !extensionOrigin || req.url === "/connect" && origin !== EXTENSION_ORIGIN) {
|
|
29986
29986
|
socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
29987
29987
|
socket.destroy();
|
|
29988
29988
|
return;
|
|
29989
29989
|
}
|
|
29990
29990
|
sockets.handleUpgrade(req, socket, head, (ws) => {
|
|
29991
|
-
if (req.url === "/
|
|
29992
|
-
else if (req.url === "/connect") void this.acceptConnect(ws);
|
|
29991
|
+
if (req.url === "/connect") void this.acceptConnect(ws);
|
|
29993
29992
|
else this.accept(ws);
|
|
29994
29993
|
});
|
|
29995
29994
|
});
|
|
@@ -30011,16 +30010,6 @@ var LocalBridge = class {
|
|
|
30011
30010
|
validHost(host) {
|
|
30012
30011
|
return host === `127.0.0.1:${this.port}` || host === `localhost:${this.port}`;
|
|
30013
30012
|
}
|
|
30014
|
-
async beginPairing() {
|
|
30015
|
-
const state = await this.status();
|
|
30016
|
-
if (!this.primary && state.pairing_available !== true) throw new Error("\u672C\u673A\u7AEF\u53E3\u4ECD\u7531\u65E7\u7248 MCP \u5360\u7528\u3002\u8BF7\u91CD\u542F\u4ECD\u4F7F\u7528\u65E7\u7248\u672C\u7684 agent/MCP \u8FDB\u7A0B\uFF0C\u4FDD\u7559\u539F\u914D\u7F6E\u540E\u518D\u751F\u6210\u914D\u5BF9\u7801\u3002");
|
|
30017
|
-
if (!this.primary) return this.httpCall("/pairing", {});
|
|
30018
|
-
const secret = (0, import_node_crypto2.randomBytes)(8).toString("hex").toUpperCase().match(/.{4}/g).join("-");
|
|
30019
|
-
const code = `WD1-${this.port}-${secret}`;
|
|
30020
|
-
const expiresAt = Date.now() + 18e4;
|
|
30021
|
-
this.pairing = { code, expiresAt, attempts: 0 };
|
|
30022
|
-
return { pairing_code: code, expires_at: new Date(expiresAt).toISOString(), expires_in_seconds: 180, instructions: "\u5C06\u6B64\u4E00\u6B21\u6027\u914D\u5BF9\u7801\u7C98\u8D34\u5230\u6269\u5C55\u7684\u300CMCP \u8FDE\u63A5 \u2192 \u8F93\u5165\u4E00\u6B21\u6027\u914D\u5BF9\u7801\u300D\u3002\u6709\u6548\u671F 3 \u5206\u949F\uFF0C\u4F7F\u7528\u4E00\u6B21\u540E\u5931\u6548\u3002\u65E0\u9700\u91CD\u65B0\u5B89\u88C5 MCP \u6216\u4FEE\u6539 agent \u914D\u7F6E\u3002\u786E\u8BA4\u7528\u6237\u8981\u8FDE\u63A5\u5F53\u524D\u6269\u5C55\u540E\u518D\u751F\u6210\u3002" };
|
|
30023
|
-
}
|
|
30024
30013
|
/** Replaces the extension credential and drops whatever was using the old one. */
|
|
30025
30014
|
async replaceExtensionToken(reason) {
|
|
30026
30015
|
const token = (0, import_node_crypto2.randomBytes)(32).toString("hex");
|
|
@@ -30034,7 +30023,7 @@ var LocalBridge = class {
|
|
|
30034
30023
|
value.reject(new Error(reason));
|
|
30035
30024
|
}
|
|
30036
30025
|
this.pending.clear();
|
|
30037
|
-
previousClient?.close(1e3, "Extension
|
|
30026
|
+
previousClient?.close(1e3, "Extension connection replaced");
|
|
30038
30027
|
return token;
|
|
30039
30028
|
}
|
|
30040
30029
|
/** One-click connection: only this extension's fixed origin can reach here, and the user has just clicked 连接. */
|
|
@@ -30047,55 +30036,18 @@ var LocalBridge = class {
|
|
|
30047
30036
|
ws.close(1011, "Connect failed");
|
|
30048
30037
|
}
|
|
30049
30038
|
}
|
|
30050
|
-
acceptPairing(ws) {
|
|
30051
|
-
const pairing = this.pairing;
|
|
30052
|
-
if (!pairing || Date.now() > pairing.expiresAt || pairing.attempts >= 8) {
|
|
30053
|
-
ws.close(1008, "Pairing code expired");
|
|
30054
|
-
return;
|
|
30055
|
-
}
|
|
30056
|
-
pairing.attempts++;
|
|
30057
|
-
const nonce = (0, import_node_crypto2.randomBytes)(32).toString("hex");
|
|
30058
|
-
const timer = setTimeout(() => ws.close(1008, "Pairing timed out"), 1e4);
|
|
30059
|
-
ws.send(JSON.stringify({ type: "pairing_challenge", nonce, proof: proof(pairing.code, "pairing-server", nonce) }));
|
|
30060
|
-
let consumed = false;
|
|
30061
|
-
ws.on("message", (raw) => {
|
|
30062
|
-
void (async () => {
|
|
30063
|
-
if (consumed) return;
|
|
30064
|
-
consumed = true;
|
|
30065
|
-
const body = raw.toString();
|
|
30066
|
-
if (body.length > 2048) throw new Error("Invalid pairing message");
|
|
30067
|
-
const message = JSON.parse(body);
|
|
30068
|
-
if (this.pairing !== pairing || Date.now() > pairing.expiresAt || message.type !== "pairing_hello" || typeof message.proof !== "string" || !equal(message.proof, proof(pairing.code, "pairing-extension", nonce))) throw new Error("Pairing failed");
|
|
30069
|
-
this.pairing = void 0;
|
|
30070
|
-
const token = await this.replaceExtensionToken("\u6269\u5C55\u914D\u5BF9\u5DF2\u66F4\u6362\uFF1B\u8BF7\u67E5\u8BE2\u539F\u540C\u6B65\u4EFB\u52A1\uFF0C\u4E0D\u8981\u81EA\u52A8\u91CD\u65B0\u63D0\u4EA4\u3002");
|
|
30071
|
-
const key2 = (0, import_node_crypto2.createHmac)("sha256", pairing.code).update(`writedock-pairing-key:${nonce}`).digest();
|
|
30072
|
-
const iv = (0, import_node_crypto2.randomBytes)(12);
|
|
30073
|
-
const cipher = (0, import_node_crypto2.createCipheriv)("aes-256-gcm", key2, iv);
|
|
30074
|
-
cipher.setAAD(Buffer.from("writedock-pairing-v1"));
|
|
30075
|
-
const ciphertext = Buffer.concat([cipher.update(JSON.stringify({ token, port: this.port, protocol: "writedock/1" })), cipher.final(), cipher.getAuthTag()]);
|
|
30076
|
-
clearTimeout(timer);
|
|
30077
|
-
if (ws.readyState === import_websocket.default.OPEN) ws.send(JSON.stringify({ type: "pairing_result", iv: iv.toString("base64"), ciphertext: ciphertext.toString("base64") }));
|
|
30078
|
-
ws.close(1e3);
|
|
30079
|
-
})().catch(() => {
|
|
30080
|
-
ws.close(1008, "Pairing failed");
|
|
30081
|
-
});
|
|
30082
|
-
});
|
|
30083
|
-
ws.on("close", () => clearTimeout(timer));
|
|
30084
|
-
ws.on("error", () => {
|
|
30085
|
-
});
|
|
30086
|
-
}
|
|
30087
30039
|
accept(ws) {
|
|
30088
30040
|
const nonce = (0, import_node_crypto2.randomBytes)(32).toString("hex");
|
|
30089
30041
|
const token = this.extensionToken;
|
|
30090
30042
|
let authenticated = false;
|
|
30091
|
-
const timer = setTimeout(() => ws.close(1008, "
|
|
30043
|
+
const timer = setTimeout(() => ws.close(1008, "Handshake timed out"), 8e3);
|
|
30092
30044
|
ws.send(JSON.stringify({ type: "challenge", nonce, proof: proof(token, "server", nonce) }));
|
|
30093
30045
|
ws.on("message", (raw) => {
|
|
30094
30046
|
try {
|
|
30095
30047
|
const message = JSON.parse(raw.toString());
|
|
30096
30048
|
if (!authenticated) {
|
|
30097
30049
|
if (token !== this.extensionToken || message.type !== "hello" || typeof message.proof !== "string" || !equal(message.proof, proof(token, "extension", nonce))) {
|
|
30098
|
-
ws.close(1008, "
|
|
30050
|
+
ws.close(1008, "Handshake failed");
|
|
30099
30051
|
return;
|
|
30100
30052
|
}
|
|
30101
30053
|
if (this.client?.readyState === import_websocket.default.OPEN) {
|
|
@@ -30188,11 +30140,7 @@ var LocalBridge = class {
|
|
|
30188
30140
|
this.seenNonces.set(nonce, now + 3e4);
|
|
30189
30141
|
responseNonce = nonce;
|
|
30190
30142
|
if (req.method === "GET" && req.url === "/status") {
|
|
30191
|
-
send(200, { connected: this.client?.readyState === import_websocket.default.OPEN, extensionId: this.extensionId, protocol: "writedock/1",
|
|
30192
|
-
return;
|
|
30193
|
-
}
|
|
30194
|
-
if (req.method === "POST" && req.url === "/pairing") {
|
|
30195
|
-
send(200, await this.beginPairing());
|
|
30143
|
+
send(200, { connected: this.client?.readyState === import_websocket.default.OPEN, extensionId: this.extensionId, protocol: "writedock/1", ...mcpReleaseInfo() });
|
|
30196
30144
|
return;
|
|
30197
30145
|
}
|
|
30198
30146
|
if (req.method === "POST" && req.url === "/assets/register") {
|
|
@@ -30231,14 +30179,14 @@ var LocalBridge = class {
|
|
|
30231
30179
|
}
|
|
30232
30180
|
async status() {
|
|
30233
30181
|
await this.start();
|
|
30234
|
-
if (this.primary) return { connected: this.client?.readyState === import_websocket.default.OPEN, extensionId: this.extensionId, protocol: "writedock/1",
|
|
30182
|
+
if (this.primary) return { connected: this.client?.readyState === import_websocket.default.OPEN, extensionId: this.extensionId, protocol: "writedock/1", ...mcpReleaseInfo() };
|
|
30235
30183
|
try {
|
|
30236
30184
|
return await this.httpCall("/status");
|
|
30237
30185
|
} catch (error2) {
|
|
30238
30186
|
if (!(error2 instanceof TypeError)) throw error2;
|
|
30239
30187
|
this.starting = void 0;
|
|
30240
30188
|
await this.start();
|
|
30241
|
-
if (this.primary) return { connected: false, protocol: "writedock/1",
|
|
30189
|
+
if (this.primary) return { connected: false, protocol: "writedock/1", ...mcpReleaseInfo() };
|
|
30242
30190
|
throw error2;
|
|
30243
30191
|
}
|
|
30244
30192
|
}
|
|
@@ -30258,7 +30206,7 @@ var LocalBridge = class {
|
|
|
30258
30206
|
return await this.httpCall("/assets/register", { path, kind });
|
|
30259
30207
|
}
|
|
30260
30208
|
async requestConnected(method, params) {
|
|
30261
|
-
if (this.client?.readyState !== import_websocket.default.OPEN) throw new Error("\u6269\u5C55\u672A\u8FDE\u63A5\u3002\u8BF7\u8BA9\u7528\u6237\u6253\u5F00\
|
|
30209
|
+
if (this.client?.readyState !== import_websocket.default.OPEN) throw new Error("\u6269\u5C55\u672A\u8FDE\u63A5\uFF0C\u8FD9\u6B21\u6CA1\u6709\u53D1\u5E03\u3002\u8BF7\u8BA9\u7528\u6237\u6253\u5F00 Chrome \u6216 Edge\uFF0C\u8FDE\u63A5\u8FC7\u7684\u6269\u5C55\u901A\u5E38\u4F1A\u81EA\u52A8\u8FDE\u4E0A\uFF0C\u7136\u540E\u518D\u8BD5\uFF1B\u5982\u679C\u8FD8\u662F\u4E0D\u884C\uFF0C\u6253\u5F00 WriteDock \u6269\u5C55\u70B9\u201C\u8FDE\u63A5\u201D\u3002\u65E0\u9700\u91CD\u88C5 MCP\u3002");
|
|
30262
30210
|
const id = (0, import_node_crypto2.randomUUID)();
|
|
30263
30211
|
return new Promise((resolve4, reject) => {
|
|
30264
30212
|
const timer = setTimeout(() => {
|
|
@@ -30275,7 +30223,6 @@ var LocalBridge = class {
|
|
|
30275
30223
|
});
|
|
30276
30224
|
}
|
|
30277
30225
|
async close() {
|
|
30278
|
-
this.pairing = void 0;
|
|
30279
30226
|
clearInterval(this.heartbeat);
|
|
30280
30227
|
for (const value of this.pending.values()) {
|
|
30281
30228
|
clearTimeout(value.timer);
|
|
@@ -50382,7 +50329,7 @@ var postSource = {
|
|
|
50382
50329
|
weibo_video_creation: external_exports.enum(["original", "secondary", "repost"]).optional().describe("\u5FAE\u535A\u72EC\u7ACB\u89C6\u9891\u5FC5\u586B\uFF1Aoriginal \u539F\u521B\u3001secondary \u4E8C\u521B\u3001repost \u8F6C\u8F7D\u3002\u5FC5\u987B\u6839\u636E\u7528\u6237\u5B9E\u9645\u5185\u5BB9\u660E\u786E\u9009\u62E9\uFF0C\u4E0D\u5F97\u9ED8\u8BA4\u539F\u521B\u3002\u5176\u4ED6\u5E73\u53F0\u4E0D\u4F7F\u7528\u6B64\u5B57\u6BB5\uFF1B\u6765\u6E90\u3001\u5185\u5BB9\u58F0\u660E\u6216\u534F\u8BAE\u9700\u8865\u5145\u65F6\u8F6C\u4EBA\u5DE5\u5904\u7406"),
|
|
50383
50330
|
base_directory: external_exports.string().optional().describe("\u4F7F\u7528\u76F8\u5BF9\u7D20\u6750\u8DEF\u5F84\u65F6\uFF0C\u660E\u786E\u6307\u5B9A\u7D20\u6750\u76EE\u5F55")
|
|
50384
50331
|
};
|
|
50385
|
-
var agentWorkflow = "Agent \u53EA\u901A\u8FC7 WriteDock MCP \u6295\u7A3F\u548C\u67E5\u8BE2\u56DE\u6267\uFF0C\u4E0D\u4F9D\u8D56\u4E5F\u4E0D\u8981\u8C03\u7528\u6D4F\u89C8\u5668\u3001Playwright \u6216\u5176\u4ED6\u7F51\u9875\u5DE5\u5177\u3002\u6309\u7ED3\u679C\u7684 next_action \u64CD\u4F5C\uFF1Apoll_task \u7528\u539F task_id/request_id \u67E5\u8BE2 get_sync_task\uFF1Breport_result \u76F4\u63A5\u5411\u7528\u6237\u6C47\u62A5\u9010\u5E73\u53F0\u72B6\u6001\u548C\u94FE\u63A5\u3002submitted/published \u5DF2\u6709\u6269\u5C55\u786E\u8BA4\uFF0C\u4E0D\u518D\u6253\u5F00\u7F51\u9875\u9A8C\u8BC1\uFF1B\u9A8C\u8BC1\u7801\u3001\u56FE\u5F62\u9A8C\u8BC1\u3001\u534F\u8BAE\u3001\u53D1\u5E03\u8BBE\u7F6E\u6216 unknown/interrupted \u90FD\u5C06\u539F\u56E0\u548C\u94FE\u63A5\u4EA4\u7ED9\u7528\u6237\u81EA\u884C\u70B9\u51FB\u5904\u7406\uFF0C\u4E0D\u81EA\u52A8\u91CD\u53D1\u3001\u4E0D\u6362 request_id\u3002\u4EBA\u5DE5\u63A5\u7BA1\u4E0D\u4EE3\u8868\u5DF2\u7ECF\u53D1\u5E03\u3002";
|
|
50332
|
+
var agentWorkflow = "Agent \u53EA\u901A\u8FC7 WriteDock MCP \u6295\u7A3F\u548C\u67E5\u8BE2\u56DE\u6267\uFF0C\u4E0D\u4F9D\u8D56\u4E5F\u4E0D\u8981\u8C03\u7528\u6D4F\u89C8\u5668\u3001Playwright \u6216\u5176\u4ED6\u7F51\u9875\u5DE5\u5177\u3002\u6309\u7ED3\u679C\u7684 next_action \u64CD\u4F5C\uFF1Apoll_task \u7528\u539F task_id/request_id \u67E5\u8BE2 get_sync_task\uFF1Breport_result \u76F4\u63A5\u5411\u7528\u6237\u6C47\u62A5\u9010\u5E73\u53F0\u72B6\u6001\u548C\u94FE\u63A5\u3002submitted/published \u5DF2\u6709\u6269\u5C55\u786E\u8BA4\uFF0C\u4E0D\u518D\u6253\u5F00\u7F51\u9875\u9A8C\u8BC1\uFF1B\u9A8C\u8BC1\u7801\u3001\u56FE\u5F62\u9A8C\u8BC1\u3001\u534F\u8BAE\u3001\u53D1\u5E03\u8BBE\u7F6E\u6216 unknown/interrupted \u90FD\u5C06\u539F\u56E0\u548C\u94FE\u63A5\u4EA4\u7ED9\u7528\u6237\u81EA\u884C\u70B9\u51FB\u5904\u7406\uFF0C\u4E0D\u81EA\u52A8\u91CD\u53D1\u3001\u4E0D\u6362 request_id\u3002\u4EBA\u5DE5\u63A5\u7BA1\u4E0D\u4EE3\u8868\u5DF2\u7ECF\u53D1\u5E03\u3002\u552F\u4E00\u4F8B\u5916\u662F user_actions \u7684 type=old_draft\uFF1A\u5E73\u53F0\u6709\u4E00\u4EFD\u7528\u6237\u4E0A\u6B21\u6CA1\u53D1\u5B8C\u7684\u5185\u5BB9\uFF0C\u8FD9\u7BC7\u5C1A\u672A\u63D0\u4EA4\u3002\u5FC5\u987B\u5148\u95EE\u7528\u6237\u662F\u5426\u653E\u5F03\u90A3\u4EFD\u65E7\u5185\u5BB9\uFF0C\u4E0D\u5F97\u66FF\u7528\u6237\u51B3\u5B9A\uFF1B\u7528\u6237\u660E\u786E\u540C\u610F\u540E\uFF0C\u624D\u7528\u65B0\u7684 request_id \u53EA\u5BF9\u8BE5\u5E73\u53F0\u518D\u8C03\u7528 publish\uFF0C\u5E76\u4F20 discard_old_drafts\uFF1B\u7528\u6237\u4E0D\u540C\u610F\u5C31\u4FDD\u7559\u65E7\u5185\u5BB9\uFF0C\u544A\u8BC9\u7528\u6237\u8FD9\u4E2A\u5E73\u53F0\u6CA1\u6709\u53D1\u3002";
|
|
50386
50333
|
function createMcpServer(bridge) {
|
|
50387
50334
|
const server = new McpServer({ name: "writedock", version: package_default.version }, {
|
|
50388
50335
|
instructions: agentWorkflow + 'WriteDock \u901A\u8FC7\u672C\u673A\u6269\u5C55\u6267\u884C\u6295\u7A3F\u3002\u5148\u68C0\u67E5\u8FDE\u63A5\u548C\u5E73\u53F0\u767B\u5F55\u3002\u5B9E\u9645\u5E73\u53F0\u80FD\u529B\u4EE5 list_platforms \u8FD4\u56DE\u7684 action\u3001publishModes\u3001tools\u3001modeOptions\u3001confirmation\u3001capabilities\u3001coverRequired \u4E3A\u51C6\uFF0C\u6269\u5C55\u66F4\u65B0\u540E\u4F18\u5148\u6309\u8BE5\u5143\u4FE1\u606F\u9009\u62E9\u5DE5\u5177\u3002\u77E5\u4E4E\u3001\u767E\u5BB6\u53F7\u3001\u5FAE\u535A\u957F\u6587\u7AE0\u3001B\u7AD9\u4E13\u680F\u3001\u5C0F\u7EA2\u4E66\u957F\u6587\u4F7F\u7528 publish_article\uFF0CAgent \u76F4\u63A5\u63D0\u4F9B Markdown/HTML \u6216\u6587\u4EF6\u8DEF\u5F84\u5373\u53EF\u53D1\u5E03\uFF0C\u65E0\u9700\u7528\u6237\u5728\u6269\u5C55\u7F16\u8F91\u3002\u767E\u5BB6\u53F7\u5FC5\u987B\u63D0\u4F9B cover \u5C01\u9762\uFF1B\u77E5\u4E4E\u53E6\u652F\u6301 <video src="\u672C\u5730MP4\u8DEF\u5F84"></video> \u6587\u7AE0\u5185\u89C6\u9891\uFF0C\u72EC\u7ACB\u5C01\u9762\u6682\u672A\u63A5\u5165\u3002prepare_article \u4EC5\u4F9B\u53EF\u9009\u672C\u673A\u9884\u89C8\uFF0C\u4E0D\u662F\u53D1\u5E03\u7684\u524D\u7F6E\u8981\u6C42\u3002\u5C0F\u7EA2\u4E66 ID \u4E3A xiaohongshu\uFF0C\u4E09\u79CD\u6A21\u5F0F\u5171\u7528\u6B64\u5E73\u53F0\uFF1A\u5199\u957F\u6587\u7528 publish_article\uFF0C\u652F\u6301\u6B63\u6587\u56FE\u7247\u3001\u539F\u751F\u81EA\u52A8\u6392\u7248\u540E\u53D1\u5E03\uFF0C\u6807\u9898\u6700\u591A 64 \u5B57\u3001\u6B63\u6587\u6700\u591A 10000 \u5B57\uFF0C\u4E0D\u63A5\u72EC\u7ACB cover \u6216\u6587\u7AE0\u5185\u89C6\u9891\uFF1B\u666E\u901A\u56FE\u6587\u7528 publish_post(kind=image)\uFF0C\u72EC\u7ACB\u89C6\u9891\u7528 publish_post(kind=video)\uFF0C\u672A\u4F20 cover_path \u65F6\u4FDD\u7559\u5E73\u53F0\u81EA\u52A8\u751F\u6210\u7684\u7B2C\u4E00\u5E27\u5C01\u9762\u3002\u5C0F\u7EA2\u4E66\u6700\u7EC8\u5747\u987B\u5339\u914D POST /web_api/sns/v2/note \u7684 success=true\u3001result=0 \u548C\u7B14\u8BB0 ID \u56DE\u6267\u624D\u786E\u8BA4 submitted\u3002\u6296\u97F3\u5E73\u53F0 ID \u4E3A douyin\uFF0C\u56FE\u6587\u548C\u89C6\u9891\u540C\u6837\u7528 publish_post(kind=image|video)\uFF0C\u53EF\u9009 cover_path\uFF1B\u56FE\u6587\u8BDD\u9898\u6700\u591A 4 \u4E2A\u3001\u89C6\u9891\u6700\u591A 5 \u4E2A\uFF0C\u5F53\u524D\u53EA\u63A5\u5373\u65F6\u53D1\u5E03\u3002\u6296\u97F3 confirmation=native_page_message\uFF0C\u4EE5\u672C\u6B21\u64CD\u4F5C\u65B0\u589E\u7684\u539F\u751F\u6210\u529F\u63D0\u793A\u786E\u8BA4 submitted\uFF0C\u672A\u6838\u5BF9\u6700\u7EC8 HTTP \u4E1A\u52A1\u56DE\u6267\uFF1B\u4EC5\u8DF3\u8F6C\u4F5C\u54C1\u7BA1\u7406\u65F6\u4ECD\u4E3A unknown\uFF0CAgent \u4E0D\u81EA\u52A8\u91CD\u53D1\u3002\u5C0F\u7EA2\u4E66\u3001\u6296\u97F3\u3001\u5FAE\u535A\u666E\u901A\u56FE\u6587\u4E0E\u72EC\u7ACB\u89C6\u9891\u4F7F\u7528 prepare_post / publish_post\uFF0C\u5FAE\u535A\u5E73\u53F0 ID \u4ECD\u4E3A weibo\uFF0C\u4E0D\u53E6\u5EFA\u5E73\u53F0\u3002\u5FAE\u535A\u56FE\u6587\u6807\u9898\u653E\u5165\u6B63\u6587\u9996\u884C\uFF0C\u6700\u591A 18 \u5F20\u56FE\u7247\uFF1B\u72EC\u7ACB\u89C6\u9891\u6807\u9898\u6700\u591A 30 \u4E2A\u6C49\u5B57\uFF0860 \u4E2A\u534A\u89D2\u5B57\u7B26\uFF09\uFF0Cweibo_video_creation \u5FC5\u586B\uFF0C\u987B\u4F9D\u636E\u7528\u6237\u5B9E\u9645\u5185\u5BB9\u4F20 original\u3001secondary \u6216 repost\uFF0C\u4E0D\u80FD\u9ED8\u8BA4\u539F\u521B\u3002\u5FAE\u535A\u540C\u4E00\u6B21\u53EA\u80FD\u9009\u56FE\u6587\u6216\u89C6\u9891\uFF0C\u957F\u6587\u7AE0\u7EE7\u7EED\u7528 publish_article\u3002\u53EA\u6709\u7528\u6237\u660E\u786E\u8981\u6C42\u53D1\u5E03\u5E76\u6307\u5B9A\u5E73\u53F0\u65F6\u624D\u8C03\u7528\u53D1\u5E03\u5DE5\u5177\u3002\u5FAE\u4FE1\u516C\u4F17\u53F7\u3001\u641C\u72D0\u53F7\u76EE\u524D\u7528 sync_article \u4FDD\u5B58\u6587\u7AE0\u8349\u7A3F\u3002B\u7AD9\u4E13\u680F ID \u4E3A bilibili\uFF0C\u4F7F\u7528 publish_article \u76F4\u63A5\u53D1\u5E03\u6587\u5B57\u548C\u6B63\u6587\u56FE\u7247\uFF0C\u4E0D\u63A5\u89C6\u9891\u3001\u97F3\u9891\u3001\u9644\u52A0\u5185\u5BB9\u5361\u7247\u6216\u72EC\u7ACB\u5C01\u9762\uFF1B\u901A\u8FC7\u65B0\u7248\u539F\u751F\u7F16\u8F91\u5668\u4E0A\u4F20\u5E76\u53D1\u5E03\uFF0Ccode=-352 \u65F6\u505C\u6B62\u81EA\u52A8\u63D0\u4EA4\u5E76\u8F6C\u4EBA\u5DE5\u9A8C\u8BC1\u3002\u9047\u5230 needs_user_action \u6216 requires_user_action \u65F6\uFF0C\u628A user_actions \u4E2D\u7684\u539F\u56E0\u548C\u7A3F\u4EF6\u94FE\u63A5\u544A\u8BC9\u7528\u6237\uFF0C\u4F18\u5148\u8BA9\u7528\u6237\u5207\u6362\u5230\u6269\u5C55\u5DF2\u6253\u5F00\u7684\u9875\u9762\u5904\u7406\u9A8C\u8BC1\u7801\u3001\u5C01\u9762\u3001\u5185\u5BB9\u58F0\u660E\u3001\u6765\u6E90\u3001\u539F\u521B\u534F\u8BAE\u6216\u53D1\u5E03\u8BBE\u7F6E\uFF0C\u5E76\u624B\u52A8\u53D1\u5E03\uFF1B\u4E0D\u5F97\u81EA\u52A8\u89E3\u51B3\u9A8C\u8BC1\u7801\u3001\u91CD\u65B0\u63D0\u4EA4\u6216\u6362 request_id\u3002\u8BE5\u72B6\u6001\u53EA\u8BB0\u5F55\u4EBA\u5DE5\u63A5\u7BA1\uFF0C\u4E0D\u4EE3\u8868\u7528\u6237\u4E4B\u540E\u7684\u64CD\u4F5C\u7ED3\u679C\u5DF2\u88AB\u786E\u8BA4\u3002\u6269\u5C55\u66F4\u65B0\u590D\u7528\u5DF2\u6709 MCP\uFF1B\u672A\u8FDE\u63A5\u65F6\uFF0C\u8BA9\u7528\u6237\u6253\u5F00 WriteDock \u6269\u5C55\u7684\u300C\u8FDE\u63A5\u300D\u9875\u70B9\u201C\u8FDE\u63A5\u201D\u3002\u63D0\u4EA4\u8FD4\u56DE\u4EFB\u52A1 ID\uFF0C\u968F\u540E\u7528 get_sync_task \u67E5\u8BE2\u7ED3\u679C\u3002\u4FDD\u6301\u540C\u4E00\u64CD\u4F5C\u7684 request_id \u4E0D\u53D8\u3002publicationState=unknown \u6216\u4EFB\u52A1\u4E2D\u65AD\u65F6\uFF0C\u5411\u7528\u6237\u7ED9\u51FA\u94FE\u63A5\u8BA9\u5176\u81EA\u884C\u6838\u5BF9\u5E73\u53F0\uFF0CAgent \u4E0D\u6253\u5F00\u7F51\u9875\uFF0C\u4E0D\u8981\u81EA\u52A8\u751F\u6210\u65B0 request_id \u91CD\u53D1\u3002\u76F4\u63A5\u53D1\u5E03\u6210\u529F\u8868\u793A\u5E73\u53F0\u5DF2\u63A5\u53D7\u63D0\u4EA4\uFF0C\u662F\u5426\u516C\u5F00\u5C55\u793A\u4EE5\u5E73\u53F0\u4E3A\u51C6\u3002'
|
|
@@ -50396,11 +50343,6 @@ function createMcpServer(bridge) {
|
|
|
50396
50343
|
inputSchema: {},
|
|
50397
50344
|
annotations: { ...readOnly, openWorldHint: true }
|
|
50398
50345
|
}, () => wrap(() => bridge.status()));
|
|
50399
|
-
server.registerTool("begin_pairing", {
|
|
50400
|
-
description: "\u901A\u5E38\u4E0D\u9700\u8981\uFF1A\u672A\u8FDE\u63A5\u65F6\uFF0C\u8BA9\u7528\u6237\u6253\u5F00 WriteDock \u6269\u5C55\u7684\u300C\u8FDE\u63A5\u300D\u9875\u70B9\u201C\u8FDE\u63A5\u201D\u5373\u53EF\u3002\u53EA\u6709\u7528\u6237\u4F7F\u7528\u65E7\u7248\u6269\u5C55\u65F6\u624D\u751F\u6210 3 \u5206\u949F\u6709\u6548\u7684\u4E00\u6B21\u6027\u914D\u5BF9\u7801\u3002",
|
|
50401
|
-
inputSchema: {},
|
|
50402
|
-
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
50403
|
-
}, () => wrap(() => bridge.beginPairing()));
|
|
50404
50346
|
server.registerTool("list_platforms", {
|
|
50405
50347
|
description: "\u5217\u51FA\u6269\u5C55\u5B9E\u9645\u652F\u6301\u7684\u5E73\u53F0\u3001\u767B\u5F55\u72B6\u6001\u3001publishModes\u3001\u5BF9\u5E94 tools \u53CA modeOptions \u9650\u5236\uFF1B\u5C0F\u7EA2\u4E66 article \u5BF9\u5E94 publish_article\uFF0Cimage/video \u5BF9\u5E94 publish_post\u3002\u4E0D\u4F1A\u8FD4\u56DE Cookie\u3002",
|
|
50406
50348
|
inputSchema: { force_refresh: external_exports.boolean().optional() },
|
|
@@ -50459,9 +50401,9 @@ function createMcpServer(bridge) {
|
|
|
50459
50401
|
};
|
|
50460
50402
|
server.registerTool("publish", {
|
|
50461
50403
|
description: agentWorkflow + "\u9996\u9009\u7684\u53D1\u5E03\u5DE5\u5177\uFF1A\u4E00\u6B21\u8C03\u7528\u53D1\u5E03\u5230\u7528\u6237\u6307\u5B9A\u7684\u591A\u4E2A\u5E73\u53F0\u3002\u53EA\u6709\u7528\u6237\u660E\u786E\u8981\u6C42\u53D1\u5E03\u5E76\u6307\u5B9A\u5E73\u53F0\u65F6\u624D\u8C03\u7528\u3002\u6309\u5E73\u53F0\u4F9D\u6B21\u6267\u884C\uFF0C\u67D0\u4E2A\u5E73\u53F0\u672A\u767B\u5F55\u3001\u4E0D\u80FD\u63A5\u6536\u5185\u5BB9\u6216\u5931\u8D25\uFF0C\u53EA\u8DF3\u8FC7\u8BE5\u5E73\u53F0\uFF0C\u5176\u4ED6\u5E73\u53F0\u7EE7\u7EED\u3002\u9047\u5230\u9A8C\u8BC1\u7801\u65F6\u5E73\u53F0\u8FDB\u5165 waiting_verification\uFF0C\u7ACB\u5373\u628A\u539F\u56E0\u548C\u94FE\u63A5\u544A\u8BC9\u7528\u6237\uFF0C\u7531\u7528\u6237\u624B\u52A8\u5B8C\u6210\uFF0C\u7EE7\u7EED\u67E5\u8BE2\u5373\u53EF\uFF0C\u4E0D\u8981\u91CD\u53D1\u3002\u4EFB\u52A1\u7ED3\u675F\u540E\u6309 get_sync_task \u7684 summary \u9010\u5E73\u53F0\u544A\u8BC9\u7528\u6237\uFF1A\u54EA\u4E9B\u6210\u529F\uFF0C\u54EA\u4E9B\u672A\u767B\u5F55\uFF08not_logged_in\uFF09\u7B49\u3002\u7ACB\u5373\u8FD4\u56DE task_id\uFF0C\u518D\u7528 get_sync_task \u67E5\u8BE2\uFF1Bresults \u662F\u9010\u5E73\u53F0\u7ED3\u679C\uFF0Cwarnings \u8BF4\u660E\u4E3A\u5E73\u53F0\u505A\u7684\u8C03\u6574\u3002\u5FC5\u987B\u4F7F\u7528\u7A33\u5B9A request_id\uFF0C\u76F8\u540C ID \u4E0D\u4F1A\u91CD\u590D\u63D0\u4EA4\u3002\u90E8\u5206\u5E73\u53F0\u5931\u8D25\u65F6\uFF0C\u53EA\u9488\u5BF9\u5931\u8D25\u5E73\u53F0\u7528\u65B0 request_id \u91CD\u65B0\u63D0\u4EA4\uFF0Cunknown \u7684\u5E73\u53F0\u4E0D\u8981\u91CD\u53D1\u3002\u65E7\u7248\u6269\u5C55\u4E0D\u652F\u6301\u65F6\u4F1A\u62A5\u9519\uFF0C\u4E0D\u4F1A\u53D1\u5E03\u3002",
|
|
50462
|
-
inputSchema: { ...unifiedInput, request_id: requestId },
|
|
50404
|
+
inputSchema: { ...unifiedInput, request_id: requestId, discard_old_drafts: external_exports.array(external_exports.enum(["douyin"])).optional().describe('\u53EA\u6709\u7528\u6237\u660E\u786E\u540C\u610F\u653E\u5F03\u5E73\u53F0\u4E0A\u6B21\u6CA1\u53D1\u5B8C\u7684\u5185\u5BB9\u65F6\u624D\u4F20\uFF0C\u5982 ["douyin"]\u3002\u653E\u5F03\u540E\u65E0\u6CD5\u6062\u590D\u3002') },
|
|
50463
50405
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
50464
|
-
}, (args) => wrap(async () => bridge.request("publish", { requestId: args.request_id, ...await unifiedParams(args) })));
|
|
50406
|
+
}, (args) => wrap(async () => bridge.request("publish", { requestId: args.request_id, ...args.discard_old_drafts ? { discardOldDrafts: args.discard_old_drafts } : {}, ...await unifiedParams(args) })));
|
|
50465
50407
|
server.registerTool("rehearse_publish", {
|
|
50466
50408
|
description: "\u4E0E publish \u53C2\u6570\u76F8\u540C\u7684\u6F14\u7EC3\uFF1A\u9010\u5E73\u53F0\u6253\u5F00\u539F\u751F\u9875\u9762\uFF0C\u586B\u597D\u5185\u5BB9\u3001\u4E0A\u4F20\u7D20\u6750\uFF0C\u8D70\u5230\u70B9\u51FB\u53D1\u5E03\u4E4B\u524D\u505C\u4E0B\uFF0C\u4E0D\u63D0\u4EA4\u53D1\u5E03\u3002\u65E0\u9700 request_id\uFF0C\u4E0D\u5F71\u54CD\u4E4B\u540E\u7684\u771F\u5B9E\u53D1\u5E03\u3002\u56FE\u7247\u53EF\u80FD\u5DF2\u4E0A\u4F20\u5230\u5E73\u53F0\uFF0C\u90E8\u5206\u5E73\u53F0\u4F1A\u7559\u4E0B\u4E2D\u95F4\u8349\u7A3F\uFF0C\u516C\u4F17\u53F7\u3001\u641C\u72D0\u53F7\u4E0D\u53C2\u4E0E\u3002\u7ED3\u679C publicationState=rehearsed \u4E0D\u4EE3\u8868\u5DF2\u53D1\u5E03\uFF0C\u4E0D\u5F97\u544A\u8BC9\u7528\u6237\u5185\u5BB9\u5DF2\u53D1\u51FA\uFF0C\u4E5F\u4E0D\u8981\u81EA\u884C\u63A5\u7740\u53D1\u5E03\u3002",
|
|
50467
50409
|
inputSchema: unifiedInput,
|
|
@@ -50529,7 +50471,7 @@ async function loadIdentity(port, legacyToken) {
|
|
|
50529
50471
|
const filename = (0, import_node_path4.join)(directory, `identity-${port}.json`);
|
|
50530
50472
|
const read = async () => {
|
|
50531
50473
|
const value = JSON.parse(await (0, import_promises4.readFile)(filename, "utf8"));
|
|
50532
|
-
if (value.version !== 1 || !validToken(value.token) || !validToken(value.extensionToken)) throw new Error("MCP \
|
|
50474
|
+
if (value.version !== 1 || !validToken(value.token) || !validToken(value.extensionToken)) throw new Error("MCP \u8FDE\u63A5\u8D44\u6599\u683C\u5F0F\u4E0D\u6B63\u786E\uFF1B\u8BF7\u4FDD\u7559\u539F\u6587\u4EF6\u5E76\u68C0\u67E5\u5907\u4EFD\uFF0C\u4E0D\u4F1A\u81EA\u52A8\u8986\u76D6");
|
|
50533
50475
|
return value;
|
|
50534
50476
|
};
|
|
50535
50477
|
let identity;
|
|
@@ -50557,7 +50499,7 @@ async function loadIdentity(port, legacyToken) {
|
|
|
50557
50499
|
token: identity.token,
|
|
50558
50500
|
getExtensionToken: async () => (await read()).extensionToken,
|
|
50559
50501
|
setExtensionToken: async (extensionToken) => {
|
|
50560
|
-
if (!validToken(extensionToken)) throw new Error("\u6269\u5C55\
|
|
50502
|
+
if (!validToken(extensionToken)) throw new Error("\u6269\u5C55\u8FDE\u63A5\u5BC6\u94A5\u683C\u5F0F\u4E0D\u6B63\u786E");
|
|
50561
50503
|
const current = await read();
|
|
50562
50504
|
if (current.token !== identity.token) throw new Error("MCP \u8EAB\u4EFD\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u542F\u52A8\u670D\u52A1");
|
|
50563
50505
|
const temporary = (0, import_node_path4.join)(directory, `.identity-${(0, import_node_crypto3.randomUUID)()}.tmp`);
|
|
@@ -50577,11 +50519,11 @@ var import_node_child_process = require("child_process");
|
|
|
50577
50519
|
var import_promises5 = require("fs/promises");
|
|
50578
50520
|
var import_node_os3 = require("os");
|
|
50579
50521
|
var import_node_path5 = require("path");
|
|
50580
|
-
var
|
|
50581
|
-
var
|
|
50522
|
+
var NPX_ARGS = ["-y", "--prefer-online", "writedock-mcp@latest"];
|
|
50523
|
+
var serverCommand = (platform) => platform === "win32" ? { command: "cmd", args: ["/c", "npx", ...NPX_ARGS] } : { command: "npx", args: NPX_ARGS };
|
|
50582
50524
|
async function readIfPresent(path) {
|
|
50583
50525
|
try {
|
|
50584
|
-
return await (0, import_promises5.readFile)(path, "utf8");
|
|
50526
|
+
return (await (0, import_promises5.readFile)(path, "utf8")).replace(/^\uFEFF/, "");
|
|
50585
50527
|
} catch (error2) {
|
|
50586
50528
|
if (error2.code === "ENOENT") return void 0;
|
|
50587
50529
|
throw error2;
|
|
@@ -50592,12 +50534,13 @@ async function save(path, content, previous) {
|
|
|
50592
50534
|
if (previous !== void 0) await (0, import_promises5.copyFile)(path, `${path}.writedock-backup`);
|
|
50593
50535
|
await (0, import_promises5.writeFile)(path, content);
|
|
50594
50536
|
}
|
|
50595
|
-
async function configureCodex(path) {
|
|
50537
|
+
async function configureCodex(path, platform = process.platform) {
|
|
50596
50538
|
const previous = await readIfPresent(path);
|
|
50597
50539
|
if (previous !== void 0 && /^\s*\[mcp_servers\.writedock\]/m.test(previous)) return "present";
|
|
50540
|
+
const { command, args } = serverCommand(platform);
|
|
50598
50541
|
const entry = `[mcp_servers.writedock]
|
|
50599
|
-
command = "${
|
|
50600
|
-
args = ${JSON.stringify(
|
|
50542
|
+
command = "${command}"
|
|
50543
|
+
args = ${JSON.stringify(args)}
|
|
50601
50544
|
startup_timeout_sec = 120
|
|
50602
50545
|
`;
|
|
50603
50546
|
await save(path, previous ? `${previous.replace(/\s*$/, "")}
|
|
@@ -50605,11 +50548,11 @@ startup_timeout_sec = 120
|
|
|
50605
50548
|
${entry}` : entry, previous);
|
|
50606
50549
|
return "added";
|
|
50607
50550
|
}
|
|
50608
|
-
async function configureJsonClient(path) {
|
|
50551
|
+
async function configureJsonClient(path, platform = process.platform) {
|
|
50609
50552
|
const previous = await readIfPresent(path);
|
|
50610
50553
|
const config2 = previous?.trim() ? JSON.parse(previous) : {};
|
|
50611
50554
|
if (config2.mcpServers?.writedock) return "present";
|
|
50612
|
-
config2.mcpServers = { ...config2.mcpServers, writedock:
|
|
50555
|
+
config2.mcpServers = { ...config2.mcpServers, writedock: serverCommand(platform) };
|
|
50613
50556
|
await save(path, `${JSON.stringify(config2, null, 2)}
|
|
50614
50557
|
`, previous);
|
|
50615
50558
|
return "added";
|
|
@@ -50625,19 +50568,20 @@ function revealFolder(path) {
|
|
|
50625
50568
|
} catch {
|
|
50626
50569
|
}
|
|
50627
50570
|
}
|
|
50628
|
-
async function runSetup(home = (0, import_node_os3.homedir)(), log = console.log) {
|
|
50571
|
+
async function runSetup(home = (0, import_node_os3.homedir)(), log = console.log, platform = process.platform) {
|
|
50572
|
+
const claudeDesktop = platform === "win32" ? (0, import_node_path5.join)(process.env.APPDATA || (0, import_node_path5.join)(home, "AppData/Roaming"), "Claude/claude_desktop_config.json") : platform === "darwin" ? (0, import_node_path5.join)(home, "Library/Application Support/Claude/claude_desktop_config.json") : (0, import_node_path5.join)(home, ".config/Claude/claude_desktop_config.json");
|
|
50629
50573
|
const clients = [
|
|
50630
50574
|
{ name: "Codex", path: (0, import_node_path5.join)(home, ".codex/config.toml"), detect: (0, import_node_path5.join)(home, ".codex/config.toml"), configure: configureCodex },
|
|
50631
50575
|
{ name: "Claude Code", path: (0, import_node_path5.join)(home, ".claude.json"), detect: (0, import_node_path5.join)(home, ".claude.json"), configure: configureJsonClient },
|
|
50632
50576
|
{ name: "Cursor", path: (0, import_node_path5.join)(home, ".cursor/mcp.json"), detect: (0, import_node_path5.join)(home, ".cursor/mcp.json"), configure: configureJsonClient },
|
|
50633
|
-
{ name: "Claude Desktop", path:
|
|
50577
|
+
{ name: "Claude Desktop", path: claudeDesktop, detect: claudeDesktop, configure: configureJsonClient }
|
|
50634
50578
|
];
|
|
50635
50579
|
let found = 0;
|
|
50636
50580
|
for (const client of clients) {
|
|
50637
50581
|
if (!await exists(client.detect)) continue;
|
|
50638
50582
|
found++;
|
|
50639
50583
|
try {
|
|
50640
|
-
const state = await client.configure(client.path);
|
|
50584
|
+
const state = await client.configure(client.path, platform);
|
|
50641
50585
|
log(state === "added" ? `\u2713 ${client.name}\uFF1A\u5DF2\u6DFB\u52A0 WriteDock\uFF08\u539F\u914D\u7F6E\u5907\u4EFD\u4E3A .writedock-backup\uFF09` : `\u2713 ${client.name}\uFF1A\u5DF2\u6709 WriteDock \u914D\u7F6E\uFF0C\u672A\u6539\u52A8`);
|
|
50642
50586
|
} catch (error2) {
|
|
50643
50587
|
log(`\u2717 ${client.name}\uFF1A\u914D\u7F6E\u5931\u8D25\uFF08${error2 instanceof Error ? error2.message : String(error2)}\uFF09\uFF0C\u539F\u6587\u4EF6\u672A\u6539\u52A8`);
|
|
@@ -50645,7 +50589,7 @@ async function runSetup(home = (0, import_node_os3.homedir)(), log = console.log
|
|
|
50645
50589
|
}
|
|
50646
50590
|
if (!found) {
|
|
50647
50591
|
log("\u6CA1\u6709\u627E\u5230\u5DF2\u77E5\u7684 agent \u5BA2\u6237\u7AEF\u3002\u8BF7\u5728\u4F60\u7684\u5BA2\u6237\u7AEF\u91CC\u6DFB\u52A0\u8FD9\u4E2A MCP \u670D\u52A1\uFF1A");
|
|
50648
|
-
log(JSON.stringify({ mcpServers: { writedock:
|
|
50592
|
+
log(JSON.stringify({ mcpServers: { writedock: serverCommand(platform) } }, null, 2));
|
|
50649
50593
|
}
|
|
50650
50594
|
const bundle = bundledExtension();
|
|
50651
50595
|
const loaded = bundle ? await findInstalledExtension().catch(() => void 0) : void 0;
|
package/package.json
CHANGED
package/source/package.json
CHANGED
|
@@ -22,6 +22,25 @@ describe('setup', () => {
|
|
|
22
22
|
expect(await readFile(join(home, '.codex/config.toml.writedock-backup'), 'utf8')).toBe(original)
|
|
23
23
|
})
|
|
24
24
|
|
|
25
|
+
it('starts npx through cmd on Windows, and finds Claude Desktop in its Windows folder', async () => {
|
|
26
|
+
const appData = process.env.APPDATA
|
|
27
|
+
delete process.env.APPDATA
|
|
28
|
+
try {
|
|
29
|
+
await mkdir(join(home, '.codex'))
|
|
30
|
+
await writeFile(join(home, '.codex/config.toml'), 'model = "x"\r\n')
|
|
31
|
+
const desktop = join(home, 'AppData/Roaming/Claude')
|
|
32
|
+
await mkdir(desktop, { recursive: true })
|
|
33
|
+
await writeFile(join(desktop, 'claude_desktop_config.json'), '\uFEFF{"theme":"dark"}')
|
|
34
|
+
expect(await runSetup(home, line => log.push(line), 'win32')).toBe(2)
|
|
35
|
+
const codex = await readFile(join(home, '.codex/config.toml'), 'utf8')
|
|
36
|
+
expect(codex).toContain('command = "cmd"')
|
|
37
|
+
expect(codex).toContain('args = ["/c","npx","-y","--prefer-online","writedock-mcp@latest"]')
|
|
38
|
+
const config = JSON.parse(await readFile(join(desktop, 'claude_desktop_config.json'), 'utf8'))
|
|
39
|
+
expect(config.theme).toBe('dark')
|
|
40
|
+
expect(config.mcpServers.writedock).toEqual({ command: 'cmd', args: ['/c', 'npx', '-y', '--prefer-online', 'writedock-mcp@latest'] })
|
|
41
|
+
} finally { if (appData !== undefined) process.env.APPDATA = appData }
|
|
42
|
+
})
|
|
43
|
+
|
|
25
44
|
it('leaves an existing entry alone, including its custom port and token', async () => {
|
|
26
45
|
await mkdir(join(home, '.codex'))
|
|
27
46
|
const original = '[mcp_servers.writedock]\ncommand = "node"\n\n[mcp_servers.writedock.env]\nWRITEDOCK_PORT = "9700"\n'
|
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
|
}
|
|
@@ -299,7 +253,7 @@ export class LocalBridge {
|
|
|
299
253
|
}
|
|
300
254
|
|
|
301
255
|
private async requestConnected(method: string, params: Record<string, unknown>): Promise<unknown> {
|
|
302
|
-
if (this.client?.readyState !== WebSocket.OPEN) throw new Error('
|
|
256
|
+
if (this.client?.readyState !== WebSocket.OPEN) throw new Error('扩展未连接,这次没有发布。请让用户打开 Chrome 或 Edge,连接过的扩展通常会自动连上,然后再试;如果还是不行,打开 WriteDock 扩展点“连接”。无需重装 MCP。')
|
|
303
257
|
const id = randomUUID()
|
|
304
258
|
return new Promise((resolve, reject) => {
|
|
305
259
|
const timer = setTimeout(() => {
|
|
@@ -315,7 +269,6 @@ export class LocalBridge {
|
|
|
315
269
|
}
|
|
316
270
|
|
|
317
271
|
async close() {
|
|
318
|
-
this.pairing = undefined
|
|
319
272
|
clearInterval(this.heartbeat)
|
|
320
273
|
for (const value of this.pending.values()) { clearTimeout(value.timer); value.reject(new Error('MCP 服务已关闭')) }
|
|
321
274
|
this.pending.clear()
|
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
|
@@ -26,7 +26,7 @@ const postSource = {
|
|
|
26
26
|
base_directory: z.string().optional().describe('使用相对素材路径时,明确指定素材目录'),
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const agentWorkflow = 'Agent 只通过 WriteDock MCP 投稿和查询回执,不依赖也不要调用浏览器、Playwright 或其他网页工具。按结果的 next_action 操作:poll_task 用原 task_id/request_id 查询 get_sync_task;report_result 直接向用户汇报逐平台状态和链接。submitted/published 已有扩展确认,不再打开网页验证;验证码、图形验证、协议、发布设置或 unknown/interrupted 都将原因和链接交给用户自行点击处理,不自动重发、不换 request_id
|
|
29
|
+
const agentWorkflow = 'Agent 只通过 WriteDock MCP 投稿和查询回执,不依赖也不要调用浏览器、Playwright 或其他网页工具。按结果的 next_action 操作:poll_task 用原 task_id/request_id 查询 get_sync_task;report_result 直接向用户汇报逐平台状态和链接。submitted/published 已有扩展确认,不再打开网页验证;验证码、图形验证、协议、发布设置或 unknown/interrupted 都将原因和链接交给用户自行点击处理,不自动重发、不换 request_id。人工接管不代表已经发布。唯一例外是 user_actions 的 type=old_draft:平台有一份用户上次没发完的内容,这篇尚未提交。必须先问用户是否放弃那份旧内容,不得替用户决定;用户明确同意后,才用新的 request_id 只对该平台再调用 publish,并传 discard_old_drafts;用户不同意就保留旧内容,告诉用户这个平台没有发。'
|
|
30
30
|
|
|
31
31
|
export function createMcpServer(bridge: LocalBridge) {
|
|
32
32
|
const server = new McpServer({ name: 'writedock', version: mcpPackage.version }, {
|
|
@@ -40,10 +40,6 @@ export function createMcpServer(bridge: LocalBridge) {
|
|
|
40
40
|
server.registerTool('get_connection_status', {
|
|
41
41
|
description: '检查本机 MCP 与扩展的连接、运行版本和 npm 更新提示。update_available=true 时告知用户重新启动 WriteDock MCP,保留配置;发布任务进行中不要自动重启。旧版服务可能不返回版本字段;latest_version 缺失或 update_checking=true 时,不得声称已是最新版。', inputSchema: {}, annotations: { ...readOnly, openWorldHint: true },
|
|
42
42
|
}, () => wrap(() => bridge.status()))
|
|
43
|
-
server.registerTool('begin_pairing', {
|
|
44
|
-
description: '通常不需要:未连接时,让用户打开 WriteDock 扩展的「连接」页点“连接”即可。只有用户使用旧版扩展时才生成 3 分钟有效的一次性配对码。',
|
|
45
|
-
inputSchema: {}, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
46
|
-
}, () => wrap(() => bridge.beginPairing()))
|
|
47
43
|
server.registerTool('list_platforms', {
|
|
48
44
|
description: '列出扩展实际支持的平台、登录状态、publishModes、对应 tools 及 modeOptions 限制;小红书 article 对应 publish_article,image/video 对应 publish_post。不会返回 Cookie。',
|
|
49
45
|
inputSchema: { force_refresh: z.boolean().optional() }, annotations: { ...readOnly, openWorldHint: true },
|
|
@@ -96,9 +92,9 @@ export function createMcpServer(bridge: LocalBridge) {
|
|
|
96
92
|
}
|
|
97
93
|
server.registerTool('publish', {
|
|
98
94
|
description: agentWorkflow + '首选的发布工具:一次调用发布到用户指定的多个平台。只有用户明确要求发布并指定平台时才调用。按平台依次执行,某个平台未登录、不能接收内容或失败,只跳过该平台,其他平台继续。遇到验证码时平台进入 waiting_verification,立即把原因和链接告诉用户,由用户手动完成,继续查询即可,不要重发。任务结束后按 get_sync_task 的 summary 逐平台告诉用户:哪些成功,哪些未登录(not_logged_in)等。立即返回 task_id,再用 get_sync_task 查询;results 是逐平台结果,warnings 说明为平台做的调整。必须使用稳定 request_id,相同 ID 不会重复提交。部分平台失败时,只针对失败平台用新 request_id 重新提交,unknown 的平台不要重发。旧版扩展不支持时会报错,不会发布。',
|
|
99
|
-
inputSchema: { ...unifiedInput, request_id: requestId },
|
|
95
|
+
inputSchema: { ...unifiedInput, request_id: requestId, discard_old_drafts: z.array(z.enum(['douyin'])).optional().describe('只有用户明确同意放弃平台上次没发完的内容时才传,如 ["douyin"]。放弃后无法恢复。') },
|
|
100
96
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
101
|
-
}, args => wrap(async () => bridge.request('publish', { requestId: args.request_id, ...await unifiedParams(args) })))
|
|
97
|
+
}, args => wrap(async () => bridge.request('publish', { requestId: args.request_id, ...(args.discard_old_drafts ? { discardOldDrafts: args.discard_old_drafts } : {}), ...await unifiedParams(args) })))
|
|
102
98
|
server.registerTool('rehearse_publish', {
|
|
103
99
|
description: '与 publish 参数相同的演练:逐平台打开原生页面,填好内容、上传素材,走到点击发布之前停下,不提交发布。无需 request_id,不影响之后的真实发布。图片可能已上传到平台,部分平台会留下中间草稿,公众号、搜狐号不参与。结果 publicationState=rehearsed 不代表已发布,不得告诉用户内容已发出,也不要自行接着发布。',
|
|
104
100
|
inputSchema: unifiedInput,
|