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