writedock-mcp 0.4.11 → 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 +6 -2
- package/extension/assets/editor-CMMMIxFa.js +76 -0
- 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/src/editor/index.html +1 -1
- package/extension//345/256/211/350/243/205/344/270/216/346/233/264/346/226/260.txt +1 -1
- package/index.cjs +162 -10
- 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 +52 -11
- package/source/src/index.ts +24 -2
- package/source/src/release-info.ts +13 -0
- package/source/src/server.ts +2 -2
- package/source/src/service.ts +82 -0
- package/extension/assets/editor-DSj8NlXD.js +0 -76
package/extension/manifest.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import './assets/index.ts-
|
|
1
|
+
import './assets/index.ts-BeiBzFaR.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-CMMMIxFa.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/purify.es-1yEQDPgc.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/react-vendor-BrXlnJmF.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/task-utils-DwzGcJkm.js">
|
package/index.cjs
CHANGED
|
@@ -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,14 +29961,18 @@ var LocalBridge = class {
|
|
|
29953
29961
|
starting;
|
|
29954
29962
|
primary = false;
|
|
29955
29963
|
extensionId;
|
|
29964
|
+
extensionVersion;
|
|
29956
29965
|
seenNonces = /* @__PURE__ */ new Map();
|
|
29957
29966
|
extensionToken;
|
|
29967
|
+
lastWork = 0;
|
|
29968
|
+
lastSeen = Date.now();
|
|
29958
29969
|
media;
|
|
29959
29970
|
pending = /* @__PURE__ */ new Map();
|
|
29960
29971
|
port;
|
|
29961
29972
|
async start() {
|
|
29962
29973
|
if (this.starting) return this.starting;
|
|
29963
|
-
this.starting = this.
|
|
29974
|
+
this.starting = Promise.resolve(this.options.ensureService?.()).catch(() => {
|
|
29975
|
+
}).then(() => this.listen()).catch(async (error2) => {
|
|
29964
29976
|
if (error2.code !== "EADDRINUSE") throw error2;
|
|
29965
29977
|
this.server = void 0;
|
|
29966
29978
|
this.sockets = void 0;
|
|
@@ -30007,6 +30019,31 @@ var LocalBridge = class {
|
|
|
30007
30019
|
}, 2e4);
|
|
30008
30020
|
this.heartbeat.unref();
|
|
30009
30021
|
}
|
|
30022
|
+
get isPrimary() {
|
|
30023
|
+
return this.primary;
|
|
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
|
+
}
|
|
30034
|
+
/** How long nothing has needed this service. An open extension connection counts as needed. */
|
|
30035
|
+
idleFor() {
|
|
30036
|
+
if (this.client?.readyState === import_websocket.default.OPEN) this.lastSeen = Date.now();
|
|
30037
|
+
return Date.now() - this.lastSeen;
|
|
30038
|
+
}
|
|
30039
|
+
/** Asks the running service to step aside for a newer one. It says no while it has work. */
|
|
30040
|
+
async askServiceToStop() {
|
|
30041
|
+
try {
|
|
30042
|
+
return (await this.httpCall("/stop", {})).accepted === true;
|
|
30043
|
+
} catch {
|
|
30044
|
+
return false;
|
|
30045
|
+
}
|
|
30046
|
+
}
|
|
30010
30047
|
validHost(host) {
|
|
30011
30048
|
return host === `127.0.0.1:${this.port}` || host === `localhost:${this.port}`;
|
|
30012
30049
|
}
|
|
@@ -30018,6 +30055,7 @@ var LocalBridge = class {
|
|
|
30018
30055
|
const previousClient = this.client;
|
|
30019
30056
|
this.client = void 0;
|
|
30020
30057
|
this.extensionId = void 0;
|
|
30058
|
+
this.extensionVersion = void 0;
|
|
30021
30059
|
for (const value of this.pending.values()) {
|
|
30022
30060
|
clearTimeout(value.timer);
|
|
30023
30061
|
value.reject(new Error(reason));
|
|
@@ -30058,6 +30096,7 @@ var LocalBridge = class {
|
|
|
30058
30096
|
clearTimeout(timer);
|
|
30059
30097
|
this.client = ws;
|
|
30060
30098
|
this.extensionId = String(message.extensionId || "");
|
|
30099
|
+
this.extensionVersion = typeof message.extensionVersion === "string" && message.extensionVersion.length <= 40 ? message.extensionVersion : void 0;
|
|
30061
30100
|
ws.send(JSON.stringify({ type: "ready", ...mcpReleaseInfo() }));
|
|
30062
30101
|
return;
|
|
30063
30102
|
}
|
|
@@ -30084,6 +30123,7 @@ var LocalBridge = class {
|
|
|
30084
30123
|
if (this.client !== ws) return;
|
|
30085
30124
|
this.client = void 0;
|
|
30086
30125
|
this.extensionId = void 0;
|
|
30126
|
+
this.extensionVersion = void 0;
|
|
30087
30127
|
for (const value of this.pending.values()) {
|
|
30088
30128
|
clearTimeout(value.timer);
|
|
30089
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"));
|
|
@@ -30109,6 +30149,7 @@ var LocalBridge = class {
|
|
|
30109
30149
|
}
|
|
30110
30150
|
const assetPath = /^\/assets\/([a-f0-9-]{36})$/.exec(req.url || "");
|
|
30111
30151
|
if (assetPath && this.validHost(req.headers.host) && (!req.headers.origin || /^chrome-extension:\/\/[a-p]{32}$/.test(req.headers.origin))) {
|
|
30152
|
+
this.lastWork = this.lastSeen = Date.now();
|
|
30112
30153
|
await this.media.serve(req, res, assetPath[1]);
|
|
30113
30154
|
return;
|
|
30114
30155
|
}
|
|
@@ -30139,10 +30180,18 @@ var LocalBridge = class {
|
|
|
30139
30180
|
for (const [key2, expires] of this.seenNonces) if (expires < now) this.seenNonces.delete(key2);
|
|
30140
30181
|
this.seenNonces.set(nonce, now + 3e4);
|
|
30141
30182
|
responseNonce = nonce;
|
|
30183
|
+
if (req.method === "POST" && req.url === "/stop") {
|
|
30184
|
+
const accepted = this.pending.size === 0 && Date.now() - this.lastWork > 12e4;
|
|
30185
|
+
send(200, { accepted });
|
|
30186
|
+
if (accepted) setImmediate(() => this.options.onStopRequested?.());
|
|
30187
|
+
return;
|
|
30188
|
+
}
|
|
30189
|
+
this.lastSeen = Date.now();
|
|
30142
30190
|
if (req.method === "GET" && req.url === "/status") {
|
|
30143
|
-
send(200,
|
|
30191
|
+
send(200, this.connectionFields());
|
|
30144
30192
|
return;
|
|
30145
30193
|
}
|
|
30194
|
+
this.lastWork = Date.now();
|
|
30146
30195
|
if (req.method === "POST" && req.url === "/assets/register") {
|
|
30147
30196
|
const input2 = JSON.parse(body);
|
|
30148
30197
|
if (typeof input2.path !== "string" || !["image", "video"].includes(input2.kind)) throw new Error("\u7D20\u6750\u53C2\u6570\u4E0D\u6B63\u786E");
|
|
@@ -30179,7 +30228,7 @@ var LocalBridge = class {
|
|
|
30179
30228
|
}
|
|
30180
30229
|
async status() {
|
|
30181
30230
|
await this.start();
|
|
30182
|
-
if (this.primary) return
|
|
30231
|
+
if (this.primary) return this.connectionFields();
|
|
30183
30232
|
try {
|
|
30184
30233
|
return await this.httpCall("/status");
|
|
30185
30234
|
} catch (error2) {
|
|
@@ -30187,7 +30236,7 @@ var LocalBridge = class {
|
|
|
30187
30236
|
this.starting = void 0;
|
|
30188
30237
|
await this.start();
|
|
30189
30238
|
if (this.primary) return { connected: false, protocol: "writedock/1", ...mcpReleaseInfo() };
|
|
30190
|
-
|
|
30239
|
+
return this.httpCall("/status");
|
|
30191
30240
|
}
|
|
30192
30241
|
}
|
|
30193
30242
|
async request(method, params = {}) {
|
|
@@ -30202,7 +30251,10 @@ var LocalBridge = class {
|
|
|
30202
30251
|
}
|
|
30203
30252
|
async registerMedia(path, kind) {
|
|
30204
30253
|
await this.status();
|
|
30205
|
-
if (this.primary)
|
|
30254
|
+
if (this.primary) {
|
|
30255
|
+
this.lastWork = this.lastSeen = Date.now();
|
|
30256
|
+
return this.media.register(path, kind);
|
|
30257
|
+
}
|
|
30206
30258
|
return await this.httpCall("/assets/register", { path, kind });
|
|
30207
30259
|
}
|
|
30208
30260
|
async requestConnected(method, params) {
|
|
@@ -30220,6 +30272,9 @@ var LocalBridge = class {
|
|
|
30220
30272
|
this.pending.delete(id);
|
|
30221
30273
|
reject(error2);
|
|
30222
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;
|
|
30223
30278
|
});
|
|
30224
30279
|
}
|
|
30225
30280
|
async close() {
|
|
@@ -50329,7 +50384,7 @@ var postSource = {
|
|
|
50329
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"),
|
|
50330
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")
|
|
50331
50386
|
};
|
|
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";
|
|
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";
|
|
50333
50388
|
function createMcpServer(bridge) {
|
|
50334
50389
|
const server = new McpServer({ name: "writedock", version: package_default.version }, {
|
|
50335
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'
|
|
@@ -50339,7 +50394,7 @@ function createMcpServer(bridge) {
|
|
|
50339
50394
|
})).catch((error2) => ({ content: [{ type: "text", text: error2 instanceof Error ? error2.message : String(error2) }], isError: true }));
|
|
50340
50395
|
const readOnly = { readOnlyHint: true, destructiveHint: false, openWorldHint: false };
|
|
50341
50396
|
server.registerTool("get_connection_status", {
|
|
50342
|
-
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",
|
|
50343
50398
|
inputSchema: {},
|
|
50344
50399
|
annotations: { ...readOnly, openWorldHint: true }
|
|
50345
50400
|
}, () => wrap(() => bridge.status()));
|
|
@@ -50615,12 +50670,88 @@ async function runSetup(home = (0, import_node_os3.homedir)(), log = console.log
|
|
|
50615
50670
|
return found;
|
|
50616
50671
|
}
|
|
50617
50672
|
|
|
50673
|
+
// src/service.ts
|
|
50674
|
+
var import_node_child_process2 = require("child_process");
|
|
50675
|
+
var import_node_fs2 = require("fs");
|
|
50676
|
+
var import_node_os4 = require("os");
|
|
50677
|
+
var import_node_path6 = require("path");
|
|
50678
|
+
var sleep = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
50679
|
+
async function probe(port) {
|
|
50680
|
+
try {
|
|
50681
|
+
const response = await fetch(`http://127.0.0.1:${port}/hello`, { signal: AbortSignal.timeout(1500), cache: "no-store" });
|
|
50682
|
+
const value = await response.json();
|
|
50683
|
+
return value.service === "writedock-mcp" && typeof value.version === "string" ? { version: value.version } : "other";
|
|
50684
|
+
} catch (error2) {
|
|
50685
|
+
return error2 instanceof Error && error2.name === "TimeoutError" ? "other" : "none";
|
|
50686
|
+
}
|
|
50687
|
+
}
|
|
50688
|
+
async function until(check2, timeoutMs) {
|
|
50689
|
+
const end = Date.now() + timeoutMs;
|
|
50690
|
+
while (Date.now() < end) {
|
|
50691
|
+
if (await check2()) return true;
|
|
50692
|
+
await sleep(200);
|
|
50693
|
+
}
|
|
50694
|
+
return false;
|
|
50695
|
+
}
|
|
50696
|
+
function startDetached() {
|
|
50697
|
+
const directory = dataDirectory();
|
|
50698
|
+
(0, import_node_fs2.mkdirSync)(directory, { recursive: true, mode: 448 });
|
|
50699
|
+
const log = (0, import_node_fs2.openSync)((0, import_node_path6.join)(directory, "service.log"), "w");
|
|
50700
|
+
try {
|
|
50701
|
+
const child = (0, import_node_child_process2.spawn)(process.execPath, [__filename, "--service"], { detached: true, stdio: ["ignore", "ignore", log], windowsHide: true, cwd: (0, import_node_os4.homedir)() });
|
|
50702
|
+
child.on("error", () => {
|
|
50703
|
+
});
|
|
50704
|
+
child.unref();
|
|
50705
|
+
} finally {
|
|
50706
|
+
(0, import_node_fs2.closeSync)(log);
|
|
50707
|
+
}
|
|
50708
|
+
}
|
|
50709
|
+
async function ensureService(bridge, port) {
|
|
50710
|
+
let found = await probe(port);
|
|
50711
|
+
if (typeof found === "object" && newerVersion(package_default.version, found.version)) {
|
|
50712
|
+
if (!await bridge.askServiceToStop()) return;
|
|
50713
|
+
if (!await until(async () => await probe(port) === "none", 5e3)) return;
|
|
50714
|
+
found = "none";
|
|
50715
|
+
}
|
|
50716
|
+
if (found !== "none") return;
|
|
50717
|
+
startDetached();
|
|
50718
|
+
await until(async () => typeof await probe(port) === "object", 1e4);
|
|
50719
|
+
}
|
|
50720
|
+
function watchService(bridge, port) {
|
|
50721
|
+
let running = false;
|
|
50722
|
+
const timer = setInterval(() => {
|
|
50723
|
+
if (running) return;
|
|
50724
|
+
running = true;
|
|
50725
|
+
void ensureService(bridge, port).catch(() => {
|
|
50726
|
+
}).finally(() => {
|
|
50727
|
+
running = false;
|
|
50728
|
+
});
|
|
50729
|
+
}, 6e4);
|
|
50730
|
+
timer.unref();
|
|
50731
|
+
}
|
|
50732
|
+
var IDLE_CHECK_MS = 5e3;
|
|
50733
|
+
async function runService(bridge) {
|
|
50734
|
+
await bridge.start();
|
|
50735
|
+
if (!bridge.isPrimary) process.exit(0);
|
|
50736
|
+
const idleLimit = Number(process.env.WRITEDOCK_SERVICE_IDLE_MS) || 2 * 60 * 6e4;
|
|
50737
|
+
setInterval(() => {
|
|
50738
|
+
if (bridge.idleFor() >= idleLimit) void bridge.close().finally(() => process.exit(0));
|
|
50739
|
+
}, IDLE_CHECK_MS);
|
|
50740
|
+
const stop = () => {
|
|
50741
|
+
void bridge.close().finally(() => process.exit(0));
|
|
50742
|
+
};
|
|
50743
|
+
process.on("SIGINT", stop);
|
|
50744
|
+
process.on("SIGTERM", stop);
|
|
50745
|
+
}
|
|
50746
|
+
|
|
50618
50747
|
// src/index.ts
|
|
50619
50748
|
async function main() {
|
|
50620
50749
|
const port = Number(process.env.WRITEDOCK_PORT || 9647);
|
|
50621
50750
|
const identity = await loadIdentity(port, process.env.WRITEDOCK_TOKEN);
|
|
50622
|
-
const
|
|
50751
|
+
const background = !process.env.WRITEDOCK_NO_SERVICE;
|
|
50752
|
+
const bridge = new LocalBridge({ token: identity.token, port, credentials: identity, ...background ? { ensureService: () => ensureService(bridge, port) } : {} });
|
|
50623
50753
|
await bridge.start();
|
|
50754
|
+
if (background) watchService(bridge, port);
|
|
50624
50755
|
const server = createMcpServer(bridge);
|
|
50625
50756
|
let closing = false;
|
|
50626
50757
|
const close = async () => {
|
|
@@ -50643,10 +50774,31 @@ async function main() {
|
|
|
50643
50774
|
};
|
|
50644
50775
|
await server.connect(new StdioServerTransport());
|
|
50645
50776
|
}
|
|
50777
|
+
async function service() {
|
|
50778
|
+
const port = Number(process.env.WRITEDOCK_PORT || 9647);
|
|
50779
|
+
const identity = await loadIdentity(port, process.env.WRITEDOCK_TOKEN);
|
|
50780
|
+
const bridge = new LocalBridge({ token: identity.token, port, credentials: identity, onStopRequested: () => {
|
|
50781
|
+
void bridge.close().finally(() => process.exit(0));
|
|
50782
|
+
} });
|
|
50783
|
+
await runService(bridge);
|
|
50784
|
+
}
|
|
50785
|
+
async function setup() {
|
|
50786
|
+
await runSetup();
|
|
50787
|
+
const port = Number(process.env.WRITEDOCK_PORT || 9647);
|
|
50788
|
+
const identity = await loadIdentity(port, process.env.WRITEDOCK_TOKEN);
|
|
50789
|
+
await ensureService(new LocalBridge({ token: identity.token, port, credentials: identity }), port).catch(() => {
|
|
50790
|
+
});
|
|
50791
|
+
process.exit(0);
|
|
50792
|
+
}
|
|
50646
50793
|
if (process.argv.slice(2).some((argument) => argument === "--version" || argument === "-v")) {
|
|
50647
50794
|
console.log(package_default.version);
|
|
50648
50795
|
} else if (process.argv[2] === "setup") {
|
|
50649
|
-
|
|
50796
|
+
setup().catch((error2) => {
|
|
50797
|
+
console.error("[WriteDock MCP]", error2 instanceof Error ? error2.message : String(error2));
|
|
50798
|
+
process.exit(1);
|
|
50799
|
+
});
|
|
50800
|
+
} else if (process.argv[2] === "--service") {
|
|
50801
|
+
service().catch((error2) => {
|
|
50650
50802
|
console.error("[WriteDock MCP]", error2 instanceof Error ? error2.message : String(error2));
|
|
50651
50803
|
process.exit(1);
|
|
50652
50804
|
});
|
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,12 +27,15 @@ 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
|
|
33
|
+
private lastWork = 0
|
|
34
|
+
private lastSeen = Date.now()
|
|
32
35
|
private media: MediaStore
|
|
33
36
|
private pending = new Map<string, { resolve: (value: unknown) => void; reject: (error: Error) => void; timer: ReturnType<typeof setTimeout> }>()
|
|
34
37
|
readonly port: number
|
|
35
|
-
constructor(private options: { token: string; port?: number; timeoutMs?: number; credentials?: { getExtensionToken: () => Promise<string>; setExtensionToken: (token: string) => Promise<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 }) {
|
|
36
39
|
this.port = options.port ?? 9647
|
|
37
40
|
this.extensionToken = options.token
|
|
38
41
|
this.media = new MediaStore(this.port)
|
|
@@ -42,7 +45,7 @@ export class LocalBridge {
|
|
|
42
45
|
|
|
43
46
|
async start(): Promise<void> {
|
|
44
47
|
if (this.starting) return this.starting
|
|
45
|
-
this.starting = this.listen().catch(async error => {
|
|
48
|
+
this.starting = Promise.resolve(this.options.ensureService?.()).catch(() => {}).then(() => this.listen()).catch(async error => {
|
|
46
49
|
if ((error as NodeJS.ErrnoException).code !== 'EADDRINUSE') throw error
|
|
47
50
|
this.server = undefined
|
|
48
51
|
this.sockets = undefined
|
|
@@ -78,6 +81,31 @@ export class LocalBridge {
|
|
|
78
81
|
this.heartbeat.unref()
|
|
79
82
|
}
|
|
80
83
|
|
|
84
|
+
get isPrimary() { return this.primary }
|
|
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
|
+
|
|
97
|
+
/** How long nothing has needed this service. An open extension connection counts as needed. */
|
|
98
|
+
idleFor() {
|
|
99
|
+
if (this.client?.readyState === WebSocket.OPEN) this.lastSeen = Date.now()
|
|
100
|
+
return Date.now() - this.lastSeen
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Asks the running service to step aside for a newer one. It says no while it has work. */
|
|
104
|
+
async askServiceToStop() {
|
|
105
|
+
try { return (await this.httpCall('/stop', {})).accepted === true }
|
|
106
|
+
catch { return false } // Versions before this one have no such route, and leave with their agent.
|
|
107
|
+
}
|
|
108
|
+
|
|
81
109
|
private validHost(host?: string) { return host === `127.0.0.1:${this.port}` || host === `localhost:${this.port}` }
|
|
82
110
|
|
|
83
111
|
/** Replaces the extension credential and drops whatever was using the old one. */
|
|
@@ -86,7 +114,7 @@ export class LocalBridge {
|
|
|
86
114
|
await this.options.credentials?.setExtensionToken(token)
|
|
87
115
|
this.extensionToken = token
|
|
88
116
|
const previousClient = this.client
|
|
89
|
-
this.client = undefined; this.extensionId = undefined
|
|
117
|
+
this.client = undefined; this.extensionId = undefined; this.extensionVersion = undefined
|
|
90
118
|
for (const value of this.pending.values()) { clearTimeout(value.timer); value.reject(new Error(reason)) }
|
|
91
119
|
this.pending.clear()
|
|
92
120
|
previousClient?.close(1000, 'Extension connection replaced')
|
|
@@ -117,7 +145,7 @@ export class LocalBridge {
|
|
|
117
145
|
}
|
|
118
146
|
if (this.client?.readyState === WebSocket.OPEN) { ws.close(1008, 'An extension is already connected'); return }
|
|
119
147
|
authenticated = true; clearTimeout(timer)
|
|
120
|
-
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
|
|
121
149
|
ws.send(JSON.stringify({ type: 'ready', ...mcpReleaseInfo() }))
|
|
122
150
|
return
|
|
123
151
|
}
|
|
@@ -139,7 +167,7 @@ export class LocalBridge {
|
|
|
139
167
|
ws.on('close', () => {
|
|
140
168
|
clearTimeout(timer)
|
|
141
169
|
if (this.client !== ws) return
|
|
142
|
-
this.client = undefined; this.extensionId = undefined
|
|
170
|
+
this.client = undefined; this.extensionId = undefined; this.extensionVersion = undefined
|
|
143
171
|
for (const value of this.pending.values()) {
|
|
144
172
|
clearTimeout(value.timer)
|
|
145
173
|
value.reject(new Error('扩展连接已断开。若已提交同步,请按任务 ID 查询结果,不要更换 request_id 自动重发。'))
|
|
@@ -165,6 +193,7 @@ export class LocalBridge {
|
|
|
165
193
|
}
|
|
166
194
|
const assetPath = /^\/assets\/([a-f0-9-]{36})$/.exec(req.url || '')
|
|
167
195
|
if (assetPath && this.validHost(req.headers.host) && (!req.headers.origin || /^chrome-extension:\/\/[a-p]{32}$/.test(req.headers.origin))) {
|
|
196
|
+
this.lastWork = this.lastSeen = Date.now()
|
|
168
197
|
await this.media.serve(req, res, assetPath[1]); return
|
|
169
198
|
}
|
|
170
199
|
if (!this.validHost(req.headers.host) || req.headers.origin !== undefined) {
|
|
@@ -188,9 +217,17 @@ export class LocalBridge {
|
|
|
188
217
|
for (const [key, expires] of this.seenNonces) if (expires < now) this.seenNonces.delete(key)
|
|
189
218
|
this.seenNonces.set(nonce, now + 30_000)
|
|
190
219
|
responseNonce = nonce
|
|
220
|
+
if (req.method === 'POST' && req.url === '/stop') {
|
|
221
|
+
const accepted = this.pending.size === 0 && Date.now() - this.lastWork > 120_000
|
|
222
|
+
send(200, { accepted })
|
|
223
|
+
if (accepted) setImmediate(() => this.options.onStopRequested?.())
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
this.lastSeen = Date.now() // Status checks keep the service around, but only real work stops it being replaced.
|
|
191
227
|
if (req.method === 'GET' && req.url === '/status') {
|
|
192
|
-
send(200,
|
|
228
|
+
send(200, this.connectionFields()); return
|
|
193
229
|
}
|
|
230
|
+
this.lastWork = Date.now()
|
|
194
231
|
if (req.method === 'POST' && req.url === '/assets/register') {
|
|
195
232
|
const input = JSON.parse(body)
|
|
196
233
|
if (typeof input.path !== 'string' || !['image', 'video'].includes(input.kind)) throw new Error('素材参数不正确')
|
|
@@ -223,7 +260,7 @@ export class LocalBridge {
|
|
|
223
260
|
|
|
224
261
|
async status() {
|
|
225
262
|
await this.start()
|
|
226
|
-
if (this.primary) return
|
|
263
|
+
if (this.primary) return this.connectionFields()
|
|
227
264
|
try { return await this.httpCall('/status') }
|
|
228
265
|
catch (error) {
|
|
229
266
|
if (!(error instanceof TypeError)) throw error
|
|
@@ -231,7 +268,7 @@ export class LocalBridge {
|
|
|
231
268
|
this.starting = undefined
|
|
232
269
|
await this.start()
|
|
233
270
|
if (this.primary) return { connected: false, protocol: 'writedock/1', ...mcpReleaseInfo() }
|
|
234
|
-
|
|
271
|
+
return this.httpCall('/status') // A background service took the port in the meantime.
|
|
235
272
|
}
|
|
236
273
|
}
|
|
237
274
|
|
|
@@ -248,14 +285,14 @@ export class LocalBridge {
|
|
|
248
285
|
|
|
249
286
|
async registerMedia(path: string, kind: 'image' | 'video'): Promise<MediaAsset> {
|
|
250
287
|
await this.status()
|
|
251
|
-
if (this.primary) return this.media.register(path, kind)
|
|
288
|
+
if (this.primary) { this.lastWork = this.lastSeen = Date.now(); return this.media.register(path, kind) }
|
|
252
289
|
return await this.httpCall('/assets/register', { path, kind }) as unknown as MediaAsset
|
|
253
290
|
}
|
|
254
291
|
|
|
255
292
|
private async requestConnected(method: string, params: Record<string, unknown>): Promise<unknown> {
|
|
256
293
|
if (this.client?.readyState !== WebSocket.OPEN) throw new Error('扩展未连接,这次没有发布。请让用户打开 Chrome 或 Edge,连接过的扩展通常会自动连上,然后再试;如果还是不行,打开 WriteDock 扩展点“连接”。无需重装 MCP。')
|
|
257
294
|
const id = randomUUID()
|
|
258
|
-
return new Promise((resolve, reject) => {
|
|
295
|
+
return new Promise<unknown>((resolve, reject) => {
|
|
259
296
|
const timer = setTimeout(() => {
|
|
260
297
|
this.pending.delete(id)
|
|
261
298
|
reject(new Error('扩展响应超时;同步任务可能仍在运行,请查询任务状态。'))
|
|
@@ -265,6 +302,10 @@ export class LocalBridge {
|
|
|
265
302
|
if (!error) return
|
|
266
303
|
clearTimeout(timer); this.pending.delete(id); reject(error)
|
|
267
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
|
|
268
309
|
})
|
|
269
310
|
}
|
|
270
311
|
|
package/source/src/index.ts
CHANGED
|
@@ -5,12 +5,16 @@ import { createMcpServer } from './server'
|
|
|
5
5
|
import { loadIdentity } from './identity'
|
|
6
6
|
import mcpPackage from '../package.json'
|
|
7
7
|
import { runSetup } from './setup'
|
|
8
|
+
import { ensureService, runService, watchService } from './service'
|
|
8
9
|
|
|
9
10
|
async function main() {
|
|
10
11
|
const port = Number(process.env.WRITEDOCK_PORT || 9647)
|
|
11
12
|
const identity = await loadIdentity(port, process.env.WRITEDOCK_TOKEN)
|
|
12
|
-
|
|
13
|
+
// Set only for tests that need one process owning the port by itself.
|
|
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) } : {}) })
|
|
13
16
|
await bridge.start()
|
|
17
|
+
if (background) watchService(bridge, port)
|
|
14
18
|
const server = createMcpServer(bridge)
|
|
15
19
|
let closing = false
|
|
16
20
|
const close = async () => { if (closing) return; closing = true; await bridge.close(); await server.close() }
|
|
@@ -20,11 +24,29 @@ async function main() {
|
|
|
20
24
|
server.server.onclose = () => { void bridge.close() }
|
|
21
25
|
await server.connect(new StdioServerTransport())
|
|
22
26
|
}
|
|
27
|
+
async function service() {
|
|
28
|
+
const port = Number(process.env.WRITEDOCK_PORT || 9647)
|
|
29
|
+
const identity = await loadIdentity(port, process.env.WRITEDOCK_TOKEN)
|
|
30
|
+
const bridge: LocalBridge = new LocalBridge({ token: identity.token, port, credentials: identity, onStopRequested: () => { void bridge.close().finally(() => process.exit(0)) } })
|
|
31
|
+
await runService(bridge)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Setup starts the service too, so 连接 appears even with an agent that only runs tools when it needs them. */
|
|
35
|
+
async function setup() {
|
|
36
|
+
await runSetup()
|
|
37
|
+
const port = Number(process.env.WRITEDOCK_PORT || 9647)
|
|
38
|
+
const identity = await loadIdentity(port, process.env.WRITEDOCK_TOKEN)
|
|
39
|
+
await ensureService(new LocalBridge({ token: identity.token, port, credentials: identity }), port).catch(() => {})
|
|
40
|
+
process.exit(0)
|
|
41
|
+
}
|
|
42
|
+
|
|
23
43
|
if (process.argv.slice(2).some(argument => argument === '--version' || argument === '-v')) {
|
|
24
44
|
// Allow inspecting an npm archive without starting a bridge or reading pairing credentials.
|
|
25
45
|
console.log(mcpPackage.version)
|
|
26
46
|
} else if (process.argv[2] === 'setup') {
|
|
27
|
-
|
|
47
|
+
setup().catch(error => { console.error('[WriteDock MCP]', error instanceof Error ? error.message : String(error)); process.exit(1) })
|
|
48
|
+
} else if (process.argv[2] === '--service') {
|
|
49
|
+
service().catch(error => { console.error('[WriteDock MCP]', error instanceof Error ? error.message : String(error)); process.exit(1) })
|
|
28
50
|
} else {
|
|
29
51
|
main().catch(error => { console.error('[WriteDock MCP]', error instanceof Error ? error.message : String(error)); process.exit(1) })
|
|
30
52
|
}
|
|
@@ -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
|
+
}
|