@forsion/extend 0.2.0 → 0.3.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # 更新日志
2
2
 
3
+ ## 0.3.0 — 2026-09-28
4
+
5
+ - 账号登录态从 Forsion 本体搬入本包:登录、登出、切换账号、账号状态与登录态自动续期都由本包提供,行为与之前一致。没装本包的桌面端不再显示账号卡、Forsion 设置页与登录入口。
6
+ - 「设置 → 同步 → 本地库远程同步」里的「Forsion 云端」后端也搬入本包;没装本包的桌面端不再列出这一项。
7
+ - 需要较新的 Forsion 桌面端才会启用以上两块;旧桌面端上本包只提供 0.2 的功能,不影响使用。
8
+
9
+ Account sign-in now ships in this package, moved over from Forsion itself with its behavior unchanged: signing in, signing out, switching accounts, account status and keeping you signed in. Desktops without this package no longer show the account card, the Forsion settings page or the sign-in entry points.
10
+ The "Forsion cloud" backend under Settings → Sync → Remote sync of the local vault ships here too; desktops without this package no longer list it.
11
+ Both need a newer Forsion Desktop build; on older desktops this package keeps providing the 0.2 features and nothing else changes.
12
+
3
13
  ## 0.2.0 — 2026-09-27
4
14
 
5
15
  - Forsion Connect(Coding Space 项目发布、应用商店上架申请、预览态 AI 代理)从 Forsion 本体搬入本包,行为与之前一致。预览页里的 `window.forsion`(`/forsion-connect.js`)与 `/__forsion/*` 云端代理也随之由本包提供。
package/README.md CHANGED
@@ -1,14 +1,14 @@
1
1
  # Forsion Extend
2
2
 
3
- Forsion 桌面端里**跟云端账号打交道的那一半**:个人中心 / 会员页入口、额度视图与重置卡、后台额度转换、反馈中心提交,插件以当前账号身份调 Forsion 云端 API 的通用接缝 `cloud:fetch`,以及(0.2 起)Forsion Connect —— Coding Space 项目发布、应用商店上架申请、预览态 AI 代理(预览页的 `window.forsion` SDK 与 `/__forsion/*` 云端代理)。它是一个 Forsion 捆绑包(manifest.json + 渲染半身 + 主进程半身),编译产物以 npm 包 `@forsion/extend` 发布。
3
+ Forsion 桌面端里**跟云端账号打交道的那一半**:账号登录 / 登出 / 切换与登录态续期(0.3 起),个人中心 / 会员页入口、额度视图与重置卡、后台额度转换、反馈中心提交,插件以当前账号身份调 Forsion 云端 API 的通用接缝 `cloud:fetch`,Forsion Connect(0.2 起)—— Coding Space 项目发布、应用商店上架申请、预览态 AI 代理(预览页的 `window.forsion` SDK 与 `/__forsion/*` 云端代理),以及本地库远程同步的 Forsion 云端后端(0.3 起)。它是一个 Forsion 捆绑包(manifest.json + 渲染半身 + 主进程半身),编译产物以 npm 包 `@forsion/extend` 发布。
4
4
 
5
- The **cloud-account half of Forsion Desktop**: the account center and membership page shortcuts, the quota view and reset cards, background quota conversion, feedback submission, `cloud:fetch` (the shared seam plugins use to call the Forsion cloud API as the signed-in account), and, since 0.2, Forsion Connect: Coding Space project publishing, app store listing requests, and the preview-mode AI proxy (the `window.forsion` SDK served to preview pages plus the `/__forsion/*` cloud proxy). It is a Forsion bundle (manifest.json + a renderer half + a main-process half), published as the npm package `@forsion/extend`.
5
+ The **cloud-account half of Forsion Desktop**: account sign-in, sign-out, switching and staying signed in (since 0.3), the account center and membership page shortcuts, the quota view and reset cards, background quota conversion, feedback submission, `cloud:fetch` (the shared seam plugins use to call the Forsion cloud API as the signed-in account), Forsion Connect (since 0.2): Coding Space project publishing, app store listing requests, and the preview-mode AI proxy (the `window.forsion` SDK served to preview pages plus the `/__forsion/*` cloud proxy), and the Forsion cloud backend for remote sync of the local vault (since 0.3). It is a Forsion bundle (manifest.json + a renderer half + a main-process half), published as the npm package `@forsion/extend`.
6
6
 
7
7
  ## Forsion 桌面端怎么装载它 / How Forsion Desktop loads it
8
8
 
9
- Forsion 桌面端把本包当**内置捆绑包**:随桌面安装包带一份,启动时播种进 `<home>/plugins/forsion-extend/`,之后由 npm 更新器在后台下载新版、下次启动换上。带主进程半身的包比普通捆绑包多一道闸:播种 / 换版前,桌面端用钉在自己代码里的 ed25519 公钥核对包内 `SIGNATURE`(每个打包文件的 sha256 + 签名),验不过的那份不换、不载;验过后 `import()` `dist/desktop.mjs`,调用 `registerCloud(host)` 注册 8 个云端账号 IPC 通道;桌面端够新(提供 Connect 所需的宿主接缝)时再注册 7 个 `connect:*` 通道与预览挂钩,旧桌面端上这一块自动跳过、账号功能照常。
9
+ Forsion 桌面端把本包当**内置捆绑包**:随桌面安装包带一份,启动时播种进 `<home>/plugins/forsion-extend/`,之后由 npm 更新器在后台下载新版、下次启动换上。带主进程半身的包比普通捆绑包多一道闸:播种 / 换版前,桌面端用钉在自己代码里的 ed25519 公钥核对包内 `SIGNATURE`(每个打包文件的 sha256 + 签名),验不过的那份不换、不载;验过后 `import()` `dist/desktop.mjs`,调用 `registerCloud(host)` 注册 8 个云端账号 IPC 通道;桌面端够新(提供对应的宿主接缝)时再注册 7 个 `connect:*` 通道与预览挂钩(0.2)、5 个 `auth:*` 登录态通道与远程同步的 penzor 后端(0.3),旧桌面端上缺接缝的那块自动跳过、其余功能照常。桌面端的界面按这些通道是否存在显示或隐藏:没装本包就没有账号卡、Forsion 设置页、登录入口与「Forsion 云端」同步后端。
10
10
 
11
- Forsion Desktop treats this package as a **built-in bundle**: a copy ships inside the desktop installer, is seeded into `<home>/plugins/forsion-extend/` on startup, and the npm updater downloads newer versions in the background and switches to them on the next launch. A bundle with a main-process half has one extra gate: before seeding or switching, the desktop checks the in-package `SIGNATURE` (sha256 of every packed file plus an ed25519 signature) against the public key pinned in its own code; a copy that fails is neither installed nor loaded. Once verified, the desktop `import()`s `dist/desktop.mjs` and calls `registerCloud(host)`, which registers the 8 cloud-account IPC channels and, when the desktop is new enough to provide the host seam Connect needs, the 7 `connect:*` channels plus the preview hooks; on older desktops that part is skipped and the account features work as before.
11
+ Forsion Desktop treats this package as a **built-in bundle**: a copy ships inside the desktop installer, is seeded into `<home>/plugins/forsion-extend/` on startup, and the npm updater downloads newer versions in the background and switches to them on the next launch. A bundle with a main-process half has one extra gate: before seeding or switching, the desktop checks the in-package `SIGNATURE` (sha256 of every packed file plus an ed25519 signature) against the public key pinned in its own code; a copy that fails is neither installed nor loaded. Once verified, the desktop `import()`s `dist/desktop.mjs` and calls `registerCloud(host)`, which registers the 8 cloud-account IPC channels and, when the desktop is new enough to provide the matching host seam, the 7 `connect:*` channels plus the preview hooks (0.2) and the 5 `auth:*` sign-in channels plus the penzor remote-sync backend (0.3); on older desktops the parts without a seam are skipped and the rest works as before. The desktop shows or hides its UI by whether these channels exist: without this package there is no account card, Forsion settings page, sign-in entry point or "Forsion cloud" sync backend.
12
12
 
13
13
  | 包内文件 / file | 用途 / role |
14
14
  |---|---|
