writedock-mcp 0.4.12 → 0.4.13
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 +3 -1
- package/extension/assets/{index.ts-CSY6FJ6h.js → index.ts-BeiBzFaR.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 +29 -5
- package/package.json +1 -1
- package/source/package.json +1 -1
- package/source/src/__tests__/update-notice.test.ts +27 -0
- package/source/src/bridge.ts +23 -7
- 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-BeiBzFaR.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.13",
|
|
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,15 @@ 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
|
+
return { connected, extensionId: this.extensionId, ...this.extensionVersion ? { extension_version: this.extensionVersion } : {}, ...notice ? { update_notice: notice } : {}, protocol: "writedock/1", ...mcpReleaseInfo() };
|
|
30033
|
+
}
|
|
30016
30034
|
/** How long nothing has needed this service. An open extension connection counts as needed. */
|
|
30017
30035
|
idleFor() {
|
|
30018
30036
|
if (this.client?.readyState === import_websocket.default.OPEN) this.lastSeen = Date.now();
|
|
@@ -30037,6 +30055,7 @@ var LocalBridge = class {
|
|
|
30037
30055
|
const previousClient = this.client;
|
|
30038
30056
|
this.client = void 0;
|
|
30039
30057
|
this.extensionId = void 0;
|
|
30058
|
+
this.extensionVersion = void 0;
|
|
30040
30059
|
for (const value of this.pending.values()) {
|
|
30041
30060
|
clearTimeout(value.timer);
|
|
30042
30061
|
value.reject(new Error(reason));
|
|
@@ -30077,6 +30096,7 @@ var LocalBridge = class {
|
|
|
30077
30096
|
clearTimeout(timer);
|
|
30078
30097
|
this.client = ws;
|
|
30079
30098
|
this.extensionId = String(message.extensionId || "");
|
|
30099
|
+
this.extensionVersion = typeof message.extensionVersion === "string" && message.extensionVersion.length <= 40 ? message.extensionVersion : void 0;
|
|
30080
30100
|
ws.send(JSON.stringify({ type: "ready", ...mcpReleaseInfo() }));
|
|
30081
30101
|
return;
|
|
30082
30102
|
}
|
|
@@ -30103,6 +30123,7 @@ var LocalBridge = class {
|
|
|
30103
30123
|
if (this.client !== ws) return;
|
|
30104
30124
|
this.client = void 0;
|
|
30105
30125
|
this.extensionId = void 0;
|
|
30126
|
+
this.extensionVersion = void 0;
|
|
30106
30127
|
for (const value of this.pending.values()) {
|
|
30107
30128
|
clearTimeout(value.timer);
|
|
30108
30129
|
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 +30188,7 @@ var LocalBridge = class {
|
|
|
30167
30188
|
}
|
|
30168
30189
|
this.lastSeen = Date.now();
|
|
30169
30190
|
if (req.method === "GET" && req.url === "/status") {
|
|
30170
|
-
send(200,
|
|
30191
|
+
send(200, this.connectionFields());
|
|
30171
30192
|
return;
|
|
30172
30193
|
}
|
|
30173
30194
|
this.lastWork = Date.now();
|
|
@@ -30207,7 +30228,7 @@ var LocalBridge = class {
|
|
|
30207
30228
|
}
|
|
30208
30229
|
async status() {
|
|
30209
30230
|
await this.start();
|
|
30210
|
-
if (this.primary) return
|
|
30231
|
+
if (this.primary) return this.connectionFields();
|
|
30211
30232
|
try {
|
|
30212
30233
|
return await this.httpCall("/status");
|
|
30213
30234
|
} catch (error2) {
|
|
@@ -30251,6 +30272,9 @@ var LocalBridge = class {
|
|
|
30251
30272
|
this.pending.delete(id);
|
|
30252
30273
|
reject(error2);
|
|
30253
30274
|
});
|
|
30275
|
+
}).then((result) => {
|
|
30276
|
+
const notice = result && typeof result === "object" && result.finished === true ? this.notice() : void 0;
|
|
30277
|
+
return notice ? { ...result, update_notice: notice } : result;
|
|
30254
30278
|
});
|
|
30255
30279
|
}
|
|
30256
30280
|
async close() {
|
|
@@ -50360,7 +50384,7 @@ var postSource = {
|
|
|
50360
50384
|
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
50385
|
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
50386
|
};
|
|
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";
|
|
50387
|
+
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
50388
|
function createMcpServer(bridge) {
|
|
50365
50389
|
const server = new McpServer({ name: "writedock", version: package_default.version }, {
|
|
50366
50390
|
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 +50394,7 @@ function createMcpServer(bridge) {
|
|
|
50370
50394
|
})).catch((error2) => ({ content: [{ type: "text", text: error2 instanceof Error ? error2.message : String(error2) }], isError: true }));
|
|
50371
50395
|
const readOnly = { readOnlyHint: true, destructiveHint: false, openWorldHint: false };
|
|
50372
50396
|
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",
|
|
50397
|
+
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_bundle_version \u53EA\u662F npm \u5305\u5185\u7F6E\u7684\u6269\u5C55\u7248\u672C\uFF0Cextension_version \u624D\u662F\u7528\u6237\u6B63\u5728\u7528\u7684\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
50398
|
inputSchema: {},
|
|
50375
50399
|
annotations: { ...readOnly, openWorldHint: true }
|
|
50376
50400
|
}, () => wrap(() => bridge.status()));
|
package/package.json
CHANGED
package/source/package.json
CHANGED
|
@@ -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
|
|
@@ -82,6 +83,17 @@ 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
|
+
return { connected, extensionId: this.extensionId, ...(this.extensionVersion ? { extension_version: this.extensionVersion } : {}), ...(notice ? { update_notice: notice } : {}), protocol: 'writedock/1', ...mcpReleaseInfo() }
|
|
95
|
+
}
|
|
96
|
+
|
|
85
97
|
/** How long nothing has needed this service. An open extension connection counts as needed. */
|
|
86
98
|
idleFor() {
|
|
87
99
|
if (this.client?.readyState === WebSocket.OPEN) this.lastSeen = Date.now()
|
|
@@ -102,7 +114,7 @@ export class LocalBridge {
|
|
|
102
114
|
await this.options.credentials?.setExtensionToken(token)
|
|
103
115
|
this.extensionToken = token
|
|
104
116
|
const previousClient = this.client
|
|
105
|
-
this.client = undefined; this.extensionId = undefined
|
|
117
|
+
this.client = undefined; this.extensionId = undefined; this.extensionVersion = undefined
|
|
106
118
|
for (const value of this.pending.values()) { clearTimeout(value.timer); value.reject(new Error(reason)) }
|
|
107
119
|
this.pending.clear()
|
|
108
120
|
previousClient?.close(1000, 'Extension connection replaced')
|
|
@@ -133,7 +145,7 @@ export class LocalBridge {
|
|
|
133
145
|
}
|
|
134
146
|
if (this.client?.readyState === WebSocket.OPEN) { ws.close(1008, 'An extension is already connected'); return }
|
|
135
147
|
authenticated = true; clearTimeout(timer)
|
|
136
|
-
this.client = ws; this.extensionId = String(message.extensionId || '')
|
|
148
|
+
this.client = ws; this.extensionId = String(message.extensionId || ''); this.extensionVersion = typeof message.extensionVersion === 'string' && message.extensionVersion.length <= 40 ? message.extensionVersion : undefined
|
|
137
149
|
ws.send(JSON.stringify({ type: 'ready', ...mcpReleaseInfo() }))
|
|
138
150
|
return
|
|
139
151
|
}
|
|
@@ -155,7 +167,7 @@ export class LocalBridge {
|
|
|
155
167
|
ws.on('close', () => {
|
|
156
168
|
clearTimeout(timer)
|
|
157
169
|
if (this.client !== ws) return
|
|
158
|
-
this.client = undefined; this.extensionId = undefined
|
|
170
|
+
this.client = undefined; this.extensionId = undefined; this.extensionVersion = undefined
|
|
159
171
|
for (const value of this.pending.values()) {
|
|
160
172
|
clearTimeout(value.timer)
|
|
161
173
|
value.reject(new Error('扩展连接已断开。若已提交同步,请按任务 ID 查询结果,不要更换 request_id 自动重发。'))
|
|
@@ -213,7 +225,7 @@ export class LocalBridge {
|
|
|
213
225
|
}
|
|
214
226
|
this.lastSeen = Date.now() // Status checks keep the service around, but only real work stops it being replaced.
|
|
215
227
|
if (req.method === 'GET' && req.url === '/status') {
|
|
216
|
-
send(200,
|
|
228
|
+
send(200, this.connectionFields()); return
|
|
217
229
|
}
|
|
218
230
|
this.lastWork = Date.now()
|
|
219
231
|
if (req.method === 'POST' && req.url === '/assets/register') {
|
|
@@ -248,7 +260,7 @@ export class LocalBridge {
|
|
|
248
260
|
|
|
249
261
|
async status() {
|
|
250
262
|
await this.start()
|
|
251
|
-
if (this.primary) return
|
|
263
|
+
if (this.primary) return this.connectionFields()
|
|
252
264
|
try { return await this.httpCall('/status') }
|
|
253
265
|
catch (error) {
|
|
254
266
|
if (!(error instanceof TypeError)) throw error
|
|
@@ -280,7 +292,7 @@ export class LocalBridge {
|
|
|
280
292
|
private async requestConnected(method: string, params: Record<string, unknown>): Promise<unknown> {
|
|
281
293
|
if (this.client?.readyState !== WebSocket.OPEN) throw new Error('扩展未连接,这次没有发布。请让用户打开 Chrome 或 Edge,连接过的扩展通常会自动连上,然后再试;如果还是不行,打开 WriteDock 扩展点“连接”。无需重装 MCP。')
|
|
282
294
|
const id = randomUUID()
|
|
283
|
-
return new Promise((resolve, reject) => {
|
|
295
|
+
return new Promise<unknown>((resolve, reject) => {
|
|
284
296
|
const timer = setTimeout(() => {
|
|
285
297
|
this.pending.delete(id)
|
|
286
298
|
reject(new Error('扩展响应超时;同步任务可能仍在运行,请查询任务状态。'))
|
|
@@ -290,6 +302,10 @@ export class LocalBridge {
|
|
|
290
302
|
if (!error) return
|
|
291
303
|
clearTimeout(timer); this.pending.delete(id); reject(error)
|
|
292
304
|
})
|
|
305
|
+
}).then(result => {
|
|
306
|
+
// Only on a finished task, so the agent says it once at the end and never in the middle of a publication.
|
|
307
|
+
const notice = result && typeof result === 'object' && (result as { finished?: unknown }).finished === true ? this.notice() : undefined
|
|
308
|
+
return notice ? { ...result as object, update_notice: notice } : result
|
|
293
309
|
})
|
|
294
310
|
}
|
|
295
311
|
|
|
@@ -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_bundle_version 只是 npm 包内置的扩展版本,extension_version 才是用户正在用的。旧版服务可能不返回版本字段;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。',
|