@forsion/extend 0.1.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 +18 -0
- package/README.md +5 -5
- package/SIGNATURE +6 -6
- package/dist/desktop.mjs +193 -1
- package/manifest.json +3 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
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
|
+
|
|
13
|
+
## 0.2.0 — 2026-09-27
|
|
14
|
+
|
|
15
|
+
- Forsion Connect(Coding Space 项目发布、应用商店上架申请、预览态 AI 代理)从 Forsion 本体搬入本包,行为与之前一致。预览页里的 `window.forsion`(`/forsion-connect.js`)与 `/__forsion/*` 云端代理也随之由本包提供。
|
|
16
|
+
- 需要较新的 Forsion 桌面端才会启用 Connect;旧桌面端上本包只提供云端账号功能,不影响使用。
|
|
17
|
+
|
|
18
|
+
Forsion Connect (Coding Space project publishing, app store listing requests, and the preview-mode AI proxy) now ships in this package, moved over from Forsion itself with its behavior unchanged. The `window.forsion` SDK served to preview pages (`/forsion-connect.js`) and the `/__forsion/*` cloud proxy come from this package as well.
|
|
19
|
+
Connect is enabled on newer Forsion Desktop builds only; on older desktops this package keeps providing the cloud-account features and nothing else changes.
|
|
20
|
+
|
|
3
21
|
## 0.1.0 — 2026-09-27
|
|
4
22
|
|
|
5
23
|
- 首个版本。Forsion 云端账号相关的桌面功能首次从 Forsion 本体搬出,作为内置插件随桌面端发布、自动更新:个人中心与会员页入口、额度视图与重置卡、后台额度转换,以及反馈中心提交。
|
package/README.md
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
# Forsion Extend
|
|
2
2
|
|
|
3
|
-
Forsion
|
|
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,
|
|
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
|
|
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 IPC channels.
|
|
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
|
|---|---|
|
|
15
15
|
| `manifest.json` | 捆绑包清单:`id` = `forsion-extend`,`main` = `dist/main.js` / bundle manifest |
|
|
16
|
-
| `dist/main.js` | 渲染半身(iife,宿主 `new Function('ctx', code)`
|
|
16
|
+
| `dist/main.js` | 渲染半身(iife,宿主 `new Function('ctx', code)` 装载;目前为空)/ renderer half, currently empty |
|
|
17
17
|
| `dist/desktop.mjs` | 主进程半身(ESM,验签后 `import()`)/ main-process half, imported after signature check |
|
|
18
18
|
| `SIGNATURE` | 包内签名 / in-package signature |
|
|
19
19
|
| `CHANGELOG.md` | 显示在桌面「设置 → 插件」详情页 / shown in Settings → Plugins |
|
package/SIGNATURE
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"alg": "ed25519",
|
|
3
3
|
"files": {
|
|
4
|
-
"CHANGELOG.md": "
|
|
4
|
+
"CHANGELOG.md": "00eab66ddc784373fa030c7f88885dac39e894fec99d1a9a192efba14e272eca",
|
|
5
5
|
"LICENSE": "5594544ee08011ed5785ee26410f1f2ef005f66e9283833e8d435e34c10de1ed",
|
|
6
|
-
"README.md": "
|
|
7
|
-
"dist/desktop.mjs": "
|
|
6
|
+
"README.md": "2e47ea40144d4b49ad23bfb550b7acbb64f7eac2004bebdebfbc02184333314c",
|
|
7
|
+
"dist/desktop.mjs": "652418d8a89ad3a9f7e306ed830b698df9b871147d58a9d5856b62e0f58b33fc",
|
|
8
8
|
"dist/main.js": "8669f048a0e9a5d47dc657306109ab437da6683ad925fbd6a451aef72542a787",
|
|
9
|
-
"manifest.json": "
|
|
10
|
-
"package.json": "
|
|
9
|
+
"manifest.json": "619832ba87b6d239244d51062537601ff84181ee00e6baabba6e3867ebcd9de7",
|
|
10
|
+
"package.json": "27cf696785b46a722f9efd4d7f114aeea02d7cf664ce343193f6ccc3efd9cfdf"
|
|
11
11
|
},
|
|
12
|
-
"sig": "
|
|
12
|
+
"sig": "IrYOnk4++zOfgv9C/bm+Bcq27QnCRwe011X3Wste3V4aeib5Aa6HAsWK9Vh4EtdDlRnLxq5kA5hYajktDfnCCQ=="
|
|
13
13
|
}
|
package/dist/desktop.mjs
CHANGED
|
@@ -1 +1,193 @@
|
|
|
1
|
-
function
|
|
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
|
+
|
|
4
|
+
`)}catch{}n.end()}else a(502,g)}finally{clearTimeout(p)}}}var G=`
|
|
5
|
+
(function () {
|
|
6
|
+
'use strict';
|
|
7
|
+
if (window.forsion) return;
|
|
8
|
+
|
|
9
|
+
function jsonOrThrow(r) {
|
|
10
|
+
if (r.ok) return r.json();
|
|
11
|
+
return r.json().catch(function () { return {}; }).then(function (b) {
|
|
12
|
+
throw new Error((b && b.detail) || ('HTTP ' + r.status));
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// \u5BF9\u8BDD\u6A21\u578B\u4E0D\u5728\u5BA2\u6237\u7AEF\u89E3\u6790:chat \u4E0D\u6307\u540D\u5C31\u4E0D\u4F20,agent \u4E00\u5F8B\u4E0D\u4F20,\u670D\u52A1\u7AEF\u6309 Connect \u7B56\u7565\u73B0\u586B\u3002
|
|
17
|
+
var modelCache = {};
|
|
18
|
+
function listModels(type) {
|
|
19
|
+
var t = type === 'image' ? 'image_gen' : (type === 'asr' ? 'asr' : 'llm');
|
|
20
|
+
if (!modelCache[t]) {
|
|
21
|
+
modelCache[t] = fetch('/__forsion/models?type=' + t).then(jsonOrThrow).then(function (arr) {
|
|
22
|
+
return (Array.isArray(arr) ? arr : []).map(function (m) { return { id: m.id, name: m.name || m.id, type: t }; });
|
|
23
|
+
});
|
|
24
|
+
modelCache[t].catch(function () { modelCache[t] = null; });
|
|
25
|
+
}
|
|
26
|
+
return modelCache[t];
|
|
27
|
+
}
|
|
28
|
+
function pickImageModel(explicit) {
|
|
29
|
+
if (explicit) return Promise.resolve(explicit);
|
|
30
|
+
return listModels('image').then(function (ms) {
|
|
31
|
+
if (ms[0]) return ms[0].id;
|
|
32
|
+
throw new Error('\u5E73\u53F0\u672A\u914D\u7F6E\u53EF\u7528\u7684\u751F\u56FE\u6A21\u578B');
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getUser() {
|
|
37
|
+
return fetch('/__forsion/user').then(function (r) {
|
|
38
|
+
if (r.status === 401 || r.status === 403) return null;
|
|
39
|
+
if (!r.ok) return null;
|
|
40
|
+
return r.json().then(function (u) {
|
|
41
|
+
return u ? {
|
|
42
|
+
username: u.username || u.nickname || '',
|
|
43
|
+
nickname: u.nickname || '',
|
|
44
|
+
avatar: u.avatar || u.avatarUrl || u.avatar_url || null,
|
|
45
|
+
tier: u.membershipTier || u.membership_tier || 'free'
|
|
46
|
+
} : null;
|
|
47
|
+
});
|
|
48
|
+
}).catch(function () { return null; });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function chat(opts) {
|
|
52
|
+
opts = opts || {};
|
|
53
|
+
var messages = Array.isArray(opts.messages) ? opts.messages.slice()
|
|
54
|
+
: opts.prompt ? [{ role: 'user', content: String(opts.prompt) }] : null;
|
|
55
|
+
if (!messages || !messages.length) throw new Error('chat \u9700\u8981 messages \u6216 prompt');
|
|
56
|
+
if (opts.system) messages.unshift({ role: 'system', content: String(opts.system) });
|
|
57
|
+
// \u9875\u9762\u6CA1\u6307\u540D\u6A21\u578B\u5C31\u4E0D\u4F20:\u670D\u52A1\u7AEF\u6309 Connect \u7B56\u7565\u73B0\u67E5\u73B0\u586B\u9ED8\u8BA4(admin \u6539\u9ED8\u8BA4\u5373\u65F6\u751F\u6548,
|
|
58
|
+
// \u4E0D\u53D7\u672C\u9875\u52A0\u8F7D\u65F6\u7F13\u5B58\u7684 config \u5F71\u54CD);\u6307\u540D\u6A21\u578B\u5219\u539F\u6837\u900F\u4F20,\u8D8A\u767D\u540D\u5355\u7531\u670D\u52A1\u7AEF 403\u3002
|
|
59
|
+
var model = opts.model || null;
|
|
60
|
+
var body = { messages: messages, stream: true };
|
|
61
|
+
if (model) body.model_id = model;
|
|
62
|
+
if (typeof opts.temperature === 'number') body.temperature = opts.temperature;
|
|
63
|
+
if (typeof opts.maxTokens === 'number') body.max_tokens = opts.maxTokens;
|
|
64
|
+
var r = await fetch('/__forsion/chat', {
|
|
65
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
|
|
66
|
+
});
|
|
67
|
+
if (!r.ok) {
|
|
68
|
+
var eb = await r.json().catch(function () { return {}; });
|
|
69
|
+
throw new Error((eb && eb.detail) || ('HTTP ' + r.status));
|
|
70
|
+
}
|
|
71
|
+
var reader = r.body.getReader();
|
|
72
|
+
var dec = new TextDecoder();
|
|
73
|
+
var buf = '', text = '', sawDone = false;
|
|
74
|
+
for (;;) {
|
|
75
|
+
var step = await reader.read();
|
|
76
|
+
if (step.done) break;
|
|
77
|
+
buf += dec.decode(step.value, { stream: true });
|
|
78
|
+
var lines = buf.split('\\n');
|
|
79
|
+
buf = lines.pop();
|
|
80
|
+
for (var i = 0; i < lines.length; i++) {
|
|
81
|
+
var line = lines[i].trim();
|
|
82
|
+
if (!line || line.indexOf('data:') !== 0) continue;
|
|
83
|
+
var payload = line.slice(5).trim();
|
|
84
|
+
if (payload === '[DONE]') { sawDone = true; continue; }
|
|
85
|
+
try {
|
|
86
|
+
var j = JSON.parse(payload);
|
|
87
|
+
if (j.error) throw new Error(typeof j.error === 'string' ? j.error : (j.error.message || 'AI \u9519\u8BEF'));
|
|
88
|
+
if (j.model) model = j.model; // \u670D\u52A1\u7AEF\u4EE3\u586B\u9ED8\u8BA4\u65F6,\u4ECE\u6D41\u5757\u91CC\u56DE\u8BFB\u5B9E\u9645\u6A21\u578B
|
|
89
|
+
var d = j.choices && j.choices[0] && (j.choices[0].delta && j.choices[0].delta.content ||
|
|
90
|
+
j.choices[0].message && j.choices[0].message.content) || '';
|
|
91
|
+
if (d) { text += d; if (typeof opts.onDelta === 'function') try { opts.onDelta(d); } catch (e) {} }
|
|
92
|
+
} catch (e) {
|
|
93
|
+
if (e instanceof SyntaxError) continue;
|
|
94
|
+
throw e;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// \u670D\u52A1\u7AEF\u6210\u529F\u5FC5\u53D1 data: [DONE];\u6CA1\u89C1\u5230 = \u6D41\u88AB\u4E2D\u65AD,\u5224\u5931\u8D25\u800C\u975E\u8FD4\u56DE\u622A\u65AD\u7684\u90E8\u5206\u7ED3\u679C\u3002
|
|
99
|
+
if (!sawDone) throw new Error('AI \u54CD\u5E94\u4E2D\u65AD\uFF08\u8FDE\u63A5\u672A\u6B63\u5E38\u7ED3\u675F\uFF09');
|
|
100
|
+
return { text: text, model: model };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// \u2500\u2500 Agent \u901A\u9053(\u5951\u7EA6\u4E0E\u53D1\u5E03\u6001\u58F3\u9875 doAgent \u4E00\u81F4):\u9700\u6C42\u9012\u7ED9\u4E91\u7AEF worker,\u4E0A\u4E0B\u6587/\u5DE5\u5177\u670D\u52A1\u7AEF\u6258\u7BA1 \u2500\u2500
|
|
104
|
+
function genSession() {
|
|
105
|
+
if (window.crypto && crypto.randomUUID) return crypto.randomUUID();
|
|
106
|
+
var a = new Uint8Array(16); crypto.getRandomValues(a);
|
|
107
|
+
return Array.prototype.map.call(a, function (b) { return ('0' + b.toString(16)).slice(-2); }).join('');
|
|
108
|
+
}
|
|
109
|
+
async function agentRun(opts) {
|
|
110
|
+
opts = opts || {};
|
|
111
|
+
var input = opts.input != null ? String(opts.input) : '';
|
|
112
|
+
if (!input.trim()) throw new Error('agent \u9700\u8981 input');
|
|
113
|
+
var session = typeof opts.session === 'string' && opts.session ? opts.session : genSession();
|
|
114
|
+
// \u4E0D\u53D1 model_id:\u6A21\u578B\u5B8C\u5168\u7531\u7F51\u5173\u6309 Connect \u7B56\u7565\u51B3\u5B9A(app_id/agent_config/model \u90FD\u7531\u4EE3\u7406+\u7F51\u5173\u9489\u6B7B)
|
|
115
|
+
var r = await fetch('/__forsion/agent', {
|
|
116
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
117
|
+
body: JSON.stringify({ session_id: session, message: input })
|
|
118
|
+
});
|
|
119
|
+
if (!r.ok) {
|
|
120
|
+
var eb = await r.json().catch(function () { return {}; });
|
|
121
|
+
throw new Error((eb && eb.detail) || ('HTTP ' + r.status));
|
|
122
|
+
}
|
|
123
|
+
var runId = (await r.json()).runId;
|
|
124
|
+
var er = await fetch('/__forsion/agent-events/' + encodeURIComponent(runId));
|
|
125
|
+
if (!er.ok) {
|
|
126
|
+
var eb2 = await er.json().catch(function () { return {}; });
|
|
127
|
+
throw new Error((eb2 && eb2.detail) || ('HTTP ' + er.status));
|
|
128
|
+
}
|
|
129
|
+
// \u5E27\u683C\u5F0F data: {seq,type,payload}:token.delta=\u6587\u672C\u589E\u91CF,done.content=\u6743\u5A01\u7EC8\u7A3F,error=\u5931\u8D25;
|
|
130
|
+
// \u4EE3\u7406\u4E2D\u65AD\u4F1A\u8865 data: {error} \u5E27(\u65E0 type),\u540C\u6837\u5224\u5931\u8D25\u3002
|
|
131
|
+
var reader = er.body.getReader();
|
|
132
|
+
var dec = new TextDecoder();
|
|
133
|
+
var buf = '', streamed = '', finalText = null, errMsg = null;
|
|
134
|
+
for (;;) {
|
|
135
|
+
var step = await reader.read();
|
|
136
|
+
if (step.done) break;
|
|
137
|
+
buf += dec.decode(step.value, { stream: true });
|
|
138
|
+
var lines = buf.split('\\n');
|
|
139
|
+
buf = lines.pop();
|
|
140
|
+
for (var i = 0; i < lines.length; i++) {
|
|
141
|
+
var line = lines[i].trim();
|
|
142
|
+
if (!line || line.indexOf('data:') !== 0) continue;
|
|
143
|
+
var ev;
|
|
144
|
+
try { ev = JSON.parse(line.slice(5)); } catch (e) { continue; }
|
|
145
|
+
if (ev.error && !ev.type) { errMsg = String(ev.error); continue; }
|
|
146
|
+
if (ev.type === 'token' && ev.payload && ev.payload.delta) {
|
|
147
|
+
streamed += ev.payload.delta;
|
|
148
|
+
if (typeof opts.onDelta === 'function') try { opts.onDelta(ev.payload.delta); } catch (e) {}
|
|
149
|
+
} else if (ev.type === 'done') {
|
|
150
|
+
// done.content \u662F\u6743\u5A01\u7EC8\u7A3F(\u54EA\u6015\u4E3A\u7A7A\u4E32\u4E5F\u7167\u4FE1);\u53EA\u6709\u8001 worker \u7F3A\u5B57\u6BB5\u624D\u56DE\u9000\u6D41\u7D2F\u8BA1
|
|
151
|
+
finalText = (ev.payload && typeof ev.payload.content === 'string') ? ev.payload.content : streamed;
|
|
152
|
+
} else if (ev.type === 'error') {
|
|
153
|
+
errMsg = (ev.payload && (ev.payload.detail || ev.payload.error)) || 'AI agent \u6267\u884C\u5931\u8D25';
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (errMsg) throw new Error(errMsg);
|
|
158
|
+
if (finalText == null) throw new Error('AI \u54CD\u5E94\u4E2D\u65AD\uFF08\u8FDE\u63A5\u672A\u6B63\u5E38\u7ED3\u675F\uFF09');
|
|
159
|
+
return { text: finalText, session: session };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function generateImage(opts) {
|
|
163
|
+
opts = opts || {};
|
|
164
|
+
if (!opts.prompt) throw new Error('generateImage \u9700\u8981 prompt');
|
|
165
|
+
var model = await pickImageModel(opts.model);
|
|
166
|
+
var body = {
|
|
167
|
+
model_id: model,
|
|
168
|
+
prompt: String(opts.prompt),
|
|
169
|
+
n: Math.max(1, Math.min(4, opts.n || 1)),
|
|
170
|
+
response_format: opts.responseFormat === 'b64' ? 'b64_json' : 'url'
|
|
171
|
+
};
|
|
172
|
+
if (opts.size) body.size = String(opts.size);
|
|
173
|
+
var j = await fetch('/__forsion/images', {
|
|
174
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
|
|
175
|
+
}).then(jsonOrThrow);
|
|
176
|
+
var data = (j && j.data) || [];
|
|
177
|
+
return { model: model, images: data.map(function (d) { return { url: d.url || null, b64: d.b64_json || null }; }) };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
window.forsion = {
|
|
181
|
+
mode: 'preview',
|
|
182
|
+
user: function () { return getUser(); },
|
|
183
|
+
login: function () {
|
|
184
|
+
return getUser().then(function (u) {
|
|
185
|
+
if (u) return u;
|
|
186
|
+
throw new Error('\u9884\u89C8\u6A21\u5F0F\u4F7F\u7528\u684C\u9762\u7AEF\u767B\u5F55\u6001\uFF1A\u8BF7\u5728 Forsion Desktop \u8BBE\u7F6E\u4E2D\u767B\u5F55 Forsion \u8D26\u53F7');
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
models: function (type) { return listModels(type); },
|
|
190
|
+
ai: { chat: chat, agent: agentRun, generateImage: generateImage }
|
|
191
|
+
};
|
|
192
|
+
})();
|
|
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.
|
|
5
|
+
"version": "0.3.0",
|
|
6
6
|
"apiVersion": 1,
|
|
7
7
|
"minAppVersion": "2.11.4",
|
|
8
8
|
"main": "dist/main.js",
|
|
9
|
-
"description": "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,
|
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forsion/extend",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Forsion Extend: the cloud-account half of Forsion Desktop, shipped as a built-in bundle (signed, updated through npm).",
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
|
7
7
|
"repository": {
|