package/SIGNATURE CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "alg": "ed25519",
3
3
  "files": {
4
- "CHANGELOG.md": "da1f256d4f7c18fe0b5e120f9985ca1205b783197f7f54ccdeec08e9a6451e93",
4
+ "CHANGELOG.md": "00eab66ddc784373fa030c7f88885dac39e894fec99d1a9a192efba14e272eca",
5
5
  "LICENSE": "5594544ee08011ed5785ee26410f1f2ef005f66e9283833e8d435e34c10de1ed",
6
- "README.md": "041eee1d470243f33f31bb1a3bc89c124b3b2dd493794aa4e4b865e07fcee7f3",
7
- "dist/desktop.mjs": "0c3aefeab0d2f8f341b75a2c58afc0d038cfdd93dee8e23f4c5c6be66ecf4427",
6
+ "README.md": "2e47ea40144d4b49ad23bfb550b7acbb64f7eac2004bebdebfbc02184333314c",
7
+ "dist/desktop.mjs": "652418d8a89ad3a9f7e306ed830b698df9b871147d58a9d5856b62e0f58b33fc",
8
8
  "dist/main.js": "8669f048a0e9a5d47dc657306109ab437da6683ad925fbd6a451aef72542a787",
9
- "manifest.json": "8eeb1fe22f305a45fb8a8d8be4c5ea36a679f7dcfee07c4ae9732275c776bcbb",
10
- "package.json": "51f549f7dab0cb60bd84399a7b6fc544745f10ab1f903dd8ee967e536f503629"
9
+ "manifest.json": "619832ba87b6d239244d51062537601ff84181ee00e6baabba6e3867ebcd9de7",
10
+ "package.json": "27cf696785b46a722f9efd4d7f114aeea02d7cf664ce343193f6ccc3efd9cfdf"
11
11
  },
12
- "sig": "gLd7ckUY+zTvo5WwIrwOu9F+rQOA+t1OWVHqydjWm4axD50O/NoUvGKpc/pvlifuThhJm+4gXofkzoVh1cZUBw=="
12
+ "sig": "IrYOnk4++zOfgv9C/bm+Bcq27QnCRwe011X3Wste3V4aeib5Aa6HAsWK9Vh4EtdDlRnLxq5kA5hYajktDfnCCQ=="
13
13
  }
