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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  连接你自己的 agent 与 WriteDock 浏览器扩展:agent 写稿并准备图片或视频,一次调用发布到多个平台,并按平台查询结果。
4
4
 
5
- MCP 运行在你自己的电脑上,只监听 `127.0.0.1`。平台 Cookie 留在浏览器里,稿件和媒体不经过 npm。
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` 在每次启动时检查新版,运行中的进程不会自动替换,重启 agent 即可。发布任务进行中不要重启。
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,{})}));
@@ -4,7 +4,7 @@
4
4
  "name": "写作台 WriteDock",
5
5
  "short_name": "写作台",
6
6
  "description": "连接自己的 AI agent,通过 MCP 发布小红书长文、图文和视频,微博图文、视频及长文章,知乎、百家号、B站专栏文章与抖音图文视频。",
7
- "version": "0.4.11",
7
+ "version": "0.4.12",
8
8
  "minimum_chrome_version": "116",
9
9
  "icons": {
10
10
  "16": "assets/icon-16.png",
@@ -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-DSj8NlXD.js"></script>
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">
@@ -1,4 +1,4 @@
1
- WriteDock 安装与更新 v0.4.11
1
+ WriteDock 安装与更新 v0.4.12
2
2
 
3
3
  首次使用
4
4
  1. 把这句话发给你的 agent:帮我安装 WriteDock:先检查电脑上有没有 Node.js 22 或更高版本,没有就帮我安装,需要我操作时告诉我;然后运行 npx -y writedock-mcp@latest setup,最后告诉我怎么重启你。
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.11",
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.listen().catch(async (error2) => {
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
- throw error2;
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) return this.media.register(path, kind);
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 bridge = new LocalBridge({ token: identity.token, port, credentials: identity });
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
- runSetup().catch((error2) => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "writedock-mcp",
3
- "version": "0.4.11",
3
+ "version": "0.4.12",
4
4
  "description": "WriteDock local MCP server for the browser extension",
5
5
  "type": "commonjs",
6
6
  "license": "GPL-3.0-only",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@writedock/mcp-server",
3
- "version": "0.4.11",
3
+ "version": "0.4.12",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "WriteDock local MCP server for the browser extension",
@@ -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
- throw error
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
 
@@ -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
- const bridge = new LocalBridge({ token: identity.token, port, credentials: identity })
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
- runSetup().catch(error => { console.error('[WriteDock MCP]', error instanceof Error ? error.message : String(error)); process.exit(1) })
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,{})}));