writedock-mcp 0.4.11 → 0.4.12
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 +4 -2
- package/extension/assets/editor-CMMMIxFa.js +76 -0
- package/extension/manifest.json +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 +134 -6
- package/package.json +1 -1
- package/source/package.json +1 -1
- package/source/src/bridge.ts +29 -4
- package/source/src/index.ts +24 -2
- package/source/src/service.ts +82 -0
- package/extension/assets/editor-DSj8NlXD.js +0 -76
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
连接你自己的 agent 与 WriteDock 浏览器扩展:agent 写稿并准备图片或视频,一次调用发布到多个平台,并按平台查询结果。
|
|
4
4
|
|
|
5
|
-
MCP 运行在你自己的电脑上,只监听 `127.0.0.1
|
|
5
|
+
MCP 运行在你自己的电脑上,只监听 `127.0.0.1`。第一次启动时会在后台开一个服务,负责端口和扩展连接,所以 agent 退出或只在需要时才启动 MCP,都不影响连接;没有扩展和 agent 用它超过 2 小时,它会自己退出,下次用到时再启动。平台 Cookie 留在浏览器里,稿件和媒体不经过 npm。
|
|
6
6
|
|
|
7
7
|
## 安装
|
|
8
8
|
|
|
@@ -115,7 +115,7 @@ Windows 上要写成 `"command": "cmd"`、`"args": ["/c", "npx", "-y", "--prefer
|
|
|
115
115
|
|
|
116
116
|
## 更新
|
|
117
117
|
|
|
118
|
-
- **MCP:** `--prefer-online`
|
|
118
|
+
- **MCP:** `--prefer-online` 在每次启动时检查新版,重启 agent 即可。新版会在旧的后台服务空闲后替换它;有任务时会等,通常几分钟内完成。
|
|
119
119
|
- **扩展:** 新版 MCP 内置更新的扩展时,扩展的「连接 agent」页会出现「更新扩展」。用户点一下,MCP 覆盖扩展目录,扩展自行重新加载;发布任务进行中不能更新。
|
|
120
120
|
|
|
121
121
|
连接资料保存在用户数据目录的 `identity-端口.json`:
|
|
@@ -124,6 +124,8 @@ Windows 上要写成 `"command": "cmd"`、`"args": ["/c", "npx", "-y", "--prefer
|
|
|
124
124
|
- Windows:`%LOCALAPPDATA%/WriteDock/mcp/`
|
|
125
125
|
- Linux:`${XDG_DATA_HOME:-~/.local/share}/writedock/mcp/`
|
|
126
126
|
|
|
127
|
+
后台服务的日志是同目录下的 `service.log`。
|
|
128
|
+
|
|
127
129
|
卸载重装扩展后,在扩展里再点一次「连接」即可,不需要重装 MCP。`npx -y writedock-mcp@latest --version` 只显示版本。
|
|
128
130
|
|
|
129
131
|
对应源码位于包内 `source/`。本服务遵循 GPL-3.0-only,见 `LICENSE`、`NOTICE.md` 和 `THIRD_PARTY_NOTICES.txt`。
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import{p as oe}from"./purify.es-1yEQDPgc.js";import{r as i,j as e,c as de}from"./react-vendor-BrXlnJmF.js";import{S as B,s as P,e as he}from"./task-utils-DwzGcJkm.js";/**
|
|
2
|
+
* @license lucide-react v0.303.0 - ISC
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the ISC license.
|
|
5
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/var ue={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};/**
|
|
7
|
+
* @license lucide-react v0.303.0 - ISC
|
|
8
|
+
*
|
|
9
|
+
* This source code is licensed under the ISC license.
|
|
10
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
11
|
+
*/const xe=t=>t.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase().trim(),m=(t,d)=>{const l=i.forwardRef(({color:g="currentColor",size:o=24,strokeWidth:f=2,absoluteStrokeWidth:x,className:y="",children:u,...N},c)=>i.createElement("svg",{ref:c,...ue,width:o,height:o,stroke:g,strokeWidth:x?Number(f)*24/Number(o):f,className:["lucide",`lucide-${xe(t)}`,y].join(" "),...N},[...d.map(([v,k])=>i.createElement(v,k)),...Array.isArray(u)?u:[u]]));return l.displayName=`${t}`,l};/**
|
|
12
|
+
* @license lucide-react v0.303.0 - ISC
|
|
13
|
+
*
|
|
14
|
+
* This source code is licensed under the ISC license.
|
|
15
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
16
|
+
*/const Z=m("Check",[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]]);/**
|
|
17
|
+
* @license lucide-react v0.303.0 - ISC
|
|
18
|
+
*
|
|
19
|
+
* This source code is licensed under the ISC license.
|
|
20
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
21
|
+
*/const pe=m("Copy",[["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2",key:"17jyea"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",key:"zix9uf"}]]);/**
|
|
22
|
+
* @license lucide-react v0.303.0 - ISC
|
|
23
|
+
*
|
|
24
|
+
* This source code is licensed under the ISC license.
|
|
25
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
26
|
+
*/const q=m("ExternalLink",[["path",{d:"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6",key:"a6xqqp"}],["polyline",{points:"15 3 21 3 21 9",key:"mznyad"}],["line",{x1:"10",x2:"21",y1:"14",y2:"3",key:"18c3s4"}]]);/**
|
|
27
|
+
* @license lucide-react v0.303.0 - ISC
|
|
28
|
+
*
|
|
29
|
+
* This source code is licensed under the ISC license.
|
|
30
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
31
|
+
*/const me=m("Eye",[["path",{d:"M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z",key:"rwhkz3"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]]);/**
|
|
32
|
+
* @license lucide-react v0.303.0 - ISC
|
|
33
|
+
*
|
|
34
|
+
* This source code is licensed under the ISC license.
|
|
35
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
36
|
+
*/const U=m("FileText",[["path",{d:"M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z",key:"1nnpy2"}],["polyline",{points:"14 2 14 8 20 8",key:"1ew0cm"}],["line",{x1:"16",x2:"8",y1:"13",y2:"13",key:"14keom"}],["line",{x1:"16",x2:"8",y1:"17",y2:"17",key:"17nazh"}],["line",{x1:"10",x2:"8",y1:"9",y2:"9",key:"1a5vjj"}]]);/**
|
|
37
|
+
* @license lucide-react v0.303.0 - ISC
|
|
38
|
+
*
|
|
39
|
+
* This source code is licensed under the ISC license.
|
|
40
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
41
|
+
*/const O=m("History",[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"1357e3"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}],["path",{d:"M12 7v5l4 2",key:"1fdv2h"}]]);/**
|
|
42
|
+
* @license lucide-react v0.303.0 - ISC
|
|
43
|
+
*
|
|
44
|
+
* This source code is licensed under the ISC license.
|
|
45
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
46
|
+
*/const je=m("Info",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 16v-4",key:"1dtifu"}],["path",{d:"M12 8h.01",key:"e9boi3"}]]);/**
|
|
47
|
+
* @license lucide-react v0.303.0 - ISC
|
|
48
|
+
*
|
|
49
|
+
* This source code is licensed under the ISC license.
|
|
50
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
51
|
+
*/const G=m("Link2",[["path",{d:"M9 17H7A5 5 0 0 1 7 7h2",key:"8i5ue5"}],["path",{d:"M15 7h2a5 5 0 1 1 0 10h-2",key:"1b9ql8"}],["line",{x1:"8",x2:"16",y1:"12",y2:"12",key:"1jonct"}]]);/**
|
|
52
|
+
* @license lucide-react v0.303.0 - ISC
|
|
53
|
+
*
|
|
54
|
+
* This source code is licensed under the ISC license.
|
|
55
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
56
|
+
*/const W=m("Loader2",[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56",key:"13zald"}]]);/**
|
|
57
|
+
* @license lucide-react v0.303.0 - ISC
|
|
58
|
+
*
|
|
59
|
+
* This source code is licensed under the ISC license.
|
|
60
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
61
|
+
*/const K=m("RefreshCw",[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]]);/**
|
|
62
|
+
* @license lucide-react v0.303.0 - ISC
|
|
63
|
+
*
|
|
64
|
+
* This source code is licensed under the ISC license.
|
|
65
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
66
|
+
*/const fe=m("Trash2",[["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6",key:"4alrt4"}],["path",{d:"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2",key:"v07s0e"}],["line",{x1:"10",x2:"10",y1:"11",y2:"17",key:"1uufr5"}],["line",{x1:"14",x2:"14",y1:"11",y2:"17",key:"xtxkd"}]]);/**
|
|
67
|
+
* @license lucide-react v0.303.0 - ISC
|
|
68
|
+
*
|
|
69
|
+
* This source code is licensed under the ISC license.
|
|
70
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
71
|
+
*/const ye=m("Users",[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",key:"1yyitq"}],["circle",{cx:"9",cy:"7",r:"4",key:"nufk8"}],["path",{d:"M22 21v-2a4 4 0 0 0-3-3.87",key:"kshegd"}],["path",{d:"M16 3.13a4 4 0 0 1 0 7.75",key:"1da9ce"}]]);/**
|
|
72
|
+
* @license lucide-react v0.303.0 - ISC
|
|
73
|
+
*
|
|
74
|
+
* This source code is licensed under the ISC license.
|
|
75
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
76
|
+
*/const J=m("X",[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]]);async function ke(){return(await chrome.storage.local.get(B.draftIndex))[B.draftIndex]||[]}async function ve(t){const d=`writedockDraft:${t}`;return(await chrome.storage.local.get(d))[d]||null}async function Ne(t){return E("DELETE_DRAFT",{draftId:t})}async function E(t,d={}){const l=await chrome.runtime.sendMessage({type:t,...d});if(l!=null&&l.error)throw new Error(l.error);if(!l||!("data"in l))throw new Error("扩展后台未响应,请在扩展管理页重新加载 WriteDock");return l.data}function ge({bridge:t,busy:d,syncing:l,update:g,connect:o,updateExtension:f,copy:x}){const[y,u]=i.useState(String(t.port));i.useEffect(()=>u(String(t.port)),[t.port]);const N="帮我安装 WriteDock:先检查电脑上有没有 Node.js 22 或更高版本,没有就帮我安装,需要我操作时告诉我;然后运行 npx -y writedock-mcp@latest setup,最后告诉我怎么重启你。",c=[t.mcpVersion?`服务版本 v${t.mcpVersion}`:"",t.connected&&t.mcpLatestVersion?`最新 v${t.mcpLatestVersion}`:""].filter(Boolean).join(" · ");return e.jsxs("div",{className:"connection-page",children:[t.connected?e.jsxs("section",{className:"panel",children:[e.jsx("div",{className:"panel-heading",children:e.jsxs("div",{children:[e.jsxs("h2",{children:[e.jsx(Z,{size:16})," 已连接"]}),e.jsxs("p",{children:["告诉你的 agent 要写什么、发到哪些平台就行。",c&&` ${c}`]})]})}),t.mcpUpdateAvailable&&e.jsx("p",{className:"help-text",role:"status",children:"有新版本。等发布结束后重启你的 agent 就会更新,不用重新连接。"})]}):t.mcpFound?e.jsxs("section",{className:"panel",children:[e.jsx("h2",{children:"发现本机的 WriteDock 服务"}),e.jsxs("p",{className:"help-text",children:[t.mcpFoundVersion?`服务版本 v${t.mcpFoundVersion}。`:"","点一下就能连上,以后会自动连接。"]}),e.jsx("div",{className:"settings-actions",children:e.jsx("button",{className:"primary-button",disabled:d||l,onClick:()=>void o(),children:d?"正在连接…":"连接"})})]}):t.enabled?e.jsxs("section",{className:"panel",children:[e.jsx("h2",{children:"WriteDock 服务没有运行"}),e.jsx("p",{className:"help-text",children:"比如电脑刚重启过。对你的 agent 说一句“检查 WriteDock 连接状态”,服务就会启动,这里会自动连上。"})]}):e.jsxs("section",{className:"panel",children:[e.jsx("h2",{children:"把这句话发给你的 agent"}),e.jsx("p",{className:"help-text",children:"它会帮你装好,需要你操作时会告诉你。"}),e.jsx("pre",{tabIndex:0,className:"setup-sentence",children:N}),e.jsx("div",{className:"settings-actions",children:e.jsxs("button",{className:"primary-button",onClick:()=>void x(N),children:[e.jsx(pe,{size:14}),"复制这句话"]})}),e.jsx("p",{className:"help-text",children:"agent 重启后,这里会出现“连接”按钮。"})]}),t.connected&&t.extensionUpdateVersion&&e.jsxs("section",{className:"panel",children:[e.jsxs("h2",{children:["扩展有新版本 v",t.extensionUpdateVersion]}),e.jsx("p",{className:"help-text",children:"点一下就能更新,连接和稿件都会保留。"}),e.jsx("div",{className:"settings-actions",children:e.jsx("button",{className:"primary-button",disabled:d||l,onClick:()=>void f(),children:d?"正在更新…":"更新扩展"})}),l&&e.jsx("p",{className:"help-text",children:"发布结束后才能更新。"})]}),e.jsxs("section",{className:"panel compact",children:[e.jsx("h2",{children:"怎么用"}),e.jsxs("ol",{children:[e.jsx("li",{children:"在「平台登录」里确认要发布的平台已登录。"}),e.jsx("li",{children:"告诉 agent 写什么、发到哪些平台。"}),e.jsx("li",{children:"遇到验证码时,agent 会告诉你去哪个页面完成。"})]})]}),e.jsxs("details",{className:"panel developer-settings",children:[e.jsx("summary",{children:"高级设置"}),e.jsx("p",{className:"help-text",children:"通常不需要动这里。"}),e.jsxs("label",{children:["启用自动连接",e.jsx("button",{role:"switch","aria-label":"启用 MCP 连接","aria-checked":t.enabled,className:`switch ${t.enabled?"on":""}`,disabled:d||l,onClick:()=>g({enabled:!t.enabled}),children:e.jsx("span",{})})]}),e.jsx("div",{className:"settings-grid",children:e.jsxs("label",{children:["连接端口",e.jsx("input",{"aria-label":"连接端口",inputMode:"numeric",value:y,onChange:v=>u(v.target.value)})]})}),e.jsx("div",{className:"settings-actions",children:e.jsx("button",{className:"secondary-button",disabled:d||l||y===String(t.port),onClick:()=>g({port:Number(y)}),children:"保存端口"})})]})]})}const X=t=>new Intl.DateTimeFormat("zh-CN",{month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",hour12:!1}).format(t),M=t=>t.trim()||"未命名稿件",we={syncing:"正在保存",completed:"已保存草稿",partial:"部分成功",failed:"失败",interrupted:"中断了",unknown:"请去平台确认",needs_user_action:"需要你处理",rehearsed:"演练完成"},Q={syncing:"正在发布",completed:"已发布",partial:"部分成功",failed:"发布失败",interrupted:"中断了",unknown:"请去平台确认",needs_user_action:"需要你处理",rehearsed:"演练完成"},be={...Q,syncing:"正在演练",completed:"演练完成",partial:"部分通过",failed:"演练失败"},Ee={pending:"排队中",checking_auth:"检查登录",uploading_images:"上传图片",transferring_media:"读取素材",uploading_media:"上传素材",preparing_article:"准备内容",publishing:"发布中",verifying:"等待平台确认",saving:"保存草稿",completed:"完成",failed:"失败",unknown:"请去平台确认",needs_user_action:"需要你处理",waiting_verification:"等你完成验证"},Te=t=>(t.videos||[]).reduce((d,l)=>d.replace(l.marker,`视频:${he(l.asset.name)}`),t.html),Ce=t=>t.publicationState==="rehearsed"?"演练通过,未发布":t.errorCode==="not_logged_in"?"未登录,已跳过":t.publicationState==="needs_user_action"?"需要你处理":t.publicationState==="unknown"?"请去平台确认":t.success?t.draftOnly===!1?"已发布":"已保存草稿":"失败";function Y({title:t,children:d,close:l,wide:g=!1}){const o=i.useRef(null),f=i.useRef(l);return f.current=l,i.useEffect(()=>{var u,N;const x=document.activeElement;(N=(u=o.current)==null?void 0:u.querySelector("button"))==null||N.focus();const y=c=>{var w;if(c.key==="Escape"){c.preventDefault(),f.current();return}if(c.key!=="Tab")return;const v=Array.from(((w=o.current)==null?void 0:w.querySelectorAll('button:not(:disabled), a[href], input, [tabindex="0"]'))||[]),k=v[0],T=v.at(-1);c.shiftKey&&document.activeElement===k?(c.preventDefault(),T==null||T.focus()):!c.shiftKey&&document.activeElement===T&&(c.preventDefault(),k==null||k.focus())};return document.addEventListener("keydown",y),()=>{document.removeEventListener("keydown",y),x==null||x.focus()}},[]),e.jsx("div",{className:"modal-backdrop",onMouseDown:x=>{x.target===x.currentTarget&&l()},children:e.jsxs("section",{className:`modal ${g?"wide":""}`,ref:o,role:"dialog","aria-modal":"true","aria-label":t,children:[e.jsxs("header",{children:[e.jsx("h2",{children:t}),e.jsx("button",{className:"icon-button","aria-label":"关闭弹窗",onClick:l,children:e.jsx(J,{size:18})})]}),d]})})}function _e(){const[t,d]=i.useState("tasks"),[l,g]=i.useState([]),[o,f]=i.useState(null),[x,y]=i.useState([]),[u,N]=i.useState([]),[c,v]=i.useState(null),[k,T]=i.useState(!1),[w,b]=i.useState(!1),[_,ee]=i.useState(!1),[L,p]=i.useState(""),[S,I]=i.useState(null),[F,z]=i.useState(null),R=i.useRef(!1),C=i.useCallback(async()=>{const[s,r,n]=await Promise.all([ke(),E("GET_SYNC_STATE"),E("GET_SYNC_HISTORY")]);g(s),f(r),y(n)},[]),A=i.useCallback(async()=>{const s=await E("GET_BRIDGE_STATE");return v(s),s},[]),$=i.useCallback(async(s=!1)=>{T(!0);try{N(await E("CHECK_ALL_AUTH",{forceRefresh:s}))}catch(r){p(r instanceof Error?r.message:"登录检测失败")}finally{T(!1)}},[]);i.useEffect(()=>{let s=!1;Promise.all([C(),A()]).then(([,a])=>{s||(a.connected||d("connection"),ee(!0),$())}).catch(a=>p(String(a.message||a)));const r=a=>(a.type==="TASK_UPDATED"&&a.task&&(f(a.task),a.task.status!=="syncing"&&C().catch(()=>{})),["DRAFT_SAVED","DRAFT_DELETED"].includes(a.type)&&(a.type==="DRAFT_DELETED"&&(I(h=>(h==null?void 0:h.id)===a.draftId?null:h),f(h=>(h==null?void 0:h.draftId)===a.draftId?null:h),y(h=>h.filter(j=>j.draftId!==a.draftId))),C().catch(()=>{})),a.type==="BRIDGE_STATUS_UPDATED"&&A().catch(()=>{}),!1);chrome.runtime.onMessage.addListener(r);const n=()=>{R.current&&(R.current=!1,$(!0))};return window.addEventListener("focus",n),()=>{s=!0,chrome.runtime.onMessage.removeListener(r),window.removeEventListener("focus",n)}},[C,A,$]),i.useEffect(()=>{if(t!=="connection"||c!=null&&c.connected)return;const s=setInterval(()=>{A().catch(()=>{})},3e3);return()=>clearInterval(s)},[t,c==null?void 0:c.connected,A]),i.useEffect(()=>{if(!L)return;const s=setTimeout(()=>p(""),7e3);return()=>clearTimeout(s)},[L]);const se=async s=>{b(!0);try{v(await E("SET_BRIDGE_CONFIG",{payload:s}))}catch(r){p(r instanceof Error?r.message:"连接设置保存失败")}finally{b(!1)}},te=async()=>{b(!0);try{return v(await E("CONNECT_BRIDGE")),!0}catch(s){return p(s instanceof Error?s.message:"连接失败"),!1}finally{b(!1)}},ae=async()=>{b(!0);try{await E("UPDATE_EXTENSION"),p("更新完成,正在重新加载")}catch(s){p(s instanceof Error?s.message:"更新失败")}finally{b(!1)}},H=(s,r=!1)=>{const n=P(s);n&&(async()=>{if(r){const a=new URL(n),j=(await chrome.tabs.query({})).find(le=>{try{const D=new URL(le.url||"");return D.origin===a.origin&&D.pathname===a.pathname&&(a.searchParams.has("article_id")?D.searchParams.get("article_id")===a.searchParams.get("article_id"):a.hash?D.hash===a.hash:D.href===a.href)}catch{return!1}});if(j!=null&&j.id){await chrome.tabs.update(j.id,{active:!0}),await chrome.windows.update(j.windowId,{focused:!0});return}}await chrome.tabs.create({url:n})})().catch(a=>p(a instanceof Error?a.message:"无法打开稿件页"))},ne=async s=>{try{await navigator.clipboard.writeText(s),p("已复制")}catch{p("复制失败,请选中文字后复制")}},V=o?[o,...x.filter(s=>s.id!==o.id)]:x,ce=s=>{var r;return((r=u.find(n=>n.id===s))==null?void 0:r.name)||s},re=u.filter(s=>s.isAuthenticated).length,ie=s=>e.jsxs("section",{className:"task-card","aria-label":`任务:${s.title}`,children:[e.jsxs("div",{className:"task-title",children:[e.jsxs("div",{children:[e.jsx("h3",{children:M(s.title)}),e.jsx("span",{children:X(s.startedAt)})]}),e.jsxs("span",{className:`task-status ${s.status}`,children:[s.status==="syncing"&&e.jsx(W,{size:13,className:"spin"}),(s.action==="rehearsal"?be:s.action==="publish"?Q:we)[s.status]]})]}),e.jsx("div",{className:"task-result-list",children:s.platforms.map(r=>{var h;const n=s.results.find(j=>j.platform===r),a=s.progress[r];return e.jsxs("div",{className:"task-result",children:[e.jsxs("div",{children:[e.jsx("strong",{children:ce(r)}),e.jsxs("span",{className:(n==null?void 0:n.publicationState)==="needs_user_action"||(a==null?void 0:a.stage)==="waiting_verification"?"manual-text":n!=null&&n.success||(n==null?void 0:n.publicationState)==="rehearsed"?"success-text":n?"error-text":"",children:[n?Ce(n):Ee[(a==null?void 0:a.stage)||"pending"],(a==null?void 0:a.stage)==="uploading_images"&&a.total?` ${a.current}/${a.total}`:"",(a==null?void 0:a.stage)==="transferring_media"&&a.total?` ${((a.current||0)/1048576).toFixed(1)} / ${(a.total/1048576).toFixed(1)} MB`:""]}),n&&(n.success||n.draftOnly===!1)&&P(((h=n.userAction)==null?void 0:h.url)||n.postUrl)&&e.jsxs("button",{className:"link-button",onClick:()=>{var j;return H(((j=n.userAction)==null?void 0:j.url)||n.postUrl,n.requiresUserAction===!0)},children:[n.requiresUserAction?"去页面处理":n.publicationState==="unknown"?"去平台确认":n.publicationState==="rehearsed"?"查看页面":n.draftOnly===!1?"查看":"打开草稿",e.jsx(q,{size:12})]})]}),(n==null?void 0:n.error)&&e.jsx("p",{className:"error-text",children:n.error}),(n==null?void 0:n.message)&&!n.success&&e.jsx("p",{children:n.message}),!n&&(a==null?void 0:a.message)&&e.jsx("p",{children:a.message})]},r)})}),s.error&&e.jsx("p",{className:"error-text task-error",children:s.error}),e.jsx("footer",{children:s.action==="rehearsal"?`演练 ${s.results.filter(r=>r.publicationState==="rehearsed").length} / ${s.platforms.length} 个平台通过`:`${s.results.filter(r=>r.success).length} / ${s.platforms.length} 个平台成功`})]},s.id);return e.jsxs("div",{className:"console-shell",children:[e.jsxs("header",{className:"app-header",children:[e.jsxs("div",{className:"brand",children:[e.jsx("img",{src:"/assets/icon-48.png",width:"26",height:"26",alt:""}),e.jsx("strong",{children:"WriteDock"})]}),e.jsxs("button",{className:`connection-indicator ${c!=null&&c.connected?"online":""}`,onClick:()=>d("connection"),children:[e.jsx("span",{}),c!=null&&c.connected?"已连接":"未连接"]})]}),e.jsxs("aside",{className:"navigation",children:[e.jsx("nav",{"aria-label":"控制台导航",children:[["tasks",O,"发布记录"],["accounts",ye,"平台登录"],["articles",U,"稿件"],["connection",G,"连接 agent"]].map(([s,r,n])=>e.jsxs("button",{className:t===s?"selected":"",onClick:()=>d(s),children:[e.jsx(r,{size:17}),n,s==="tasks"&&(o==null?void 0:o.status)==="syncing"&&e.jsx("span",{className:"nav-dot"}),s==="connection"&&((c==null?void 0:c.mcpUpdateAvailable)||(c==null?void 0:c.extensionUpdateVersion))&&e.jsx("span",{className:"nav-dot",title:"有新版本"})]},s))}),e.jsxs("footer",{children:[e.jsx(je,{size:13}),e.jsxs("span",{children:["v",chrome.runtime.getManifest().version]})]})]}),e.jsxs("main",{className:"console-main",children:[e.jsxs("div",{className:"page-heading",children:[e.jsxs("div",{children:[e.jsx("h1",{children:{articles:"稿件",tasks:"发布记录",accounts:"平台登录",connection:"连接 agent"}[t]}),e.jsx("p",{children:{articles:"agent 写好的内容,可以在这里预览。",tasks:"每次发布到各平台的结果。",accounts:"在这个浏览器里登录要发布的平台。",connection:"连上后,你的 agent 就能把内容发到这个浏览器。"}[t]})]}),t==="articles"&&e.jsxs("button",{className:"secondary-button",onClick:()=>void C().catch(s=>p(s.message)),children:[e.jsx(K,{size:14}),"刷新"]}),t==="accounts"&&e.jsxs("button",{className:"secondary-button",disabled:k,onClick:()=>$(!0),children:[e.jsx(K,{size:14,className:k?"spin":""}),"重新检查"]})]}),!_&&e.jsxs("div",{className:"empty-state",children:[e.jsx(W,{className:"spin",size:24}),e.jsx("p",{children:"正在加载…"})]}),_&&t==="articles"&&e.jsxs(e.Fragment,{children:[!(c!=null&&c.connected)&&e.jsxs("div",{className:"connection-banner",children:[e.jsx(G,{size:17}),e.jsx("span",{children:c!=null&&c.mcpFound?"发现本机的 WriteDock 服务,点一下就能连接。":"还没有连接到你的 agent。"}),e.jsx("button",{className:"link-button",onClick:()=>d("connection"),children:"去连接"})]}),l.length===0?e.jsxs("div",{className:"empty-state bordered",children:[e.jsx(U,{size:30}),e.jsx("h2",{children:"还没有稿件"}),e.jsx("p",{children:"告诉你的 agent 要写什么,内容会出现在这里。"})]}):e.jsx("div",{className:"article-list",children:l.map(s=>e.jsxs("article",{className:"article-row",children:[e.jsx(U,{size:19}),e.jsxs("div",{children:[e.jsx("h2",{children:M(s.title)}),e.jsx("p",{children:X(s.updatedAt)})]}),e.jsxs("button",{className:"secondary-button","aria-label":`预览稿件:${M(s.title)}`,onClick:()=>void ve(s.id).then(r=>r&&I(r)),children:[e.jsx(me,{size:14}),"预览"]}),e.jsx("button",{className:"icon-button","aria-label":`删除稿件:${M(s.title)}`,disabled:(o==null?void 0:o.status)==="syncing"&&o.draftId===s.id,onClick:()=>z(s),children:e.jsx(fe,{size:15})})]},s.id))}),e.jsx("p",{className:"footnote",children:"稿件只保存在这台电脑上。"})]}),_&&t==="tasks"&&(V.length?e.jsx("div",{className:"task-list",children:V.map(ie)}):e.jsxs("div",{className:"empty-state bordered",children:[e.jsx(O,{size:30}),e.jsx("h2",{children:"还没有发布记录"}),e.jsx("p",{children:"让 agent 发布后,这里会显示每个平台的结果。"})]})),_&&t==="accounts"&&e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"section-summary",children:["已登录 ",re," / ",u.length," 个平台",e.jsx("span",{children:k?"正在检查…":""})]}),e.jsx("div",{className:"account-grid",children:u.map(s=>e.jsxs("section",{className:"account-card",children:[e.jsxs("div",{children:[e.jsx("h2",{children:s.name}),e.jsx("span",{className:s.isAuthenticated?"success-text":"",children:s.isAuthenticated?s.username||"已登录":k&&!s.checked?"检查中…":s.error?"暂时查不到":"未登录"})]}),e.jsx("div",{children:e.jsxs("button",{className:"secondary-button","aria-label":`打开${s.name}`,onClick:()=>{R.current=!0,H(s.homepage)},children:[s.isAuthenticated?"打开":"去登录",e.jsx(q,{size:12})]})})]},s.id))}),e.jsx("p",{className:"footnote",children:"登录后回到这里会自动更新。显示“暂时查不到”时,可以打开平台确认后点“重新检查”。"})]}),_&&t==="connection"&&c&&e.jsx(ge,{bridge:c,busy:w,syncing:(o==null?void 0:o.status)==="syncing",update:se,connect:te,updateExtension:ae,copy:ne})]}),L&&e.jsxs("div",{className:"toast",role:"status",children:[e.jsx(Z,{size:16}),e.jsx("span",{children:L}),e.jsx("button",{className:"icon-button","aria-label":"关闭提示",onClick:()=>p(""),children:e.jsx(J,{size:15})})]}),S&&e.jsx(Y,{title:"稿件预览",close:()=>I(null),wide:!0,children:e.jsxs("div",{className:"preview-content",children:[S.cover&&e.jsx("img",{className:"preview-cover",src:S.cover,alt:"文章封面"}),e.jsx("h1",{children:M(S.title)}),e.jsx("div",{className:"article-content",dangerouslySetInnerHTML:{__html:oe.sanitize(Te(S))}})]})}),F&&e.jsx(Y,{title:"删除这篇稿件?",close:()=>{w||z(null)},children:e.jsxs("div",{className:"modal-content",children:[e.jsx("p",{children:"只删除这台电脑上的稿件和记录,已发到平台的内容不受影响。"}),e.jsxs("div",{className:"modal-actions",children:[e.jsx("button",{className:"secondary-button",disabled:w,onClick:()=>z(null),children:"保留稿件"}),e.jsx("button",{className:"danger-button",disabled:w,onClick:async()=>{b(!0);try{await Ne(F.id),z(null),await C()}catch(s){p(s instanceof Error?s.message:"删除失败")}finally{b(!1)}},children:w?"正在删除…":"删除稿件"})]})]})})]})}de(document.getElementById("root")).render(e.jsx(i.StrictMode,{children:e.jsx(_e,{})}));
|
package/extension/manifest.json
CHANGED
|
@@ -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.12",
|
|
29744
29744
|
private: true,
|
|
29745
29745
|
type: "module",
|
|
29746
29746
|
description: "WriteDock local MCP server for the browser extension",
|
|
@@ -29955,12 +29955,15 @@ var LocalBridge = class {
|
|
|
29955
29955
|
extensionId;
|
|
29956
29956
|
seenNonces = /* @__PURE__ */ new Map();
|
|
29957
29957
|
extensionToken;
|
|
29958
|
+
lastWork = 0;
|
|
29959
|
+
lastSeen = Date.now();
|
|
29958
29960
|
media;
|
|
29959
29961
|
pending = /* @__PURE__ */ new Map();
|
|
29960
29962
|
port;
|
|
29961
29963
|
async start() {
|
|
29962
29964
|
if (this.starting) return this.starting;
|
|
29963
|
-
this.starting = this.
|
|
29965
|
+
this.starting = Promise.resolve(this.options.ensureService?.()).catch(() => {
|
|
29966
|
+
}).then(() => this.listen()).catch(async (error2) => {
|
|
29964
29967
|
if (error2.code !== "EADDRINUSE") throw error2;
|
|
29965
29968
|
this.server = void 0;
|
|
29966
29969
|
this.sockets = void 0;
|
|
@@ -30007,6 +30010,22 @@ var LocalBridge = class {
|
|
|
30007
30010
|
}, 2e4);
|
|
30008
30011
|
this.heartbeat.unref();
|
|
30009
30012
|
}
|
|
30013
|
+
get isPrimary() {
|
|
30014
|
+
return this.primary;
|
|
30015
|
+
}
|
|
30016
|
+
/** How long nothing has needed this service. An open extension connection counts as needed. */
|
|
30017
|
+
idleFor() {
|
|
30018
|
+
if (this.client?.readyState === import_websocket.default.OPEN) this.lastSeen = Date.now();
|
|
30019
|
+
return Date.now() - this.lastSeen;
|
|
30020
|
+
}
|
|
30021
|
+
/** Asks the running service to step aside for a newer one. It says no while it has work. */
|
|
30022
|
+
async askServiceToStop() {
|
|
30023
|
+
try {
|
|
30024
|
+
return (await this.httpCall("/stop", {})).accepted === true;
|
|
30025
|
+
} catch {
|
|
30026
|
+
return false;
|
|
30027
|
+
}
|
|
30028
|
+
}
|
|
30010
30029
|
validHost(host) {
|
|
30011
30030
|
return host === `127.0.0.1:${this.port}` || host === `localhost:${this.port}`;
|
|
30012
30031
|
}
|
|
@@ -30109,6 +30128,7 @@ var LocalBridge = class {
|
|
|
30109
30128
|
}
|
|
30110
30129
|
const assetPath = /^\/assets\/([a-f0-9-]{36})$/.exec(req.url || "");
|
|
30111
30130
|
if (assetPath && this.validHost(req.headers.host) && (!req.headers.origin || /^chrome-extension:\/\/[a-p]{32}$/.test(req.headers.origin))) {
|
|
30131
|
+
this.lastWork = this.lastSeen = Date.now();
|
|
30112
30132
|
await this.media.serve(req, res, assetPath[1]);
|
|
30113
30133
|
return;
|
|
30114
30134
|
}
|
|
@@ -30139,10 +30159,18 @@ var LocalBridge = class {
|
|
|
30139
30159
|
for (const [key2, expires] of this.seenNonces) if (expires < now) this.seenNonces.delete(key2);
|
|
30140
30160
|
this.seenNonces.set(nonce, now + 3e4);
|
|
30141
30161
|
responseNonce = nonce;
|
|
30162
|
+
if (req.method === "POST" && req.url === "/stop") {
|
|
30163
|
+
const accepted = this.pending.size === 0 && Date.now() - this.lastWork > 12e4;
|
|
30164
|
+
send(200, { accepted });
|
|
30165
|
+
if (accepted) setImmediate(() => this.options.onStopRequested?.());
|
|
30166
|
+
return;
|
|
30167
|
+
}
|
|
30168
|
+
this.lastSeen = Date.now();
|
|
30142
30169
|
if (req.method === "GET" && req.url === "/status") {
|
|
30143
30170
|
send(200, { connected: this.client?.readyState === import_websocket.default.OPEN, extensionId: this.extensionId, protocol: "writedock/1", ...mcpReleaseInfo() });
|
|
30144
30171
|
return;
|
|
30145
30172
|
}
|
|
30173
|
+
this.lastWork = Date.now();
|
|
30146
30174
|
if (req.method === "POST" && req.url === "/assets/register") {
|
|
30147
30175
|
const input2 = JSON.parse(body);
|
|
30148
30176
|
if (typeof input2.path !== "string" || !["image", "video"].includes(input2.kind)) throw new Error("\u7D20\u6750\u53C2\u6570\u4E0D\u6B63\u786E");
|
|
@@ -30187,7 +30215,7 @@ var LocalBridge = class {
|
|
|
30187
30215
|
this.starting = void 0;
|
|
30188
30216
|
await this.start();
|
|
30189
30217
|
if (this.primary) return { connected: false, protocol: "writedock/1", ...mcpReleaseInfo() };
|
|
30190
|
-
|
|
30218
|
+
return this.httpCall("/status");
|
|
30191
30219
|
}
|
|
30192
30220
|
}
|
|
30193
30221
|
async request(method, params = {}) {
|
|
@@ -30202,7 +30230,10 @@ var LocalBridge = class {
|
|
|
30202
30230
|
}
|
|
30203
30231
|
async registerMedia(path, kind) {
|
|
30204
30232
|
await this.status();
|
|
30205
|
-
if (this.primary)
|
|
30233
|
+
if (this.primary) {
|
|
30234
|
+
this.lastWork = this.lastSeen = Date.now();
|
|
30235
|
+
return this.media.register(path, kind);
|
|
30236
|
+
}
|
|
30206
30237
|
return await this.httpCall("/assets/register", { path, kind });
|
|
30207
30238
|
}
|
|
30208
30239
|
async requestConnected(method, params) {
|
|
@@ -50615,12 +50646,88 @@ async function runSetup(home = (0, import_node_os3.homedir)(), log = console.log
|
|
|
50615
50646
|
return found;
|
|
50616
50647
|
}
|
|
50617
50648
|
|
|
50649
|
+
// src/service.ts
|
|
50650
|
+
var import_node_child_process2 = require("child_process");
|
|
50651
|
+
var import_node_fs2 = require("fs");
|
|
50652
|
+
var import_node_os4 = require("os");
|
|
50653
|
+
var import_node_path6 = require("path");
|
|
50654
|
+
var sleep = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
50655
|
+
async function probe(port) {
|
|
50656
|
+
try {
|
|
50657
|
+
const response = await fetch(`http://127.0.0.1:${port}/hello`, { signal: AbortSignal.timeout(1500), cache: "no-store" });
|
|
50658
|
+
const value = await response.json();
|
|
50659
|
+
return value.service === "writedock-mcp" && typeof value.version === "string" ? { version: value.version } : "other";
|
|
50660
|
+
} catch (error2) {
|
|
50661
|
+
return error2 instanceof Error && error2.name === "TimeoutError" ? "other" : "none";
|
|
50662
|
+
}
|
|
50663
|
+
}
|
|
50664
|
+
async function until(check2, timeoutMs) {
|
|
50665
|
+
const end = Date.now() + timeoutMs;
|
|
50666
|
+
while (Date.now() < end) {
|
|
50667
|
+
if (await check2()) return true;
|
|
50668
|
+
await sleep(200);
|
|
50669
|
+
}
|
|
50670
|
+
return false;
|
|
50671
|
+
}
|
|
50672
|
+
function startDetached() {
|
|
50673
|
+
const directory = dataDirectory();
|
|
50674
|
+
(0, import_node_fs2.mkdirSync)(directory, { recursive: true, mode: 448 });
|
|
50675
|
+
const log = (0, import_node_fs2.openSync)((0, import_node_path6.join)(directory, "service.log"), "w");
|
|
50676
|
+
try {
|
|
50677
|
+
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)() });
|
|
50678
|
+
child.on("error", () => {
|
|
50679
|
+
});
|
|
50680
|
+
child.unref();
|
|
50681
|
+
} finally {
|
|
50682
|
+
(0, import_node_fs2.closeSync)(log);
|
|
50683
|
+
}
|
|
50684
|
+
}
|
|
50685
|
+
async function ensureService(bridge, port) {
|
|
50686
|
+
let found = await probe(port);
|
|
50687
|
+
if (typeof found === "object" && newerVersion(package_default.version, found.version)) {
|
|
50688
|
+
if (!await bridge.askServiceToStop()) return;
|
|
50689
|
+
if (!await until(async () => await probe(port) === "none", 5e3)) return;
|
|
50690
|
+
found = "none";
|
|
50691
|
+
}
|
|
50692
|
+
if (found !== "none") return;
|
|
50693
|
+
startDetached();
|
|
50694
|
+
await until(async () => typeof await probe(port) === "object", 1e4);
|
|
50695
|
+
}
|
|
50696
|
+
function watchService(bridge, port) {
|
|
50697
|
+
let running = false;
|
|
50698
|
+
const timer = setInterval(() => {
|
|
50699
|
+
if (running) return;
|
|
50700
|
+
running = true;
|
|
50701
|
+
void ensureService(bridge, port).catch(() => {
|
|
50702
|
+
}).finally(() => {
|
|
50703
|
+
running = false;
|
|
50704
|
+
});
|
|
50705
|
+
}, 6e4);
|
|
50706
|
+
timer.unref();
|
|
50707
|
+
}
|
|
50708
|
+
var IDLE_CHECK_MS = 5e3;
|
|
50709
|
+
async function runService(bridge) {
|
|
50710
|
+
await bridge.start();
|
|
50711
|
+
if (!bridge.isPrimary) process.exit(0);
|
|
50712
|
+
const idleLimit = Number(process.env.WRITEDOCK_SERVICE_IDLE_MS) || 2 * 60 * 6e4;
|
|
50713
|
+
setInterval(() => {
|
|
50714
|
+
if (bridge.idleFor() >= idleLimit) void bridge.close().finally(() => process.exit(0));
|
|
50715
|
+
}, IDLE_CHECK_MS);
|
|
50716
|
+
const stop = () => {
|
|
50717
|
+
void bridge.close().finally(() => process.exit(0));
|
|
50718
|
+
};
|
|
50719
|
+
process.on("SIGINT", stop);
|
|
50720
|
+
process.on("SIGTERM", stop);
|
|
50721
|
+
}
|
|
50722
|
+
|
|
50618
50723
|
// src/index.ts
|
|
50619
50724
|
async function main() {
|
|
50620
50725
|
const port = Number(process.env.WRITEDOCK_PORT || 9647);
|
|
50621
50726
|
const identity = await loadIdentity(port, process.env.WRITEDOCK_TOKEN);
|
|
50622
|
-
const
|
|
50727
|
+
const background = !process.env.WRITEDOCK_NO_SERVICE;
|
|
50728
|
+
const bridge = new LocalBridge({ token: identity.token, port, credentials: identity, ...background ? { ensureService: () => ensureService(bridge, port) } : {} });
|
|
50623
50729
|
await bridge.start();
|
|
50730
|
+
if (background) watchService(bridge, port);
|
|
50624
50731
|
const server = createMcpServer(bridge);
|
|
50625
50732
|
let closing = false;
|
|
50626
50733
|
const close = async () => {
|
|
@@ -50643,10 +50750,31 @@ async function main() {
|
|
|
50643
50750
|
};
|
|
50644
50751
|
await server.connect(new StdioServerTransport());
|
|
50645
50752
|
}
|
|
50753
|
+
async function service() {
|
|
50754
|
+
const port = Number(process.env.WRITEDOCK_PORT || 9647);
|
|
50755
|
+
const identity = await loadIdentity(port, process.env.WRITEDOCK_TOKEN);
|
|
50756
|
+
const bridge = new LocalBridge({ token: identity.token, port, credentials: identity, onStopRequested: () => {
|
|
50757
|
+
void bridge.close().finally(() => process.exit(0));
|
|
50758
|
+
} });
|
|
50759
|
+
await runService(bridge);
|
|
50760
|
+
}
|
|
50761
|
+
async function setup() {
|
|
50762
|
+
await runSetup();
|
|
50763
|
+
const port = Number(process.env.WRITEDOCK_PORT || 9647);
|
|
50764
|
+
const identity = await loadIdentity(port, process.env.WRITEDOCK_TOKEN);
|
|
50765
|
+
await ensureService(new LocalBridge({ token: identity.token, port, credentials: identity }), port).catch(() => {
|
|
50766
|
+
});
|
|
50767
|
+
process.exit(0);
|
|
50768
|
+
}
|
|
50646
50769
|
if (process.argv.slice(2).some((argument) => argument === "--version" || argument === "-v")) {
|
|
50647
50770
|
console.log(package_default.version);
|
|
50648
50771
|
} else if (process.argv[2] === "setup") {
|
|
50649
|
-
|
|
50772
|
+
setup().catch((error2) => {
|
|
50773
|
+
console.error("[WriteDock MCP]", error2 instanceof Error ? error2.message : String(error2));
|
|
50774
|
+
process.exit(1);
|
|
50775
|
+
});
|
|
50776
|
+
} else if (process.argv[2] === "--service") {
|
|
50777
|
+
service().catch((error2) => {
|
|
50650
50778
|
console.error("[WriteDock MCP]", error2 instanceof Error ? error2.message : String(error2));
|
|
50651
50779
|
process.exit(1);
|
|
50652
50780
|
});
|
package/package.json
CHANGED
package/source/package.json
CHANGED
package/source/src/bridge.ts
CHANGED
|
@@ -29,10 +29,12 @@ export class LocalBridge {
|
|
|
29
29
|
private extensionId?: string
|
|
30
30
|
private seenNonces = new Map<string, number>()
|
|
31
31
|
private extensionToken: string
|
|
32
|
+
private lastWork = 0
|
|
33
|
+
private lastSeen = Date.now()
|
|
32
34
|
private media: MediaStore
|
|
33
35
|
private pending = new Map<string, { resolve: (value: unknown) => void; reject: (error: Error) => void; timer: ReturnType<typeof setTimeout> }>()
|
|
34
36
|
readonly port: number
|
|
35
|
-
constructor(private options: { token: string; port?: number; timeoutMs?: number; credentials?: { getExtensionToken: () => Promise<string>; setExtensionToken: (token: string) => Promise<void> } }) {
|
|
37
|
+
constructor(private options: { token: string; port?: number; timeoutMs?: number; credentials?: { getExtensionToken: () => Promise<string>; setExtensionToken: (token: string) => Promise<void> }; ensureService?: () => Promise<void>; onStopRequested?: () => void }) {
|
|
36
38
|
this.port = options.port ?? 9647
|
|
37
39
|
this.extensionToken = options.token
|
|
38
40
|
this.media = new MediaStore(this.port)
|
|
@@ -42,7 +44,7 @@ export class LocalBridge {
|
|
|
42
44
|
|
|
43
45
|
async start(): Promise<void> {
|
|
44
46
|
if (this.starting) return this.starting
|
|
45
|
-
this.starting = this.listen().catch(async error => {
|
|
47
|
+
this.starting = Promise.resolve(this.options.ensureService?.()).catch(() => {}).then(() => this.listen()).catch(async error => {
|
|
46
48
|
if ((error as NodeJS.ErrnoException).code !== 'EADDRINUSE') throw error
|
|
47
49
|
this.server = undefined
|
|
48
50
|
this.sockets = undefined
|
|
@@ -78,6 +80,20 @@ export class LocalBridge {
|
|
|
78
80
|
this.heartbeat.unref()
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
get isPrimary() { return this.primary }
|
|
84
|
+
|
|
85
|
+
/** How long nothing has needed this service. An open extension connection counts as needed. */
|
|
86
|
+
idleFor() {
|
|
87
|
+
if (this.client?.readyState === WebSocket.OPEN) this.lastSeen = Date.now()
|
|
88
|
+
return Date.now() - this.lastSeen
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Asks the running service to step aside for a newer one. It says no while it has work. */
|
|
92
|
+
async askServiceToStop() {
|
|
93
|
+
try { return (await this.httpCall('/stop', {})).accepted === true }
|
|
94
|
+
catch { return false } // Versions before this one have no such route, and leave with their agent.
|
|
95
|
+
}
|
|
96
|
+
|
|
81
97
|
private validHost(host?: string) { return host === `127.0.0.1:${this.port}` || host === `localhost:${this.port}` }
|
|
82
98
|
|
|
83
99
|
/** Replaces the extension credential and drops whatever was using the old one. */
|
|
@@ -165,6 +181,7 @@ export class LocalBridge {
|
|
|
165
181
|
}
|
|
166
182
|
const assetPath = /^\/assets\/([a-f0-9-]{36})$/.exec(req.url || '')
|
|
167
183
|
if (assetPath && this.validHost(req.headers.host) && (!req.headers.origin || /^chrome-extension:\/\/[a-p]{32}$/.test(req.headers.origin))) {
|
|
184
|
+
this.lastWork = this.lastSeen = Date.now()
|
|
168
185
|
await this.media.serve(req, res, assetPath[1]); return
|
|
169
186
|
}
|
|
170
187
|
if (!this.validHost(req.headers.host) || req.headers.origin !== undefined) {
|
|
@@ -188,9 +205,17 @@ export class LocalBridge {
|
|
|
188
205
|
for (const [key, expires] of this.seenNonces) if (expires < now) this.seenNonces.delete(key)
|
|
189
206
|
this.seenNonces.set(nonce, now + 30_000)
|
|
190
207
|
responseNonce = nonce
|
|
208
|
+
if (req.method === 'POST' && req.url === '/stop') {
|
|
209
|
+
const accepted = this.pending.size === 0 && Date.now() - this.lastWork > 120_000
|
|
210
|
+
send(200, { accepted })
|
|
211
|
+
if (accepted) setImmediate(() => this.options.onStopRequested?.())
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
this.lastSeen = Date.now() // Status checks keep the service around, but only real work stops it being replaced.
|
|
191
215
|
if (req.method === 'GET' && req.url === '/status') {
|
|
192
216
|
send(200, { connected: this.client?.readyState === WebSocket.OPEN, extensionId: this.extensionId, protocol: 'writedock/1', ...mcpReleaseInfo() }); return
|
|
193
217
|
}
|
|
218
|
+
this.lastWork = Date.now()
|
|
194
219
|
if (req.method === 'POST' && req.url === '/assets/register') {
|
|
195
220
|
const input = JSON.parse(body)
|
|
196
221
|
if (typeof input.path !== 'string' || !['image', 'video'].includes(input.kind)) throw new Error('素材参数不正确')
|
|
@@ -231,7 +256,7 @@ export class LocalBridge {
|
|
|
231
256
|
this.starting = undefined
|
|
232
257
|
await this.start()
|
|
233
258
|
if (this.primary) return { connected: false, protocol: 'writedock/1', ...mcpReleaseInfo() }
|
|
234
|
-
|
|
259
|
+
return this.httpCall('/status') // A background service took the port in the meantime.
|
|
235
260
|
}
|
|
236
261
|
}
|
|
237
262
|
|
|
@@ -248,7 +273,7 @@ export class LocalBridge {
|
|
|
248
273
|
|
|
249
274
|
async registerMedia(path: string, kind: 'image' | 'video'): Promise<MediaAsset> {
|
|
250
275
|
await this.status()
|
|
251
|
-
if (this.primary) return this.media.register(path, kind)
|
|
276
|
+
if (this.primary) { this.lastWork = this.lastSeen = Date.now(); return this.media.register(path, kind) }
|
|
252
277
|
return await this.httpCall('/assets/register', { path, kind }) as unknown as MediaAsset
|
|
253
278
|
}
|
|
254
279
|
|
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
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { closeSync, mkdirSync, openSync } from 'node:fs'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import mcpPackage from '../package.json'
|
|
6
|
+
import type { LocalBridge } from './bridge'
|
|
7
|
+
import { dataDirectory } from './identity'
|
|
8
|
+
import { newerVersion } from './release-info'
|
|
9
|
+
|
|
10
|
+
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
|
11
|
+
|
|
12
|
+
/** 'none' means nothing listens. 'other' is a service too old to say hello, or not ours. */
|
|
13
|
+
async function probe(port: number): Promise<{ version: string } | 'none' | 'other'> {
|
|
14
|
+
try {
|
|
15
|
+
const response = await fetch(`http://127.0.0.1:${port}/hello`, { signal: AbortSignal.timeout(1500), cache: 'no-store' })
|
|
16
|
+
const value = await response.json() as { service?: unknown; version?: unknown }
|
|
17
|
+
return value.service === 'writedock-mcp' && typeof value.version === 'string' ? { version: value.version } : 'other'
|
|
18
|
+
} catch (error) {
|
|
19
|
+
// A timeout means something holds the port but did not answer.
|
|
20
|
+
return error instanceof Error && error.name === 'TimeoutError' ? 'other' : 'none'
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function until(check: () => Promise<boolean>, timeoutMs: number) {
|
|
25
|
+
const end = Date.now() + timeoutMs
|
|
26
|
+
while (Date.now() < end) { if (await check()) return true; await sleep(200) }
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function startDetached() {
|
|
31
|
+
const directory = dataDirectory()
|
|
32
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 })
|
|
33
|
+
// The service has no terminal, so this file is the only record of why it stopped.
|
|
34
|
+
const log = openSync(join(directory, 'service.log'), 'w')
|
|
35
|
+
try {
|
|
36
|
+
// cwd: Windows keeps a process's working folder locked, so do not hold on to the agent's project.
|
|
37
|
+
const child = spawn(process.execPath, [__filename, '--service'], { detached: true, stdio: ['ignore', 'ignore', log], windowsHide: true, cwd: homedir() })
|
|
38
|
+
child.on('error', () => {})
|
|
39
|
+
child.unref()
|
|
40
|
+
} finally { closeSync(log) }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Makes sure a background service owns the port, so the extension can reach it whether or not the agent
|
|
45
|
+
* keeps this process running. If it cannot start, the caller's bridge takes the port itself, as before.
|
|
46
|
+
*/
|
|
47
|
+
export async function ensureService(bridge: LocalBridge, port: number) {
|
|
48
|
+
let found = await probe(port)
|
|
49
|
+
if (typeof found === 'object' && newerVersion(mcpPackage.version, found.version)) {
|
|
50
|
+
// It stays while it is busy with a publication. This runs again soon, so it is replaced once idle.
|
|
51
|
+
if (!await bridge.askServiceToStop()) return
|
|
52
|
+
if (!await until(async () => await probe(port) === 'none', 5000)) return
|
|
53
|
+
found = 'none'
|
|
54
|
+
}
|
|
55
|
+
if (found !== 'none') return
|
|
56
|
+
startDetached()
|
|
57
|
+
await until(async () => typeof await probe(port) === 'object', 10_000)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Agents may keep this process open for days: bring back a service that stopped, and replace an older one. */
|
|
61
|
+
export function watchService(bridge: LocalBridge, port: number) {
|
|
62
|
+
let running = false
|
|
63
|
+
const timer = setInterval(() => {
|
|
64
|
+
if (running) return
|
|
65
|
+
running = true
|
|
66
|
+
void ensureService(bridge, port).catch(() => {}).finally(() => { running = false })
|
|
67
|
+
}, 60_000)
|
|
68
|
+
timer.unref()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const IDLE_CHECK_MS = 5000
|
|
72
|
+
|
|
73
|
+
/** The background service itself. It leaves after a long time with no extension and no agent. */
|
|
74
|
+
export async function runService(bridge: LocalBridge) {
|
|
75
|
+
await bridge.start()
|
|
76
|
+
if (!bridge.isPrimary) process.exit(0) // Another service won the race for the port.
|
|
77
|
+
const idleLimit = Number(process.env.WRITEDOCK_SERVICE_IDLE_MS) || 2 * 60 * 60_000
|
|
78
|
+
setInterval(() => { if (bridge.idleFor() >= idleLimit) void bridge.close().finally(() => process.exit(0)) }, IDLE_CHECK_MS)
|
|
79
|
+
const stop = () => { void bridge.close().finally(() => process.exit(0)) }
|
|
80
|
+
process.on('SIGINT', stop)
|
|
81
|
+
process.on('SIGTERM', stop)
|
|
82
|
+
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import{p as oe}from"./purify.es-1yEQDPgc.js";import{r as i,j as e,c as de}from"./react-vendor-BrXlnJmF.js";import{S as B,s as P,e as he}from"./task-utils-DwzGcJkm.js";/**
|
|
2
|
-
* @license lucide-react v0.303.0 - ISC
|
|
3
|
-
*
|
|
4
|
-
* This source code is licensed under the ISC license.
|
|
5
|
-
* See the LICENSE file in the root directory of this source tree.
|
|
6
|
-
*/var ue={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};/**
|
|
7
|
-
* @license lucide-react v0.303.0 - ISC
|
|
8
|
-
*
|
|
9
|
-
* This source code is licensed under the ISC license.
|
|
10
|
-
* See the LICENSE file in the root directory of this source tree.
|
|
11
|
-
*/const xe=t=>t.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase().trim(),m=(t,d)=>{const l=i.forwardRef(({color:g="currentColor",size:o=24,strokeWidth:j=2,absoluteStrokeWidth:x,className:y="",children:u,...N},c)=>i.createElement("svg",{ref:c,...ue,width:o,height:o,stroke:g,strokeWidth:x?Number(j)*24/Number(o):j,className:["lucide",`lucide-${xe(t)}`,y].join(" "),...N},[...d.map(([v,k])=>i.createElement(v,k)),...Array.isArray(u)?u:[u]]));return l.displayName=`${t}`,l};/**
|
|
12
|
-
* @license lucide-react v0.303.0 - ISC
|
|
13
|
-
*
|
|
14
|
-
* This source code is licensed under the ISC license.
|
|
15
|
-
* See the LICENSE file in the root directory of this source tree.
|
|
16
|
-
*/const Z=m("Check",[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]]);/**
|
|
17
|
-
* @license lucide-react v0.303.0 - ISC
|
|
18
|
-
*
|
|
19
|
-
* This source code is licensed under the ISC license.
|
|
20
|
-
* See the LICENSE file in the root directory of this source tree.
|
|
21
|
-
*/const pe=m("Copy",[["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2",key:"17jyea"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",key:"zix9uf"}]]);/**
|
|
22
|
-
* @license lucide-react v0.303.0 - ISC
|
|
23
|
-
*
|
|
24
|
-
* This source code is licensed under the ISC license.
|
|
25
|
-
* See the LICENSE file in the root directory of this source tree.
|
|
26
|
-
*/const q=m("ExternalLink",[["path",{d:"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6",key:"a6xqqp"}],["polyline",{points:"15 3 21 3 21 9",key:"mznyad"}],["line",{x1:"10",x2:"21",y1:"14",y2:"3",key:"18c3s4"}]]);/**
|
|
27
|
-
* @license lucide-react v0.303.0 - ISC
|
|
28
|
-
*
|
|
29
|
-
* This source code is licensed under the ISC license.
|
|
30
|
-
* See the LICENSE file in the root directory of this source tree.
|
|
31
|
-
*/const me=m("Eye",[["path",{d:"M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z",key:"rwhkz3"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]]);/**
|
|
32
|
-
* @license lucide-react v0.303.0 - ISC
|
|
33
|
-
*
|
|
34
|
-
* This source code is licensed under the ISC license.
|
|
35
|
-
* See the LICENSE file in the root directory of this source tree.
|
|
36
|
-
*/const U=m("FileText",[["path",{d:"M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z",key:"1nnpy2"}],["polyline",{points:"14 2 14 8 20 8",key:"1ew0cm"}],["line",{x1:"16",x2:"8",y1:"13",y2:"13",key:"14keom"}],["line",{x1:"16",x2:"8",y1:"17",y2:"17",key:"17nazh"}],["line",{x1:"10",x2:"8",y1:"9",y2:"9",key:"1a5vjj"}]]);/**
|
|
37
|
-
* @license lucide-react v0.303.0 - ISC
|
|
38
|
-
*
|
|
39
|
-
* This source code is licensed under the ISC license.
|
|
40
|
-
* See the LICENSE file in the root directory of this source tree.
|
|
41
|
-
*/const O=m("History",[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"1357e3"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}],["path",{d:"M12 7v5l4 2",key:"1fdv2h"}]]);/**
|
|
42
|
-
* @license lucide-react v0.303.0 - ISC
|
|
43
|
-
*
|
|
44
|
-
* This source code is licensed under the ISC license.
|
|
45
|
-
* See the LICENSE file in the root directory of this source tree.
|
|
46
|
-
*/const fe=m("Info",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 16v-4",key:"1dtifu"}],["path",{d:"M12 8h.01",key:"e9boi3"}]]);/**
|
|
47
|
-
* @license lucide-react v0.303.0 - ISC
|
|
48
|
-
*
|
|
49
|
-
* This source code is licensed under the ISC license.
|
|
50
|
-
* See the LICENSE file in the root directory of this source tree.
|
|
51
|
-
*/const G=m("Link2",[["path",{d:"M9 17H7A5 5 0 0 1 7 7h2",key:"8i5ue5"}],["path",{d:"M15 7h2a5 5 0 1 1 0 10h-2",key:"1b9ql8"}],["line",{x1:"8",x2:"16",y1:"12",y2:"12",key:"1jonct"}]]);/**
|
|
52
|
-
* @license lucide-react v0.303.0 - ISC
|
|
53
|
-
*
|
|
54
|
-
* This source code is licensed under the ISC license.
|
|
55
|
-
* See the LICENSE file in the root directory of this source tree.
|
|
56
|
-
*/const W=m("Loader2",[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56",key:"13zald"}]]);/**
|
|
57
|
-
* @license lucide-react v0.303.0 - ISC
|
|
58
|
-
*
|
|
59
|
-
* This source code is licensed under the ISC license.
|
|
60
|
-
* See the LICENSE file in the root directory of this source tree.
|
|
61
|
-
*/const K=m("RefreshCw",[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]]);/**
|
|
62
|
-
* @license lucide-react v0.303.0 - ISC
|
|
63
|
-
*
|
|
64
|
-
* This source code is licensed under the ISC license.
|
|
65
|
-
* See the LICENSE file in the root directory of this source tree.
|
|
66
|
-
*/const je=m("Trash2",[["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6",key:"4alrt4"}],["path",{d:"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2",key:"v07s0e"}],["line",{x1:"10",x2:"10",y1:"11",y2:"17",key:"1uufr5"}],["line",{x1:"14",x2:"14",y1:"11",y2:"17",key:"xtxkd"}]]);/**
|
|
67
|
-
* @license lucide-react v0.303.0 - ISC
|
|
68
|
-
*
|
|
69
|
-
* This source code is licensed under the ISC license.
|
|
70
|
-
* See the LICENSE file in the root directory of this source tree.
|
|
71
|
-
*/const ye=m("Users",[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",key:"1yyitq"}],["circle",{cx:"9",cy:"7",r:"4",key:"nufk8"}],["path",{d:"M22 21v-2a4 4 0 0 0-3-3.87",key:"kshegd"}],["path",{d:"M16 3.13a4 4 0 0 1 0 7.75",key:"1da9ce"}]]);/**
|
|
72
|
-
* @license lucide-react v0.303.0 - ISC
|
|
73
|
-
*
|
|
74
|
-
* This source code is licensed under the ISC license.
|
|
75
|
-
* See the LICENSE file in the root directory of this source tree.
|
|
76
|
-
*/const J=m("X",[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]]);async function ke(){return(await chrome.storage.local.get(B.draftIndex))[B.draftIndex]||[]}async function ve(t){const d=`writedockDraft:${t}`;return(await chrome.storage.local.get(d))[d]||null}async function Ne(t){return E("DELETE_DRAFT",{draftId:t})}async function E(t,d={}){const l=await chrome.runtime.sendMessage({type:t,...d});if(l!=null&&l.error)throw new Error(l.error);if(!l||!("data"in l))throw new Error("扩展后台未响应,请在扩展管理页重新加载 WriteDock");return l.data}function ge({bridge:t,busy:d,syncing:l,update:g,connect:o,updateExtension:j,copy:x}){const[y,u]=i.useState(String(t.port));i.useEffect(()=>u(String(t.port)),[t.port]);const N="帮我安装 WriteDock:先检查电脑上有没有 Node.js 22 或更高版本,没有就帮我安装,需要我操作时告诉我;然后运行 npx -y writedock-mcp@latest setup,最后告诉我怎么重启你。",c=[t.mcpVersion?`服务版本 v${t.mcpVersion}`:"",t.connected&&t.mcpLatestVersion?`最新 v${t.mcpLatestVersion}`:""].filter(Boolean).join(" · ");return e.jsxs("div",{className:"connection-page",children:[t.connected?e.jsxs("section",{className:"panel",children:[e.jsx("div",{className:"panel-heading",children:e.jsxs("div",{children:[e.jsxs("h2",{children:[e.jsx(Z,{size:16})," 已连接"]}),e.jsxs("p",{children:["告诉你的 agent 要写什么、发到哪些平台就行。",c&&` ${c}`]})]})}),t.mcpUpdateAvailable&&e.jsx("p",{className:"help-text",role:"status",children:"有新版本。等发布结束后重启你的 agent 就会更新,不用重新连接。"})]}):t.mcpFound?e.jsxs("section",{className:"panel",children:[e.jsx("h2",{children:"发现本机的 WriteDock 服务"}),e.jsxs("p",{className:"help-text",children:[t.mcpFoundVersion?`服务版本 v${t.mcpFoundVersion}。`:"","点一下就能连上,以后会自动连接。"]}),e.jsx("div",{className:"settings-actions",children:e.jsx("button",{className:"primary-button",disabled:d||l,onClick:()=>void o(),children:d?"正在连接…":"连接"})})]}):e.jsxs("section",{className:"panel",children:[e.jsx("h2",{children:"把这句话发给你的 agent"}),e.jsx("p",{className:"help-text",children:"它会帮你装好,需要你操作时会告诉你。"}),e.jsx("pre",{tabIndex:0,className:"setup-sentence",children:N}),e.jsx("div",{className:"settings-actions",children:e.jsxs("button",{className:"primary-button",onClick:()=>void x(N),children:[e.jsx(pe,{size:14}),"复制这句话"]})}),e.jsx("p",{className:"help-text",children:"agent 重启后,这里会出现“连接”按钮。"})]}),t.connected&&t.extensionUpdateVersion&&e.jsxs("section",{className:"panel",children:[e.jsxs("h2",{children:["扩展有新版本 v",t.extensionUpdateVersion]}),e.jsx("p",{className:"help-text",children:"点一下就能更新,连接和稿件都会保留。"}),e.jsx("div",{className:"settings-actions",children:e.jsx("button",{className:"primary-button",disabled:d||l,onClick:()=>void j(),children:d?"正在更新…":"更新扩展"})}),l&&e.jsx("p",{className:"help-text",children:"发布结束后才能更新。"})]}),e.jsxs("section",{className:"panel compact",children:[e.jsx("h2",{children:"怎么用"}),e.jsxs("ol",{children:[e.jsx("li",{children:"在「平台登录」里确认要发布的平台已登录。"}),e.jsx("li",{children:"告诉 agent 写什么、发到哪些平台。"}),e.jsx("li",{children:"遇到验证码时,agent 会告诉你去哪个页面完成。"})]})]}),e.jsxs("details",{className:"panel developer-settings",children:[e.jsx("summary",{children:"高级设置"}),e.jsx("p",{className:"help-text",children:"通常不需要动这里。"}),e.jsxs("label",{children:["启用自动连接",e.jsx("button",{role:"switch","aria-label":"启用 MCP 连接","aria-checked":t.enabled,className:`switch ${t.enabled?"on":""}`,disabled:d||l,onClick:()=>g({enabled:!t.enabled}),children:e.jsx("span",{})})]}),e.jsx("div",{className:"settings-grid",children:e.jsxs("label",{children:["连接端口",e.jsx("input",{"aria-label":"连接端口",inputMode:"numeric",value:y,onChange:v=>u(v.target.value)})]})}),e.jsx("div",{className:"settings-actions",children:e.jsx("button",{className:"secondary-button",disabled:d||l||y===String(t.port),onClick:()=>g({port:Number(y)}),children:"保存端口"})})]})]})}const X=t=>new Intl.DateTimeFormat("zh-CN",{month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",hour12:!1}).format(t),M=t=>t.trim()||"未命名稿件",we={syncing:"正在保存",completed:"已保存草稿",partial:"部分成功",failed:"失败",interrupted:"中断了",unknown:"请去平台确认",needs_user_action:"需要你处理",rehearsed:"演练完成"},Q={syncing:"正在发布",completed:"已发布",partial:"部分成功",failed:"发布失败",interrupted:"中断了",unknown:"请去平台确认",needs_user_action:"需要你处理",rehearsed:"演练完成"},be={...Q,syncing:"正在演练",completed:"演练完成",partial:"部分通过",failed:"演练失败"},Ee={pending:"排队中",checking_auth:"检查登录",uploading_images:"上传图片",transferring_media:"读取素材",uploading_media:"上传素材",preparing_article:"准备内容",publishing:"发布中",verifying:"等待平台确认",saving:"保存草稿",completed:"完成",failed:"失败",unknown:"请去平台确认",needs_user_action:"需要你处理",waiting_verification:"等你完成验证"},Te=t=>(t.videos||[]).reduce((d,l)=>d.replace(l.marker,`视频:${he(l.asset.name)}`),t.html),Ce=t=>t.publicationState==="rehearsed"?"演练通过,未发布":t.errorCode==="not_logged_in"?"未登录,已跳过":t.publicationState==="needs_user_action"?"需要你处理":t.publicationState==="unknown"?"请去平台确认":t.success?t.draftOnly===!1?"已发布":"已保存草稿":"失败";function Y({title:t,children:d,close:l,wide:g=!1}){const o=i.useRef(null),j=i.useRef(l);return j.current=l,i.useEffect(()=>{var u,N;const x=document.activeElement;(N=(u=o.current)==null?void 0:u.querySelector("button"))==null||N.focus();const y=c=>{var w;if(c.key==="Escape"){c.preventDefault(),j.current();return}if(c.key!=="Tab")return;const v=Array.from(((w=o.current)==null?void 0:w.querySelectorAll('button:not(:disabled), a[href], input, [tabindex="0"]'))||[]),k=v[0],T=v.at(-1);c.shiftKey&&document.activeElement===k?(c.preventDefault(),T==null||T.focus()):!c.shiftKey&&document.activeElement===T&&(c.preventDefault(),k==null||k.focus())};return document.addEventListener("keydown",y),()=>{document.removeEventListener("keydown",y),x==null||x.focus()}},[]),e.jsx("div",{className:"modal-backdrop",onMouseDown:x=>{x.target===x.currentTarget&&l()},children:e.jsxs("section",{className:`modal ${g?"wide":""}`,ref:o,role:"dialog","aria-modal":"true","aria-label":t,children:[e.jsxs("header",{children:[e.jsx("h2",{children:t}),e.jsx("button",{className:"icon-button","aria-label":"关闭弹窗",onClick:l,children:e.jsx(J,{size:18})})]}),d]})})}function _e(){const[t,d]=i.useState("tasks"),[l,g]=i.useState([]),[o,j]=i.useState(null),[x,y]=i.useState([]),[u,N]=i.useState([]),[c,v]=i.useState(null),[k,T]=i.useState(!1),[w,b]=i.useState(!1),[_,ee]=i.useState(!1),[L,p]=i.useState(""),[S,I]=i.useState(null),[F,z]=i.useState(null),R=i.useRef(!1),C=i.useCallback(async()=>{const[s,r,n]=await Promise.all([ke(),E("GET_SYNC_STATE"),E("GET_SYNC_HISTORY")]);g(s),j(r),y(n)},[]),A=i.useCallback(async()=>{const s=await E("GET_BRIDGE_STATE");return v(s),s},[]),$=i.useCallback(async(s=!1)=>{T(!0);try{N(await E("CHECK_ALL_AUTH",{forceRefresh:s}))}catch(r){p(r instanceof Error?r.message:"登录检测失败")}finally{T(!1)}},[]);i.useEffect(()=>{let s=!1;Promise.all([C(),A()]).then(([,a])=>{s||(a.connected||d("connection"),ee(!0),$())}).catch(a=>p(String(a.message||a)));const r=a=>(a.type==="TASK_UPDATED"&&a.task&&(j(a.task),a.task.status!=="syncing"&&C().catch(()=>{})),["DRAFT_SAVED","DRAFT_DELETED"].includes(a.type)&&(a.type==="DRAFT_DELETED"&&(I(h=>(h==null?void 0:h.id)===a.draftId?null:h),j(h=>(h==null?void 0:h.draftId)===a.draftId?null:h),y(h=>h.filter(f=>f.draftId!==a.draftId))),C().catch(()=>{})),a.type==="BRIDGE_STATUS_UPDATED"&&A().catch(()=>{}),!1);chrome.runtime.onMessage.addListener(r);const n=()=>{R.current&&(R.current=!1,$(!0))};return window.addEventListener("focus",n),()=>{s=!0,chrome.runtime.onMessage.removeListener(r),window.removeEventListener("focus",n)}},[C,A,$]),i.useEffect(()=>{if(t!=="connection"||c!=null&&c.connected)return;const s=setInterval(()=>{A().catch(()=>{})},3e3);return()=>clearInterval(s)},[t,c==null?void 0:c.connected,A]),i.useEffect(()=>{if(!L)return;const s=setTimeout(()=>p(""),7e3);return()=>clearTimeout(s)},[L]);const se=async s=>{b(!0);try{v(await E("SET_BRIDGE_CONFIG",{payload:s}))}catch(r){p(r instanceof Error?r.message:"连接设置保存失败")}finally{b(!1)}},te=async()=>{b(!0);try{return v(await E("CONNECT_BRIDGE")),!0}catch(s){return p(s instanceof Error?s.message:"连接失败"),!1}finally{b(!1)}},ae=async()=>{b(!0);try{await E("UPDATE_EXTENSION"),p("更新完成,正在重新加载")}catch(s){p(s instanceof Error?s.message:"更新失败")}finally{b(!1)}},H=(s,r=!1)=>{const n=P(s);n&&(async()=>{if(r){const a=new URL(n),f=(await chrome.tabs.query({})).find(le=>{try{const D=new URL(le.url||"");return D.origin===a.origin&&D.pathname===a.pathname&&(a.searchParams.has("article_id")?D.searchParams.get("article_id")===a.searchParams.get("article_id"):a.hash?D.hash===a.hash:D.href===a.href)}catch{return!1}});if(f!=null&&f.id){await chrome.tabs.update(f.id,{active:!0}),await chrome.windows.update(f.windowId,{focused:!0});return}}await chrome.tabs.create({url:n})})().catch(a=>p(a instanceof Error?a.message:"无法打开稿件页"))},ne=async s=>{try{await navigator.clipboard.writeText(s),p("已复制")}catch{p("复制失败,请选中文字后复制")}},V=o?[o,...x.filter(s=>s.id!==o.id)]:x,ce=s=>{var r;return((r=u.find(n=>n.id===s))==null?void 0:r.name)||s},re=u.filter(s=>s.isAuthenticated).length,ie=s=>e.jsxs("section",{className:"task-card","aria-label":`任务:${s.title}`,children:[e.jsxs("div",{className:"task-title",children:[e.jsxs("div",{children:[e.jsx("h3",{children:M(s.title)}),e.jsx("span",{children:X(s.startedAt)})]}),e.jsxs("span",{className:`task-status ${s.status}`,children:[s.status==="syncing"&&e.jsx(W,{size:13,className:"spin"}),(s.action==="rehearsal"?be:s.action==="publish"?Q:we)[s.status]]})]}),e.jsx("div",{className:"task-result-list",children:s.platforms.map(r=>{var h;const n=s.results.find(f=>f.platform===r),a=s.progress[r];return e.jsxs("div",{className:"task-result",children:[e.jsxs("div",{children:[e.jsx("strong",{children:ce(r)}),e.jsxs("span",{className:(n==null?void 0:n.publicationState)==="needs_user_action"||(a==null?void 0:a.stage)==="waiting_verification"?"manual-text":n!=null&&n.success||(n==null?void 0:n.publicationState)==="rehearsed"?"success-text":n?"error-text":"",children:[n?Ce(n):Ee[(a==null?void 0:a.stage)||"pending"],(a==null?void 0:a.stage)==="uploading_images"&&a.total?` ${a.current}/${a.total}`:"",(a==null?void 0:a.stage)==="transferring_media"&&a.total?` ${((a.current||0)/1048576).toFixed(1)} / ${(a.total/1048576).toFixed(1)} MB`:""]}),n&&(n.success||n.draftOnly===!1)&&P(((h=n.userAction)==null?void 0:h.url)||n.postUrl)&&e.jsxs("button",{className:"link-button",onClick:()=>{var f;return H(((f=n.userAction)==null?void 0:f.url)||n.postUrl,n.requiresUserAction===!0)},children:[n.requiresUserAction?"去页面处理":n.publicationState==="unknown"?"去平台确认":n.publicationState==="rehearsed"?"查看页面":n.draftOnly===!1?"查看":"打开草稿",e.jsx(q,{size:12})]})]}),(n==null?void 0:n.error)&&e.jsx("p",{className:"error-text",children:n.error}),(n==null?void 0:n.message)&&!n.success&&e.jsx("p",{children:n.message}),!n&&(a==null?void 0:a.message)&&e.jsx("p",{children:a.message})]},r)})}),s.error&&e.jsx("p",{className:"error-text task-error",children:s.error}),e.jsx("footer",{children:s.action==="rehearsal"?`演练 ${s.results.filter(r=>r.publicationState==="rehearsed").length} / ${s.platforms.length} 个平台通过`:`${s.results.filter(r=>r.success).length} / ${s.platforms.length} 个平台成功`})]},s.id);return e.jsxs("div",{className:"console-shell",children:[e.jsxs("header",{className:"app-header",children:[e.jsxs("div",{className:"brand",children:[e.jsx("img",{src:"/assets/icon-48.png",width:"26",height:"26",alt:""}),e.jsx("strong",{children:"WriteDock"})]}),e.jsxs("button",{className:`connection-indicator ${c!=null&&c.connected?"online":""}`,onClick:()=>d("connection"),children:[e.jsx("span",{}),c!=null&&c.connected?"已连接":"未连接"]})]}),e.jsxs("aside",{className:"navigation",children:[e.jsx("nav",{"aria-label":"控制台导航",children:[["tasks",O,"发布记录"],["accounts",ye,"平台登录"],["articles",U,"稿件"],["connection",G,"连接 agent"]].map(([s,r,n])=>e.jsxs("button",{className:t===s?"selected":"",onClick:()=>d(s),children:[e.jsx(r,{size:17}),n,s==="tasks"&&(o==null?void 0:o.status)==="syncing"&&e.jsx("span",{className:"nav-dot"}),s==="connection"&&((c==null?void 0:c.mcpUpdateAvailable)||(c==null?void 0:c.extensionUpdateVersion))&&e.jsx("span",{className:"nav-dot",title:"有新版本"})]},s))}),e.jsxs("footer",{children:[e.jsx(fe,{size:13}),e.jsxs("span",{children:["v",chrome.runtime.getManifest().version]})]})]}),e.jsxs("main",{className:"console-main",children:[e.jsxs("div",{className:"page-heading",children:[e.jsxs("div",{children:[e.jsx("h1",{children:{articles:"稿件",tasks:"发布记录",accounts:"平台登录",connection:"连接 agent"}[t]}),e.jsx("p",{children:{articles:"agent 写好的内容,可以在这里预览。",tasks:"每次发布到各平台的结果。",accounts:"在这个浏览器里登录要发布的平台。",connection:"连上后,你的 agent 就能把内容发到这个浏览器。"}[t]})]}),t==="articles"&&e.jsxs("button",{className:"secondary-button",onClick:()=>void C().catch(s=>p(s.message)),children:[e.jsx(K,{size:14}),"刷新"]}),t==="accounts"&&e.jsxs("button",{className:"secondary-button",disabled:k,onClick:()=>$(!0),children:[e.jsx(K,{size:14,className:k?"spin":""}),"重新检查"]})]}),!_&&e.jsxs("div",{className:"empty-state",children:[e.jsx(W,{className:"spin",size:24}),e.jsx("p",{children:"正在加载…"})]}),_&&t==="articles"&&e.jsxs(e.Fragment,{children:[!(c!=null&&c.connected)&&e.jsxs("div",{className:"connection-banner",children:[e.jsx(G,{size:17}),e.jsx("span",{children:c!=null&&c.mcpFound?"发现本机的 WriteDock 服务,点一下就能连接。":"还没有连接到你的 agent。"}),e.jsx("button",{className:"link-button",onClick:()=>d("connection"),children:"去连接"})]}),l.length===0?e.jsxs("div",{className:"empty-state bordered",children:[e.jsx(U,{size:30}),e.jsx("h2",{children:"还没有稿件"}),e.jsx("p",{children:"告诉你的 agent 要写什么,内容会出现在这里。"})]}):e.jsx("div",{className:"article-list",children:l.map(s=>e.jsxs("article",{className:"article-row",children:[e.jsx(U,{size:19}),e.jsxs("div",{children:[e.jsx("h2",{children:M(s.title)}),e.jsx("p",{children:X(s.updatedAt)})]}),e.jsxs("button",{className:"secondary-button","aria-label":`预览稿件:${M(s.title)}`,onClick:()=>void ve(s.id).then(r=>r&&I(r)),children:[e.jsx(me,{size:14}),"预览"]}),e.jsx("button",{className:"icon-button","aria-label":`删除稿件:${M(s.title)}`,disabled:(o==null?void 0:o.status)==="syncing"&&o.draftId===s.id,onClick:()=>z(s),children:e.jsx(je,{size:15})})]},s.id))}),e.jsx("p",{className:"footnote",children:"稿件只保存在这台电脑上。"})]}),_&&t==="tasks"&&(V.length?e.jsx("div",{className:"task-list",children:V.map(ie)}):e.jsxs("div",{className:"empty-state bordered",children:[e.jsx(O,{size:30}),e.jsx("h2",{children:"还没有发布记录"}),e.jsx("p",{children:"让 agent 发布后,这里会显示每个平台的结果。"})]})),_&&t==="accounts"&&e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"section-summary",children:["已登录 ",re," / ",u.length," 个平台",e.jsx("span",{children:k?"正在检查…":""})]}),e.jsx("div",{className:"account-grid",children:u.map(s=>e.jsxs("section",{className:"account-card",children:[e.jsxs("div",{children:[e.jsx("h2",{children:s.name}),e.jsx("span",{className:s.isAuthenticated?"success-text":"",children:s.isAuthenticated?s.username||"已登录":k&&!s.checked?"检查中…":s.error?"暂时查不到":"未登录"})]}),e.jsx("div",{children:e.jsxs("button",{className:"secondary-button","aria-label":`打开${s.name}`,onClick:()=>{R.current=!0,H(s.homepage)},children:[s.isAuthenticated?"打开":"去登录",e.jsx(q,{size:12})]})})]},s.id))}),e.jsx("p",{className:"footnote",children:"登录后回到这里会自动更新。显示“暂时查不到”时,可以打开平台确认后点“重新检查”。"})]}),_&&t==="connection"&&c&&e.jsx(ge,{bridge:c,busy:w,syncing:(o==null?void 0:o.status)==="syncing",update:se,connect:te,updateExtension:ae,copy:ne})]}),L&&e.jsxs("div",{className:"toast",role:"status",children:[e.jsx(Z,{size:16}),e.jsx("span",{children:L}),e.jsx("button",{className:"icon-button","aria-label":"关闭提示",onClick:()=>p(""),children:e.jsx(J,{size:15})})]}),S&&e.jsx(Y,{title:"稿件预览",close:()=>I(null),wide:!0,children:e.jsxs("div",{className:"preview-content",children:[S.cover&&e.jsx("img",{className:"preview-cover",src:S.cover,alt:"文章封面"}),e.jsx("h1",{children:M(S.title)}),e.jsx("div",{className:"article-content",dangerouslySetInnerHTML:{__html:oe.sanitize(Te(S))}})]})}),F&&e.jsx(Y,{title:"删除这篇稿件?",close:()=>{w||z(null)},children:e.jsxs("div",{className:"modal-content",children:[e.jsx("p",{children:"只删除这台电脑上的稿件和记录,已发到平台的内容不受影响。"}),e.jsxs("div",{className:"modal-actions",children:[e.jsx("button",{className:"secondary-button",disabled:w,onClick:()=>z(null),children:"保留稿件"}),e.jsx("button",{className:"danger-button",disabled:w,onClick:async()=>{b(!0);try{await Ne(F.id),z(null),await C()}catch(s){p(s instanceof Error?s.message:"删除失败")}finally{b(!1)}},children:w?"正在删除…":"删除稿件"})]})]})})]})}de(document.getElementById("root")).render(e.jsx(i.StrictMode,{children:e.jsx(_e,{})}));
|