package/dist/desktop.mjs CHANGED
@@ -1,7 +1,7 @@
1
- function v(r,a){if(typeof a!="string"||!a.startsWith("/"))return null;let n=String(r||"").replace(/\/+$/,"").replace(/\/api$/,"");if(!n)return null;let t,e;try{t=new URL(n),e=new URL(n+"/api"+a)}catch{return null}return t.protocol!=="http:"&&t.protocol!=="https:"||e.origin!==t.origin||!e.pathname.startsWith("/api/")?null:e.toString()}async function g(r,a,n,t,e=6e4){let o=await fetch(r.base+n,{method:a,headers:{"Content-Type":"application/json","X-Project-Source":"connect",...r.token?{Authorization:`Bearer ${r.token}`}:{}},body:t===void 0?void 0:JSON.stringify(t),signal:AbortSignal.timeout(e)}),l=await o.json().catch(()=>({}));return{status:o.status,json:l}}import{readdirSync as $,lstatSync as N,readFileSync as w,writeFileSync as R,realpathSync as T}from"node:fs";import{join as k,extname as M,sep as U}from"node:path";var _=300,S=5*1024*1024,E=20*1024*1024,F=50*1024*1024,H=new Set(["node_modules"]),L=new Set([".ts",".tsx",".jsx",".mts",".cts"]);function j(r,a){let n=[],t=0,e=(o,l)=>{for(let s of $(o)){if(s.startsWith("."))continue;let i=k(o,s),u;try{u=N(i)}catch{continue}if(u.isSymbolicLink())continue;let f=l?`${l}/${s}`:s;if(u.isDirectory()){H.has(s)||e(i,f);continue}if(!u.isFile()||s.endsWith(".d.ts"))continue;if(n.length>=_)throw new Error(`\u6587\u4EF6\u6570\u8D85\u8FC7 ${_}\uFF0C\u8BF7\u7CBE\u7B80\u9879\u76EE\uFF08node_modules \u4E0E\u9690\u85CF\u6587\u4EF6\u5DF2\u81EA\u52A8\u8DF3\u8FC7\uFF09`);let d=M(s).toLowerCase(),p,y=a.mimeOf(d)??"application/octet-stream";if(L.has(d)){let c=a.transpile(w(i,"utf8"),d,f);p=Buffer.from(c??"","utf8"),y="text/javascript"}else p=w(i);if(p.length>S)throw new Error(`\u5355\u6587\u4EF6\u8D85\u8FC7 5MB\uFF1A${f}`);if(t+=p.length,t>E)throw new Error("\u9879\u76EE\u603B\u5927\u5C0F\u8D85\u8FC7 20MB\uFF0C\u65E0\u6CD5\u53D1\u5E03");n.push({path:f,content_b64:p.toString("base64"),content_type:y,size:p.length})}};if(e(r,""),!n.length)throw new Error("\u9879\u76EE\u4E3A\u7A7A\uFF0C\u6CA1\u6709\u53EF\u53D1\u5E03\u7684\u6587\u4EF6");return{files:n,totalBytes:t}}var O=".forsion-connect.json";function P(r){try{return JSON.parse(w(k(r,O),"utf8"))}catch{return{}}}function x(r,a){try{R(k(r,O),JSON.stringify(a,null,2)+`
2
- `,"utf8")}catch{}}function I(r){let a={"/__forsion/chat":{method:"POST",path:"/api/chat/completions",auth:!0},"/__forsion/images":{method:"POST",path:"/api/images/generations",auth:!0},"/__forsion/user":{method:"GET",path:"/api/brain/users/me",auth:!0},"/__forsion/models":{method:"GET",path:"/api/models",auth:!1},"/__forsion/config":{method:"GET",path:"/api/connect/config",auth:!1},"/__forsion/agent":{method:"POST",path:"/api/agent/runs",auth:!0,agentRun:!0}};function n(t){if(a[t])return a[t];let e="/__forsion/agent-events/";if(t.startsWith(e)){let o=t.slice(e.length);if(/^[A-Za-z0-9-]{1,64}$/.test(o))return{method:"GET",path:`/api/agent/runs/${o}/events`,auth:!0,timeoutMs:9e5}}}return async(t,e)=>{let[o,l]=(t.url||"").split("?"),s=(c,m)=>{e.headersSent||(e.statusCode=c,e.setHeader("Content-Type","application/json")),e.end(JSON.stringify({detail:m}))},i=n(o);if(!i)return s(404,"unknown connect endpoint");if((t.method||"GET")!==i.method)return s(405,"method not allowed");let u="",f="";try{({base:u,token:f}=await r())}catch{}if(!u)return s(502,"\u672A\u914D\u7F6E Forsion \u4E91\u7AEF\u5730\u5740");if(i.auth&&!f)return s(401,"\u8BF7\u5148\u5728 Forsion Desktop \u4E2D\u767B\u5F55 Forsion \u8D26\u53F7");let d;if(i.method==="POST"){let c=[],m=0;try{for await(let h of t){if(m+=h.length,m>F)return t.destroy(),s(413,"\u8BF7\u6C42\u4F53\u8FC7\u5927");c.push(h)}}catch(h){return s(400,`\u8BF7\u6C42\u4F53\u8BFB\u53D6\u5931\u8D25\uFF1A${h?.message||String(h)}`)}d=Buffer.concat(c)}if(i.agentRun){let c;try{c=JSON.parse((d||Buffer.alloc(0)).toString("utf8")||"{}")}catch{return s(400,"agent \u8BF7\u6C42\u4F53\u5FC5\u987B\u662F JSON")}d=Buffer.from(JSON.stringify({session_id:typeof c.session_id=="string"?c.session_id:"",message:typeof c.message=="string"?c.message:"",app_id:"connect",agent_config:{}}))}let p=new AbortController,y=setTimeout(()=>p.abort(new Error("proxy timeout")),i.timeoutMs??3e5);t.on("close",()=>p.abort());try{let c=await fetch(u+i.path+(l?`?${l}`:""),{method:i.method,headers:{"Content-Type":"application/json","X-Project-Source":"connect",...f?{Authorization:`Bearer ${f}`}:{}},body:d&&d.length?new Uint8Array(d):void 0,signal:p.signal});if(e.statusCode=c.status,e.setHeader("Content-Type",c.headers.get("content-type")||"application/json"),e.setHeader("Cache-Control","no-store"),c.body)for await(let m of c.body)e.write(m);e.end()}catch(c){let m=`\u4E91\u7AEF\u8BF7\u6C42\u4E2D\u65AD\uFF1A${c?.message||String(c)}`;if(e.headersSent){try{e.write(`data: ${JSON.stringify({error:m})}
1
+ function N(e,o){if(typeof o!="string"||!o.startsWith("/"))return null;let t=String(e||"").replace(/\/+$/,"").replace(/\/api$/,"");if(!t)return null;let r,n;try{r=new URL(t),n=new URL(t+"/api"+o)}catch{return null}return r.protocol!=="http:"&&r.protocol!=="https:"||n.origin!==r.origin||!n.pathname.startsWith("/api/")?null:n.toString()}async function w(e,o,t,r,n=6e4){let s=await fetch(e.base+t,{method:o,headers:{"Content-Type":"application/json","X-Project-Source":"connect",...e.token?{Authorization:`Bearer ${e.token}`}:{}},body:r===void 0?void 0:JSON.stringify(r),signal:AbortSignal.timeout(n)}),c=await s.json().catch(()=>({}));return{status:s.status,json:c}}import{readdirSync as ie,lstatSync as ce,readFileSync as E,writeFileSync as ue,realpathSync as L}from"node:fs";import{join as x,extname as le,sep as de}from"node:path";var A=300,H=5*1024*1024,z=20*1024*1024,fe=50*1024*1024,me=new Set(["node_modules"]),ge=new Set([".ts",".tsx",".jsx",".mts",".cts"]);function D(e,o){let t=[],r=0,n=(s,c)=>{for(let a of ie(s)){if(a.startsWith("."))continue;let i=x(s,a),l;try{l=ce(i)}catch{continue}if(l.isSymbolicLink())continue;let m=c?`${c}/${a}`:a;if(l.isDirectory()){me.has(a)||n(i,m);continue}if(!l.isFile()||a.endsWith(".d.ts"))continue;if(t.length>=A)throw new Error(`\u6587\u4EF6\u6570\u8D85\u8FC7 ${A}\uFF0C\u8BF7\u7CBE\u7B80\u9879\u76EE\uFF08node_modules \u4E0E\u9690\u85CF\u6587\u4EF6\u5DF2\u81EA\u52A8\u8DF3\u8FC7\uFF09`);let u=le(a).toLowerCase(),d,p=o.mimeOf(u)??"application/octet-stream";if(ge.has(u)){let f=o.transpile(E(i,"utf8"),u,m);d=Buffer.from(f??"","utf8"),p="text/javascript"}else d=E(i);if(d.length>H)throw new Error(`\u5355\u6587\u4EF6\u8D85\u8FC7 5MB\uFF1A${m}`);if(r+=d.length,r>z)throw new Error("\u9879\u76EE\u603B\u5927\u5C0F\u8D85\u8FC7 20MB\uFF0C\u65E0\u6CD5\u53D1\u5E03");t.push({path:m,content_b64:d.toString("base64"),content_type:p,size:d.length})}};if(n(e,""),!t.length)throw new Error("\u9879\u76EE\u4E3A\u7A7A\uFF0C\u6CA1\u6709\u53EF\u53D1\u5E03\u7684\u6587\u4EF6");return{files:t,totalBytes:r}}var B=".forsion-connect.json";function J(e){try{return JSON.parse(E(x(e,B),"utf8"))}catch{return{}}}function q(e,o){try{ue(x(e,B),JSON.stringify(o,null,2)+`
2
+ `,"utf8")}catch{}}function W(e){let o={"/__forsion/chat":{method:"POST",path:"/api/chat/completions",auth:!0},"/__forsion/images":{method:"POST",path:"/api/images/generations",auth:!0},"/__forsion/user":{method:"GET",path:"/api/brain/users/me",auth:!0},"/__forsion/models":{method:"GET",path:"/api/models",auth:!1},"/__forsion/config":{method:"GET",path:"/api/connect/config",auth:!1},"/__forsion/agent":{method:"POST",path:"/api/agent/runs",auth:!0,agentRun:!0}};function t(r){if(o[r])return o[r];let n="/__forsion/agent-events/";if(r.startsWith(n)){let s=r.slice(n.length);if(/^[A-Za-z0-9-]{1,64}$/.test(s))return{method:"GET",path:`/api/agent/runs/${s}/events`,auth:!0,timeoutMs:9e5}}}return async(r,n)=>{let[s,c]=(r.url||"").split("?"),a=(f,g)=>{n.headersSent||(n.statusCode=f,n.setHeader("Content-Type","application/json")),n.end(JSON.stringify({detail:g}))},i=t(s);if(!i)return a(404,"unknown connect endpoint");if((r.method||"GET")!==i.method)return a(405,"method not allowed");let l="",m="";try{({base:l,token:m}=await e())}catch{}if(!l)return a(502,"\u672A\u914D\u7F6E Forsion \u4E91\u7AEF\u5730\u5740");if(i.auth&&!m)return a(401,"\u8BF7\u5148\u5728 Forsion Desktop \u4E2D\u767B\u5F55 Forsion \u8D26\u53F7");let u;if(i.method==="POST"){let f=[],g=0;try{for await(let h of r){if(g+=h.length,g>fe)return r.destroy(),a(413,"\u8BF7\u6C42\u4F53\u8FC7\u5927");f.push(h)}}catch(h){return a(400,`\u8BF7\u6C42\u4F53\u8BFB\u53D6\u5931\u8D25\uFF1A${h?.message||String(h)}`)}u=Buffer.concat(f)}if(i.agentRun){let f;try{f=JSON.parse((u||Buffer.alloc(0)).toString("utf8")||"{}")}catch{return a(400,"agent \u8BF7\u6C42\u4F53\u5FC5\u987B\u662F JSON")}u=Buffer.from(JSON.stringify({session_id:typeof f.session_id=="string"?f.session_id:"",message:typeof f.message=="string"?f.message:"",app_id:"connect",agent_config:{}}))}let d=new AbortController,p=setTimeout(()=>d.abort(new Error("proxy timeout")),i.timeoutMs??3e5);r.on("close",()=>d.abort());try{let f=await fetch(l+i.path+(c?`?${c}`:""),{method:i.method,headers:{"Content-Type":"application/json","X-Project-Source":"connect",...m?{Authorization:`Bearer ${m}`}:{}},body:u&&u.length?new Uint8Array(u):void 0,signal:d.signal});if(n.statusCode=f.status,n.setHeader("Content-Type",f.headers.get("content-type")||"application/json"),n.setHeader("Cache-Control","no-store"),f.body)for await(let g of f.body)n.write(g);n.end()}catch(f){let g=`\u4E91\u7AEF\u8BF7\u6C42\u4E2D\u65AD\uFF1A${f?.message||String(f)}`;if(n.headersSent){try{n.write(`data: ${JSON.stringify({error:g})}
3
3
 
4
- `)}catch{}e.end()}else s(502,m)}finally{clearTimeout(y)}}}var A=`
4
+ `)}catch{}n.end()}else a(502,g)}finally{clearTimeout(p)}}}var G=`
5
5
  (function () {
6
6
  'use strict';
7
7
  if (window.forsion) return;
@@ -190,4 +190,4 @@ function v(r,a){if(typeof a!="string"||!a.startsWith("/"))return null;let n=Stri
190
190
  ai: { chat: chat, agent: agentRun, generateImage: generateImage }
191
191
  };
192
192
  })();
193
- `,D=r=>({transpile:(a,n,t)=>r.transpileForServe(a,n,t),mimeOf:a=>r.mimeOf(a)});function b(r){let a=r;return typeof a.projectsRoot=="function"&&typeof a.transpileForServe=="function"&&typeof a.mimeOf=="function"&&typeof a.setPreviewHooks=="function"}function C(r){r.setPreviewHooks({sdkJs:A,proxy:I(()=>r.getCloud())}),r.handle("connect:meta",(a,n)=>typeof n=="string"&&n?P(n):{}),r.handle("connect:list",async()=>{let a=await r.getCloud();if(!a.token)return{ok:!1,code:"not_logged_in",detail:"\u8BF7\u5148\u767B\u5F55 Forsion \u8D26\u53F7"};try{let{status:n,json:t}=await g(a,"GET","/api/connect/apps");return n===401||n===403?{ok:!1,code:"not_logged_in",detail:"\u767B\u5F55\u5DF2\u8FC7\u671F,\u8BF7\u91CD\u65B0\u767B\u5F55"}:n!==200?{ok:!1,code:"error",detail:t?.detail||`HTTP ${n}`}:{ok:!0,base:a.base,...t}}catch(n){return{ok:!1,code:"error",detail:n?.message||String(n)}}}),r.handle("connect:publish",async(a,n)=>{if(!n||typeof n.dir!="string"||!n.dir||!n.name||!n.slug||!n.entry)return{ok:!1,code:"error",detail:"\u53C2\u6570\u4E0D\u5B8C\u6574"};let t;try{let o=T(r.projectsRoot());if(t=T(n.dir),t!==o&&!t.startsWith(o+U))return{ok:!1,code:"error",detail:"\u53EA\u80FD\u53D1\u5E03 Coding Space \u9879\u76EE\u76EE\u5F55"}}catch{return{ok:!1,code:"error",detail:"\u9879\u76EE\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u65E0\u6CD5\u8BBF\u95EE"}}let e=await r.getCloud();if(!e.token)return{ok:!1,code:"not_logged_in",detail:"\u8BF7\u5148\u767B\u5F55 Forsion \u8D26\u53F7"};try{let{files:o,totalBytes:l}=j(t,D(r));if(!o.some(u=>u.path===n.entry))return{ok:!1,code:"error",detail:`\u5165\u53E3\u6587\u4EF6\u4E0D\u5B58\u5728:${n.entry}`};let{status:s,json:i}=await g(e,"POST","/api/connect/apps",{slug:n.slug,name:n.name,entry:n.entry,files:o},3e5);return s===401?{ok:!1,code:"not_logged_in",detail:"\u767B\u5F55\u5DF2\u8FC7\u671F,\u8BF7\u91CD\u65B0\u767B\u5F55"}:s!==200?{ok:!1,code:i?.code||"error",detail:i?.detail||`HTTP ${s}`,used:i?.used,limit:i?.limit}:(x(t,{slug:String(i.slug)}),{ok:!0,slug:i.slug,handle:i.handle,url:`${e.base}${i.url}`,totalBytes:l})}catch(o){return{ok:!1,code:"error",detail:o?.message||String(o)}}}),r.handle("connect:unpublish",async(a,n)=>{if(!n||typeof n!="string")return{ok:!1,code:"error",detail:"\u53C2\u6570\u4E0D\u5B8C\u6574"};let t=await r.getCloud();if(!t.token)return{ok:!1,code:"not_logged_in",detail:"\u8BF7\u5148\u767B\u5F55 Forsion \u8D26\u53F7"};try{let{status:e,json:o}=await g(t,"DELETE",`/api/connect/apps/${encodeURIComponent(n)}`);return e===401||e===403?{ok:!1,code:"not_logged_in",detail:"\u767B\u5F55\u5DF2\u8FC7\u671F,\u8BF7\u91CD\u65B0\u767B\u5F55"}:e!==200&&e!==404?{ok:!1,code:"error",detail:o?.detail||`HTTP ${e}`}:{ok:!0}}catch(e){return{ok:!1,code:"error",detail:e?.message||String(e)}}}),r.handle("connect:listingApply",async(a,n)=>{if(!n||typeof n.slug!="string"||!n.slug||typeof n.summary!="string"||!n.summary.trim())return{ok:!1,code:"error",detail:"\u53C2\u6570\u4E0D\u5B8C\u6574"};let t=await r.getCloud();if(!t.token)return{ok:!1,code:"not_logged_in",detail:"\u8BF7\u5148\u767B\u5F55 Forsion \u8D26\u53F7"};try{let{status:e,json:o}=await g(t,"PUT",`/api/connect/apps/${encodeURIComponent(n.slug)}/listing`,{summary:n.summary});return e===401?{ok:!1,code:"not_logged_in",detail:"\u767B\u5F55\u5DF2\u8FC7\u671F,\u8BF7\u91CD\u65B0\u767B\u5F55"}:e!==200?{ok:!1,code:o?.code||"error",detail:o?.detail||`HTTP ${e}`}:{ok:!0,status:o.status}}catch(e){return{ok:!1,code:"error",detail:e?.message||String(e)}}}),r.handle("connect:listingWithdraw",async(a,n)=>{if(!n||typeof n!="string")return{ok:!1,code:"error",detail:"\u53C2\u6570\u4E0D\u5B8C\u6574"};let t=await r.getCloud();if(!t.token)return{ok:!1,code:"not_logged_in",detail:"\u8BF7\u5148\u767B\u5F55 Forsion \u8D26\u53F7"};try{let{status:e,json:o}=await g(t,"DELETE",`/api/connect/apps/${encodeURIComponent(n)}/listing`);return e===401?{ok:!1,code:"not_logged_in",detail:"\u767B\u5F55\u5DF2\u8FC7\u671F,\u8BF7\u91CD\u65B0\u767B\u5F55"}:e!==200&&e!==404?{ok:!1,code:"error",detail:o?.detail||`HTTP ${e}`}:{ok:!0}}catch(e){return{ok:!1,code:"error",detail:e?.message||String(e)}}}),r.handle("connect:store",async()=>{let a=await r.getCloud();try{let{status:n,json:t}=await g(a,"GET","/api/connect/store");return n!==200?{ok:!1,detail:t?.detail||`HTTP ${n}`}:{ok:!0,base:a.base,items:Array.isArray(t.items)?t.items:[]}}catch(n){return{ok:!1,detail:n?.message||String(n)}}}),r.log("[forsion-extend] connect handlers registered")}function V(r){r.handle("auth:openAccountCenter",async(n,t)=>{let{base:e,token:o}=await r.getCloud();if(!e)return{ok:!1};let l=t&&/^[a-z0-9-]+$/i.test(t)?`#${t}`:"",s=`${e}/account${o?`?token=${encodeURIComponent(o)}`:""}${l}`;return await r.openExternal(s),{ok:!0}}),r.handle("auth:openPayCenter",async()=>{let{base:n,token:t}=await r.getCloud();if(!n)return{ok:!1};let e=`${n}/pay?tab=membership${t?`&token=${encodeURIComponent(t)}`:""}&redirect=${encodeURIComponent(`${n}/account`)}`;return await r.openExternal(e),{ok:!0}});let a=()=>r.getCloud();r.handle("account:quota",async()=>{let n=await a();if(!n.token)return{status:401,json:null};try{return await g(n,"GET","/api/token-quota/my",void 0,15e3)}catch(t){return{status:0,json:{detail:String(t?.message||t)}}}}),r.handle("account:useResetCard",async(n,t)=>{let e=await a();if(!e.token)return{status:401,json:null};if(t!==void 0&&t!=="both"&&t!=="weekly")return{status:400,json:{error:"invalid_type",detail:`invalid reset card type: ${t}`}};let o=t??"both";try{return await g(e,"POST","/api/token-quota/reset-card/use",{type:o},15e3)}catch(l){return{status:0,json:{detail:String(l?.message||l)}}}}),r.handle("account:bgConvert",async(n,t)=>{let e=await a();if(!e.token)return{status:401,json:null};if(!Number.isInteger(t)||t<1||t>100)return{status:400,json:{error:"invalid_percent"}};try{return await g(e,"POST","/api/token-quota/background/convert",{percent:t},15e3)}catch(o){return{status:0,json:{detail:String(o?.message||o)}}}}),r.handle("account:bgAutoMain",async(n,t)=>{let e=await a();if(!e.token)return{status:401,json:null};if(typeof t!="boolean")return{status:400,json:{error:"invalid_request"}};try{return await g(e,"POST","/api/token-quota/background/auto-main",{enabled:t},15e3)}catch(o){return{status:0,json:{detail:String(o?.message||o)}}}}),r.handle("feedback:submit",async(n,t)=>{let{base:e,token:o}=await r.getCloud();if(!e||!o)return{ok:!1,error:"not-logged-in"};let l=(t?.description||"").trim();if(!l)return{ok:!1,error:"empty"};if(l.length>1e4)return{ok:!1,error:"description-too-long"};let s=[],i=!1;if(t?.sessionLogJson){let u=Buffer.from(t.sessionLogJson,"utf8");if(u.length>5*1024*1024)return{ok:!1,error:"attachment-too-large"};s.push({filename:t.sessionLogName||"tangu-session.json",mime_type:"application/json",size:u.length,data_base64:u.toString("base64")})}try{let u=await fetch(`${e}/api/feedback`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${o}`},body:JSON.stringify({description:l,attachments:s}),signal:AbortSignal.timeout(2e4)});if(!u.ok){let d=`HTTP ${u.status}`;try{d=(await u.json())?.detail||d}catch{}return{ok:!1,error:d}}return{ok:!0,id:(await u.json().catch(()=>({})))?.id??null,attachmentSkipped:i}}catch(u){return{ok:!1,error:u?.message||String(u)}}}),r.handle("cloud:fetch",async(n,t)=>{if(!r.isTrustedSender(n))return{status:0,error:"untrusted"};let e=t??{},o=typeof e.path=="string"?e.path:"",l=Math.min(12e4,Math.max(1e3,Number(e.timeoutMs)||15e3)),s=typeof e.method=="string"?e.method.toUpperCase():"GET";if(!/^(GET|POST|PUT|PATCH|DELETE)$/.test(s))return{status:0,error:"bad_method"};if(!o.startsWith("/"))return{status:0,error:"bad_path"};let{base:i,token:u}=await r.getCloud();if(!String(i||""))return{status:0,error:"no_cloud_url"};let f=v(i,o);if(!f)return{status:0,error:"bad_path"};if(!u)return{status:401,error:"not_signed_in"};let d=e.body!==void 0&&s!=="GET"&&s!=="DELETE",p=new AbortController,y=setTimeout(()=>p.abort(),l);try{let c=await fetch(f,{method:s,headers:{Authorization:`Bearer ${u}`,...d?{"Content-Type":"application/json"}:{}},body:d?JSON.stringify(e.body):void 0,signal:p.signal});return{status:c.status,json:await c.json().catch(()=>null)}}catch(c){return{status:0,error:String(c?.message||c)}}finally{clearTimeout(y)}}),r.log("[forsion-extend] cloud account handlers registered"),b(r)?C(r):r.log("[forsion-extend] host has no Connect seam (projectsRoot / transpileForServe / mimeOf / setPreviewHooks); Connect stays with the host")}export{_ as CONNECT_MAX_FILES,S as CONNECT_MAX_FILE_BYTES,E as CONNECT_MAX_TOTAL_BYTES,A as FORSION_CONNECT_LOCAL_SDK,g as cloudJson,j as collectProjectFiles,b as hasConnectHost,I as makePreviewProxy,P as readConnectMeta,V as registerCloud,C as registerConnect,x as writeConnectMeta};
193
+ `,pe=e=>({transpile:(o,t,r)=>e.transpileForServe(o,t,r),mimeOf:o=>e.mimeOf(o)});function j(e){let o=e;return typeof o.projectsRoot=="function"&&typeof o.transpileForServe=="function"&&typeof o.mimeOf=="function"&&typeof o.setPreviewHooks=="function"}function P(e){e.setPreviewHooks({sdkJs:G,proxy:W(()=>e.getCloud())}),e.handle("connect:meta",(o,t)=>typeof t=="string"&&t?J(t):{}),e.handle("connect:list",async()=>{let o=await e.getCloud();if(!o.token)return{ok:!1,code:"not_logged_in",detail:"\u8BF7\u5148\u767B\u5F55 Forsion \u8D26\u53F7"};try{let{status:t,json:r}=await w(o,"GET","/api/connect/apps");return t===401||t===403?{ok:!1,code:"not_logged_in",detail:"\u767B\u5F55\u5DF2\u8FC7\u671F,\u8BF7\u91CD\u65B0\u767B\u5F55"}:t!==200?{ok:!1,code:"error",detail:r?.detail||`HTTP ${t}`}:{ok:!0,base:o.base,...r}}catch(t){return{ok:!1,code:"error",detail:t?.message||String(t)}}}),e.handle("connect:publish",async(o,t)=>{if(!t||typeof t.dir!="string"||!t.dir||!t.name||!t.slug||!t.entry)return{ok:!1,code:"error",detail:"\u53C2\u6570\u4E0D\u5B8C\u6574"};let r;try{let s=L(e.projectsRoot());if(r=L(t.dir),r!==s&&!r.startsWith(s+de))return{ok:!1,code:"error",detail:"\u53EA\u80FD\u53D1\u5E03 Coding Space \u9879\u76EE\u76EE\u5F55"}}catch{return{ok:!1,code:"error",detail:"\u9879\u76EE\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u65E0\u6CD5\u8BBF\u95EE"}}let n=await e.getCloud();if(!n.token)return{ok:!1,code:"not_logged_in",detail:"\u8BF7\u5148\u767B\u5F55 Forsion \u8D26\u53F7"};try{let{files:s,totalBytes:c}=D(r,pe(e));if(!s.some(l=>l.path===t.entry))return{ok:!1,code:"error",detail:`\u5165\u53E3\u6587\u4EF6\u4E0D\u5B58\u5728:${t.entry}`};let{status:a,json:i}=await w(n,"POST","/api/connect/apps",{slug:t.slug,name:t.name,entry:t.entry,files:s},3e5);return a===401?{ok:!1,code:"not_logged_in",detail:"\u767B\u5F55\u5DF2\u8FC7\u671F,\u8BF7\u91CD\u65B0\u767B\u5F55"}:a!==200?{ok:!1,code:i?.code||"error",detail:i?.detail||`HTTP ${a}`,used:i?.used,limit:i?.limit}:(q(r,{slug:String(i.slug)}),{ok:!0,slug:i.slug,handle:i.handle,url:`${n.base}${i.url}`,totalBytes:c})}catch(s){return{ok:!1,code:"error",detail:s?.message||String(s)}}}),e.handle("connect:unpublish",async(o,t)=>{if(!t||typeof t!="string")return{ok:!1,code:"error",detail:"\u53C2\u6570\u4E0D\u5B8C\u6574"};let r=await e.getCloud();if(!r.token)return{ok:!1,code:"not_logged_in",detail:"\u8BF7\u5148\u767B\u5F55 Forsion \u8D26\u53F7"};try{let{status:n,json:s}=await w(r,"DELETE",`/api/connect/apps/${encodeURIComponent(t)}`);return n===401||n===403?{ok:!1,code:"not_logged_in",detail:"\u767B\u5F55\u5DF2\u8FC7\u671F,\u8BF7\u91CD\u65B0\u767B\u5F55"}:n!==200&&n!==404?{ok:!1,code:"error",detail:s?.detail||`HTTP ${n}`}:{ok:!0}}catch(n){return{ok:!1,code:"error",detail:n?.message||String(n)}}}),e.handle("connect:listingApply",async(o,t)=>{if(!t||typeof t.slug!="string"||!t.slug||typeof t.summary!="string"||!t.summary.trim())return{ok:!1,code:"error",detail:"\u53C2\u6570\u4E0D\u5B8C\u6574"};let r=await e.getCloud();if(!r.token)return{ok:!1,code:"not_logged_in",detail:"\u8BF7\u5148\u767B\u5F55 Forsion \u8D26\u53F7"};try{let{status:n,json:s}=await w(r,"PUT",`/api/connect/apps/${encodeURIComponent(t.slug)}/listing`,{summary:t.summary});return n===401?{ok:!1,code:"not_logged_in",detail:"\u767B\u5F55\u5DF2\u8FC7\u671F,\u8BF7\u91CD\u65B0\u767B\u5F55"}:n!==200?{ok:!1,code:s?.code||"error",detail:s?.detail||`HTTP ${n}`}:{ok:!0,status:s.status}}catch(n){return{ok:!1,code:"error",detail:n?.message||String(n)}}}),e.handle("connect:listingWithdraw",async(o,t)=>{if(!t||typeof t!="string")return{ok:!1,code:"error",detail:"\u53C2\u6570\u4E0D\u5B8C\u6574"};let r=await e.getCloud();if(!r.token)return{ok:!1,code:"not_logged_in",detail:"\u8BF7\u5148\u767B\u5F55 Forsion \u8D26\u53F7"};try{let{status:n,json:s}=await w(r,"DELETE",`/api/connect/apps/${encodeURIComponent(t)}/listing`);return n===401?{ok:!1,code:"not_logged_in",detail:"\u767B\u5F55\u5DF2\u8FC7\u671F,\u8BF7\u91CD\u65B0\u767B\u5F55"}:n!==200&&n!==404?{ok:!1,code:"error",detail:s?.detail||`HTTP ${n}`}:{ok:!0}}catch(n){return{ok:!1,code:"error",detail:n?.message||String(n)}}}),e.handle("connect:store",async()=>{let o=await e.getCloud();try{let{status:t,json:r}=await w(o,"GET","/api/connect/store");return t!==200?{ok:!1,detail:r?.detail||`HTTP ${t}`}:{ok:!0,base:o.base,items:Array.isArray(r.items)?r.items:[]}}catch(t){return{ok:!1,detail:t?.message||String(t)}}}),e.log("[forsion-extend] connect handlers registered")}import{createHash as he}from"node:crypto";async function v(e,o){if(!e||!o)return{status:"offline"};let t=e.replace(/\/+$/,"");try{let r=await fetch(`${t}/api/brain/users/me`,{headers:{Authorization:`Bearer ${o}`},signal:AbortSignal.timeout(5e3)});if(r.status===401||r.status===403)return{status:(r.headers.get("content-type")||"").includes("application/json")?"expired":"offline"};if(!r.ok)return{status:"offline"};let n=await r.json();if(!n)return{status:"offline"};let s=n.avatar||n.avatarUrl||n.avatar_url||void 0;return s&&/^\/(?!\/)/.test(s)&&(s=t+s),{status:"ok",user:{username:n.username||n.nickname||void 0,nickname:n.nickname||void 0,avatar:s,membershipTier:n.membershipTier||n.membership_tier||void 0}}}catch{return{status:"offline"}}}var X=e=>`${e.seq}:${e.hash}`,ye=e=>he("sha256").update(e).digest("hex");function $(e){if(e===null)return{baseSeq:0,baseHash:null};if(typeof e!="string")return;let o=e.indexOf(":"),t=Number(o===-1?e:e.slice(0,o));if(!Number.isInteger(t)||t<=0)return;let r=o===-1?"":e.slice(o+1);return{baseSeq:t,baseHash:/^[0-9a-f]{64}$/.test(r)?r:null}}function V(e){let o=e.baseUrl.replace(/\/+$/,""),t=n=>`${o}/api/remotesync/vaults/${encodeURIComponent(e.vault)}${n}`,r=async(n,s)=>{let c=e.getToken();if(!c)throw new Error("penzor-not-logged-in");let a=await fetch(n,{...s,headers:{...s.headers,Authorization:`Bearer ${c}`}});if(a.status===409){let i=await a.json().catch(()=>({}));throw new Error(`cas-conflict ${String(i.code??"")}: seq=${String(i.seq??"?")}`)}if(!a.ok){let i=await a.text().catch(()=>"");throw new Error(`penzor ${s.method??"GET"} ${a.status}: ${i.slice(0,200)}`)}return a};return{kind:"penzor",async walk(n){let s=await r(t("/manifest"),{method:"GET",signal:n}),{files:c}=await s.json();return c.map(a=>({key:a.path,size:a.size,mtimeMs:a.mtimeMs,id:X(a)}))},async readFile(n,s){let a=await(await r(t(`/file?path=${encodeURIComponent(n)}`),{method:"GET",signal:s})).json(),i=await fetch(a.url,{signal:s});if(!i.ok)throw new Error(`penzor blob GET ${i.status}: ${n}`);let l=Buffer.from(await i.arrayBuffer());if(ye(l)!==a.hash)throw new Error(`penzor hash mismatch: ${n}`);return l},async writeFile(n,s,c,a,i){let l=$(i),m=new URLSearchParams({path:n,mtimeMs:String(Math.floor(c)||0)});l&&(m.set("baseSeq",String(l.baseSeq)),l.baseHash&&m.set("baseHash",l.baseHash));let d=await(await r(t(`/file?${m.toString()}`),{method:"PUT",body:new Uint8Array(s),headers:{"Content-Type":"application/octet-stream"},signal:a})).json();return{key:n,size:d.size,mtimeMs:d.mtimeMs,id:X(d)}},async rm(n,s,c){let a=$(c),i=new URLSearchParams({path:n});a&&a.baseSeq>0&&(i.set("baseSeq",String(a.baseSeq)),a.baseHash&&i.set("baseHash",a.baseHash)),await r(t(`/file?${i.toString()}`),{method:"DELETE",signal:s})},async check(){try{return await r(t("/manifest"),{method:"GET"}),{ok:!0}}catch(n){return{ok:!1,error:String(n?.message||n)}}}}}var Y=50*1024*1024,K=new Map;async function we(e,o){let t=K.get(o);if(t)return{id:t};let r=await v(e,o);if(r.status==="offline")return{error:"penzor-offline"};if(r.status!=="ok")return{error:"penzor-not-logged-in"};let n=r.user?.username?.trim();return n?(K.set(o,n),{id:n}):{error:"penzor-auth-unverified"}}var R=e=>typeof e.registerRemoteSyncBackend=="function"&&typeof e.readCreds=="function";function O(e){e.registerRemoteSyncBackend("penzor",async o=>{let t=e.readCreds();if(!t.token||!t.cloudUrl)return{error:"penzor-not-logged-in"};let r=o?.vault,n=(typeof r=="string"?r:"default").trim()||"default";if(!/^[A-Za-z0-9_-]{1,64}$/.test(n))return{error:"penzor-bad-vault"};let s=await we(t.cloudUrl,t.token);if("error"in s)return s;let c=t.token;return{remote:V({baseUrl:t.cloudUrl,vault:n,getToken:()=>c}),fingerprint:`penzor:${t.cloudUrl.replace(/\/+$/,"")}|u:${s.id}|${n}`,maxFileBytes:Y}}),e.log('[forsion-extend] remote-sync backend "penzor" registered')}import{readFileSync as ke,writeFileSync as be,mkdirSync as Ce,chmodSync as ve,renameSync as Se}from"node:fs";import{join as _e}from"node:path";function C(e,o){try{let t=new URL(e);if(t.protocol!=="https:"&&t.protocol!=="http:")return null;let r=o.split(".")[1];if(!r)return null;let n=r.replace(/-/g,"+").replace(/_/g,"/"),s=Uint8Array.from(atob(n),i=>i.charCodeAt(0)),c=JSON.parse(new TextDecoder().decode(s)),a=c.userId??c.sub;return typeof a!="string"&&typeof a!="number"||!String(a).trim()?null:`${t.origin}${t.pathname.replace(/\/+$/,"")}::${String(a)}`}catch{return null}}function U(e){try{let o=e.split(".")[1];if(!o)return null;let t=JSON.parse(Buffer.from(o,"base64url").toString("utf8"));return t&&typeof t=="object"?t:null}catch{return null}}function Q(e,o=Date.now()){let t=U(e)?.iat;return typeof t=="number"?o-t*1e3:1/0}function Te(e,o=Date.now()){let t=U(e)?.exp;return typeof t=="number"?t*1e3-o:1/0}var Ee=36e5;function ee(e,o=Date.now()){return!!e&&Q(e,o)>=Ee}async function te(e,o,t=8e3,r){if(!e||!o)return null;let n=e.replace(/\/+$/,"");try{let s=await fetch(`${n}/api/auth/refresh`,{method:"POST",headers:{Authorization:`Bearer ${o}`,...r?{"X-Forsion-Client":r}:{}},signal:AbortSignal.timeout(t)});if(!s.ok)return null;let c=await s.json().catch(()=>null),a=typeof c?.token=="string"?c.token:"";return a&&a!==o?a:null}catch{return null}}var T=(e,o)=>`${e.replace(/\/+$/,"")}\0${o}`,Ae=(e,o)=>new Promise((t,r)=>{let n=()=>{clearTimeout(s),r(new Error("Sign-in cancelled."))},s=setTimeout(()=>{o.removeEventListener("abort",n),t()},e);o.aborted?n():o.addEventListener("abort",n,{once:!0})});function ne(e){let o=()=>_e(e.homeDir(),"auth-accounts.json"),t=(u,d)=>{Ce(e.homeDir(),{recursive:!0});let p=`${u}.tmp`;be(p,JSON.stringify(d,null,2),{encoding:"utf8",mode:384}),ve(p,384),Se(p,u)},r=()=>{try{let u=JSON.parse(ke(o(),"utf8"));return Array.isArray(u.accounts)?u.accounts.filter(d=>d&&typeof d.token=="string"&&d.id===C(d.cloudUrl,d.token)):[]}catch{return[]}},n=(u,d)=>{let p=C(u.cloudUrl||"",u.token||"");if(!p||!u.token||!u.cloudUrl)return;let f=r(),g=f.find(k=>k.id===p),h=U(u.token),y={...g,id:p,cloudUrl:u.cloudUrl.replace(/\/+$/,""),token:u.token,username:d?.username||g?.username||h?.username,nickname:d?.nickname||g?.nickname};JSON.stringify(g)!==JSON.stringify(y)&&t(o(),{version:1,accounts:[...f.filter(k=>k.id!==p),y]})},s=()=>{let u=e.readCreds();n(u);let d=C(u.cloudUrl,u.token);return r().map(({token:p,...f})=>({...f,active:f.id===d}))},c=u=>{let d=r().find(p=>p.id===u);if(!d)throw new Error("Account is no longer signed in. Sign in again.");return{cloudUrl:d.cloudUrl,token:d.token}},a=u=>{let d=C(u.cloudUrl||"",u.token||"");d&&t(o(),{version:1,accounts:r().filter(p=>p.id!==d)})},i=null;return{rememberForsionAccount:n,forsionAccounts:s,savedForsionAccount:c,forgetForsionAccount:a,cancelForsionLogin:()=>{i?.abort(),i=null},forsionDeviceLogin:async(u,d,p)=>{if(i)throw new Error("A sign-in is already in progress.");if(!u)throw new Error("\u8BF7\u5148\u586B\u5199 Forsion \u4E91\u7AEF\u5730\u5740(\u6216\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF TANGU_CLOUD_URL)");let f=new AbortController;i=f;let g=()=>{if(f.signal.aborted||i!==f)throw new Error("Sign-in cancelled.")};try{let h=u.replace(/\/+$/,""),y;try{y=await fetch(`${h}/api/auth/cli/start`,{method:"POST",headers:{"Content-Type":"application/json"},body:"{}",signal:AbortSignal.any([f.signal,AbortSignal.timeout(15e3)])}).then(b=>b.json())}catch(b){throw new Error(`\u65E0\u6CD5\u8FDE\u63A5 ${h}:${b?.message||b}`)}if(g(),!y?.device_code)throw new Error("\u4E91\u7AEF\u4E0D\u652F\u6301 CLI \u767B\u5F55(/api/auth/cli/start \u8FD4\u56DE\u5F02\u5E38)");let k=y.verification_uri_complete||`${y.verification_uri}?code=${y.user_code}`,S=new URL(`${h}/auth`);S.searchParams.set("logout","1"),S.searchParams.set("app","forsion-desktop"),S.searchParams.set("redirect",k);let M=S.toString();d?.({url:M,userCode:String(y.user_code||"")}),e.openExternal(M);let oe=Date.now()+(y.expires_in||600)*1e3,se=(y.interval||2)*1e3;for(;Date.now()<oe;){await Ae(se,f.signal),g();let b=null;try{b=await fetch(`${h}/api/auth/cli/poll`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({device_code:y.device_code}),signal:AbortSignal.any([f.signal,AbortSignal.timeout(15e3)])})}catch{g();continue}if(g(),b.status===410)throw new Error("\u767B\u5F55\u7801\u5DF2\u8FC7\u671F,\u8BF7\u91CD\u65B0\u53D1\u8D77\u767B\u5F55");let _=await b.json().catch(()=>({status:"pending"}));if(g(),_.status==="approved"&&_.token){let ae={cloudUrl:h,token:_.token};return await p(ae,g),g(),{token:_.token,cloudUrl:h}}}throw new Error("\u767B\u5F55\u8D85\u65F6,\u8BF7\u91CD\u8BD5")}finally{i===f&&(i=null)}},readAccounts:r}}var re=["homeDir","appVersion","broadcast","accountBackendState","accountTransition","accountCommit","accountClear","writeCreds","onExternalCredsChange","setTokenRefresher","readCreds"],I=e=>re.every(o=>typeof e[o]=="function");function F(e){let o=ne(e),t=0,r=()=>{let s=e.readCreds();return T(s.cloudUrl,s.token)},n=null;e.handle("auth:status",async()=>{let s=t,c=r(),{base:a}=await e.getCloud(),i=e.readCreds().token,l=i?await v(a,i):null,m=await e.accountBackendState(),{base:u}=await e.getCloud(),d=e.readCreds(),p=T(u,d.token)!==T(a,i),f=T(a,i);!p&&l?.status==="ok"&&l.user&&(n={key:f,user:l.user},o.rememberForsionAccount({...d,cloudUrl:a},l.user));let g=p?void 0:l?.status==="ok"?l.user:l?.status==="offline"&&n?.key===f?n.user:void 0;if(p)return{loggedIn:!!d.token,tokenValid:null,cloudUrl:u,accountId:C(u,d.token),username:null,nickname:null,avatar:null,membershipTier:null,tokenSource:d.token?"tangu-login":null,backendState:m};if(l?.status==="expired"){await e.accountTransition(async()=>{if(s!==t||r()!==c)return;let y=()=>{if(s!==t||r()!==c)throw Z};try{await e.accountClear(y,()=>{e.log("[auth] token \u5DF2\u5931\u6548(\u670D\u52A1\u7AEF 401/403),\u81EA\u52A8\u767B\u51FA"),++t,o.cancelForsionLogin(),o.forgetForsionAccount(e.readCreds()),n=null})}catch(k){if(k!==Z)throw k}});let h=e.readCreds();return{loggedIn:!!h.token,tokenValid:null,cloudUrl:h.cloudUrl||a,accountId:C(h.cloudUrl||a,h.token),username:null,nickname:null,avatar:null,membershipTier:null,tokenSource:h.token?"tangu-login":null,backendState:m}}return{loggedIn:!!i,tokenValid:l&&l.status==="ok"?!0:null,cloudUrl:a,accountId:C(a,i),username:g?.username||null,nickname:g?.nickname||null,avatar:g?.avatar||null,membershipTier:g?.membershipTier||null,tokenSource:i?"tangu-login":null,backendState:m}}),e.handle("auth:accounts",()=>o.forsionAccounts()),e.handle("auth:switchAccount",async(s,c)=>{if(typeof c!="string")throw new Error("Invalid account.");let a=++t;return o.cancelForsionLogin(),e.accountTransition(async()=>{let i=o.savedForsionAccount(c);return await e.accountCommit(i,()=>{if(a!==t)throw new Error("Account switch cancelled.")},()=>{o.rememberForsionAccount(i),n=null}),{ok:!0,cloudUrl:i.cloudUrl||""}})}),e.handle("auth:forsionLogin",async(s,c)=>{let a=++t;o.cancelForsionLogin();let{base:i}=await e.getCloud();if(a!==t)throw new Error("Sign-in cancelled.");let l=(c||i||"").trim();return{ok:!0,cloudUrl:(await o.forsionDeviceLogin(l,u=>e.broadcast("auth:device",u),(u,d)=>e.accountTransition(()=>e.accountCommit(u,()=>{if(d(),a!==t)throw new Error("Sign-in cancelled.")},()=>{o.rememberForsionAccount(u),n=null})))).cloudUrl}}),e.handle("auth:logout",async()=>(++t,o.cancelForsionLogin(),e.accountTransition(async()=>{let{base:s}=await e.getCloud(),c=e.readCreds();return await e.accountClear(void 0,()=>{o.forgetForsionAccount(c),n=null,s&&c.token&&fetch(`${s}/api/auth/logout`,{method:"POST",headers:{Authorization:`Bearer ${c.token}`},signal:AbortSignal.timeout(5e3)}).catch(()=>{})}),{ok:!0}}))),e.onExternalCredsChange(({previous:s,current:c})=>{++t,o.cancelForsionLogin(),s.token&&!c.token&&o.forgetForsionAccount(s),n=null,o.rememberForsionAccount(c)}),e.setTokenRefresher(async s=>{let c=t,a=r(),i=e.readCreds().token;if(!ee(i))return;let{base:l}=await e.getCloud(),m=await te(l,i,s,`desktop/${e.appVersion()}`);if(!m){e.log("[auth] \u6ED1\u52A8\u7EED\u671F\u672A\u6210\u529F(\u79BB\u7EBF/\u8001\u7248\u672C server/\u8FD9\u679A\u5DF2\u5931\u6548),\u7EE7\u7EED\u7528\u65E7 token");return}c!==t||r()!==a||(o.rememberForsionAccount({cloudUrl:l,token:m}),e.writeCreds({cloudUrl:l,token:m}),e.log("[auth] \u767B\u5F55\u6001\u5DF2\u6ED1\u52A8\u7EED\u671F(\u6709\u6548\u671F\u91CD\u65B0\u8BA1\u6EE1 2 \u5468)"))}),e.log("[forsion-extend] account core registered")}var Z=Symbol("stale-auth-intent");function Ge(e){e.handle("auth:openAccountCenter",async(t,r)=>{let{base:n,token:s}=await e.getCloud();if(!n)return{ok:!1};let c=r&&/^[a-z0-9-]+$/i.test(r)?`#${r}`:"",a=`${n}/account${s?`?token=${encodeURIComponent(s)}`:""}${c}`;return await e.openExternal(a),{ok:!0}}),e.handle("auth:openPayCenter",async()=>{let{base:t,token:r}=await e.getCloud();if(!t)return{ok:!1};let n=`${t}/pay?tab=membership${r?`&token=${encodeURIComponent(r)}`:""}&redirect=${encodeURIComponent(`${t}/account`)}`;return await e.openExternal(n),{ok:!0}});let o=()=>e.getCloud();e.handle("account:quota",async()=>{let t=await o();if(!t.token)return{status:401,json:null};try{return await w(t,"GET","/api/token-quota/my",void 0,15e3)}catch(r){return{status:0,json:{detail:String(r?.message||r)}}}}),e.handle("account:useResetCard",async(t,r)=>{let n=await o();if(!n.token)return{status:401,json:null};if(r!==void 0&&r!=="both"&&r!=="weekly")return{status:400,json:{error:"invalid_type",detail:`invalid reset card type: ${r}`}};let s=r??"both";try{return await w(n,"POST","/api/token-quota/reset-card/use",{type:s},15e3)}catch(c){return{status:0,json:{detail:String(c?.message||c)}}}}),e.handle("account:bgConvert",async(t,r)=>{let n=await o();if(!n.token)return{status:401,json:null};if(!Number.isInteger(r)||r<1||r>100)return{status:400,json:{error:"invalid_percent"}};try{return await w(n,"POST","/api/token-quota/background/convert",{percent:r},15e3)}catch(s){return{status:0,json:{detail:String(s?.message||s)}}}}),e.handle("account:bgAutoMain",async(t,r)=>{let n=await o();if(!n.token)return{status:401,json:null};if(typeof r!="boolean")return{status:400,json:{error:"invalid_request"}};try{return await w(n,"POST","/api/token-quota/background/auto-main",{enabled:r},15e3)}catch(s){return{status:0,json:{detail:String(s?.message||s)}}}}),e.handle("feedback:submit",async(t,r)=>{let{base:n,token:s}=await e.getCloud();if(!n||!s)return{ok:!1,error:"not-logged-in"};let c=(r?.description||"").trim();if(!c)return{ok:!1,error:"empty"};if(c.length>1e4)return{ok:!1,error:"description-too-long"};let a=[],i=!1;if(r?.sessionLogJson){let l=Buffer.from(r.sessionLogJson,"utf8");if(l.length>5*1024*1024)return{ok:!1,error:"attachment-too-large"};a.push({filename:r.sessionLogName||"tangu-session.json",mime_type:"application/json",size:l.length,data_base64:l.toString("base64")})}try{let l=await fetch(`${n}/api/feedback`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${s}`},body:JSON.stringify({description:c,attachments:a}),signal:AbortSignal.timeout(2e4)});if(!l.ok){let u=`HTTP ${l.status}`;try{u=(await l.json())?.detail||u}catch{}return{ok:!1,error:u}}return{ok:!0,id:(await l.json().catch(()=>({})))?.id??null,attachmentSkipped:i}}catch(l){return{ok:!1,error:l?.message||String(l)}}}),e.handle("cloud:fetch",async(t,r)=>{if(!e.isTrustedSender(t))return{status:0,error:"untrusted"};let n=r??{},s=typeof n.path=="string"?n.path:"",c=Math.min(12e4,Math.max(1e3,Number(n.timeoutMs)||15e3)),a=typeof n.method=="string"?n.method.toUpperCase():"GET";if(!/^(GET|POST|PUT|PATCH|DELETE)$/.test(a))return{status:0,error:"bad_method"};if(!s.startsWith("/"))return{status:0,error:"bad_path"};let{base:i,token:l}=await e.getCloud();if(!String(i||""))return{status:0,error:"no_cloud_url"};let m=N(i,s);if(!m)return{status:0,error:"bad_path"};if(!l)return{status:401,error:"not_signed_in"};let u=n.body!==void 0&&a!=="GET"&&a!=="DELETE",d=new AbortController,p=setTimeout(()=>d.abort(),c);try{let f=await fetch(m,{method:a,headers:{Authorization:`Bearer ${l}`,...u?{"Content-Type":"application/json"}:{}},body:u?JSON.stringify(n.body):void 0,signal:d.signal});return{status:f.status,json:await f.json().catch(()=>null)}}catch(f){return{status:0,error:String(f?.message||f)}}finally{clearTimeout(p)}}),e.log("[forsion-extend] cloud account handlers registered"),j(e)?P(e):e.log("[forsion-extend] host has no Connect seam (projectsRoot / transpileForServe / mimeOf / setPreviewHooks); Connect stays with the host"),R(e)?O(e):e.log("[forsion-extend] host has no remote-sync seam (registerRemoteSyncBackend / readCreds); penzor stays with the host"),I(e)?F(e):e.log("[forsion-extend] host has no account seam (accountCommit / accountClear / \u2026); account core stays with the host")}export{re as ACCOUNT_SEAM,A as CONNECT_MAX_FILES,H as CONNECT_MAX_FILE_BYTES,z as CONNECT_MAX_TOTAL_BYTES,G as FORSION_CONNECT_LOCAL_SDK,Y as PENZOR_MAX_FILE,w as cloudJson,D as collectProjectFiles,ne as createAccountCloud,V as createPenzorRemote,$ as expectedPartsOf,C as forsionAccountId,te as forsionRefreshToken,v as forsionWhoami,I as hasAccountHost,j as hasConnectHost,R as hasRemoteSyncHost,W as makePreviewProxy,J as readConnectMeta,F as registerAccount,Ge as registerCloud,P as registerConnect,O as registerPenzor,ee as shouldRefreshToken,Q as tokenAgeMs,Te as tokenRemainingMs,q as writeConnectMeta};
package/manifest.json CHANGED
@@ -2,11 +2,11 @@
2
2
  "id": "forsion-extend",
3
3
  "name": "Forsion 扩展",
4
4
  "nameEn": "Forsion Extend",
5
- "version": "0.2.0",
5
+ "version": "0.3.0",
6
6
  "apiVersion": 1,
7
7
  "minAppVersion": "2.11.4",
8
8
  "main": "dist/main.js",
9
- "description": "Forsion 云端账号相关的桌面功能:个人中心与会员页入口、额度视图与重置卡、后台额度转换、反馈中心提交,插件以当前账号身份调用 Forsion 云端 API 的通用接口,以及 Forsion Connect(Coding Space 项目发布、应用商店上架申请、预览态 AI 代理)。随 Forsion 桌面端内置,自动更新。",
10
- "descriptionEn": "The cloud-account features of Forsion Desktop: account center and membership page shortcuts, quota view and reset cards, background quota conversion, feedback submission, the shared interface plugins use to call the Forsion cloud API as the signed-in account, and Forsion Connect (Coding Space project publishing, app store listing requests, and the preview-mode AI proxy). Built into Forsion Desktop and updated automatically.",
9
+ "description": "Forsion 云端账号相关的桌面功能:账号登录、登出、切换与登录态续期,个人中心与会员页入口、额度视图与重置卡、后台额度转换、反馈中心提交,插件以当前账号身份调用 Forsion 云端 API 的通用接口,Forsion Connect(Coding Space 项目发布、应用商店上架申请、预览态 AI 代理),以及本地库远程同步的 Forsion 云端后端。随 Forsion 桌面端内置,自动更新。",
10
+ "descriptionEn": "The cloud-account features of Forsion Desktop: signing in, signing out, switching accounts and keeping you signed in, account center and membership page shortcuts, quota view and reset cards, background quota conversion, feedback submission, the shared interface plugins use to call the Forsion cloud API as the signed-in account, Forsion Connect (Coding Space project publishing, app store listing requests, and the preview-mode AI proxy), and the Forsion cloud backend for remote sync of the local vault. Built into Forsion Desktop and updated automatically.",
11
11
  "author": "Forsion"
12
12
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forsion/extend",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Forsion Extend: the cloud-account half of Forsion Desktop plus Forsion Connect (Coding Space publishing and the preview-mode AI proxy), shipped as a built-in bundle (signed, updated through npm).",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",