@ai-workbench/connect 0.7.5 → 0.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/client/index.cjs +5 -4
- package/lib/component-download.d.ts +9 -2
- package/lib/component-download.js +7 -4
- package/lib/component-source.d.ts +52 -0
- package/lib/component-source.js +146 -0
- package/lib/components.d.ts +2 -1
- package/lib/components.js +37 -4
- package/lib/defaults.d.ts +1 -0
- package/lib/defaults.js +1 -1
- package/lib/index.d.ts +3 -1
- package/lib/index.js +13 -5
- package/lib/integration.d.ts +2 -1
- package/lib/integration.js +12 -3
- package/lib/profile-settings.js +5 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
安装:在 DSH「插件 → 添加插件」填写 `@ai-workbench/connect@0.7.
|
|
1
|
+
安装:在 DSH「插件 → 添加插件」填写 `@ai-workbench/connect@0.7.7`,或运行 `dsh plugin --profile desktop add @ai-workbench/connect@0.7.7`。
|
|
2
2
|
|
|
3
3
|
# 远程主机连接插件(L)
|
|
4
4
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
D13已通过[PR15](https://github.com/yzsnstotz/ai-workbench-connect/pull/15)合推main7915df5,移除前为909eabb;五项被拒产物及清单引用已撤除。不再生成新的聚合整包或验收ZIP,交付物为插件本身。
|
|
14
14
|
|
|
15
|
-
**插件版本0.7.
|
|
15
|
+
**插件版本0.7.6,API保持0.7.0/native-local-v1。** N0.4.0已接,配套X0.7.1。L导入动作固定使用官方系统目录窗口:自动能力为native时直接消费其pick;SSH等启动上下文提供browse时,明确调用同一官方原生包的公开pickNativeDirectory,不修改全局选择器。失败原因与临时登录路径只在本节技术详情准确投影。js-yaml/yauzl及其传递运行依赖随tgz打包,官方宿主peer由已安装官方程序提供,员工不另装npm依赖。真实中文GUI/授权及leo E门分别留证。旧140测试/46图只属于前任draft.3;本轮证据独立记录于[第2轮证据](docs/evidence/2026-10-21-L-round2-draft4/)。不得拿旧0.6.3 tgz当native-local-v1的输入。
|
|
16
16
|
|
|
17
17
|
开发使用Node24.13.1,依赖/缓存/构建和临时HOME放自有外接APFS。工程检查为`npm test`、`npm run check`、`npm run build`;它们不替代真实产品路径。安装走官方插件管理,本包自身只声明一个官方bundle条目,不使用wrapper或员工启动器。工人GUI按[SELFTEST](docs/SELFTEST.md)的PID和三项获准偏离执行,不操作owner实例。
|
|
18
18
|
|
|
@@ -23,3 +23,5 @@ L0.7.4按owner最新纠正分开供给和授权:Electron组件从自身官方
|
|
|
23
23
|
官方Electron内置Node会把.asar视作虚拟目录;组件缓存/解压专用physical-fs在Electron中使用original-fs核验物理归档,不修改全局process.noAsar。真实官方运行时新建及跨Node复用缓存均有回归。
|
|
24
24
|
|
|
25
25
|
0.7.5仅调整公开 npm 发布元数据和包内容:运行依赖继续随包,合同、源码、测试与证据不进包;无安装脚本。源码与合同见[项目仓库](https://github.com/yzsnstotz/ai-workbench-connect)。发布前可用官方插件页或上述命令安装已核验的tgz绝对路径。本轮只做发布准备,尚未发布到npm。
|
|
26
|
+
|
|
27
|
+
0.7.6:L设置节可显式选择 GitHub 官方源或 npmmirror(Electron 官方文档列出的中国镜像),显示所选来源与理由。未保存选择/未显式配置时默认 npmmirror,依据为编排在leo的可达性实测,不代表自动测速。优先级:本profile设置节已保存选择 → helper.componentDownloadSource初值 → 旧helper.source显式URL → 默认npmmirror;坏设置/保存失败明确报错,不静默切源。预设仍校验原Electron44.0.0固定SHA256。helper.componentObservationIntervalMs默认1000毫秒,只刷新“已N秒无进展”,不触发超时/重试/换源。OAuth与回调仍走N主机出口。
|
package/client/index.cjs
CHANGED
|
@@ -80,9 +80,9 @@ window.__ModuleLoader__.load({id:'@ai-workbench/connect',factory:(require)=>{
|
|
|
80
80
|
const needKey=code=>code==='ENROLLMENT_RESULT_UNKNOWN'?'enrollmentUnknown':code==='ENROLLMENT_REJECTED'?'enrollmentRejected':code==='CONNECTION_UNKNOWN'?'unknown':code==='ENROLLMENT_INVALID'?'importInvalid':code==='DEPENDENCY_REQUIRED'?'notReady':['ENROLLMENT_REQUIRED','NETWORK_KEY_INVALID'].includes(code)?'enrollment':code==='SETTINGS_REQUIRED'?'settings':code==='LOGIN_FAILED'?'loginFailed':code==='POLICY_DENIED'?'denied':code==='LOGIN_PROMPT_INVALID'?'stale':code==='ACTION_FAILED'?'actionFailed':'unavailable';
|
|
81
81
|
function messageKey(s){if(!s||s.phase==='checking')return 'checking';if(s.phase==='blocked'||s.phase==='disposed')return needKey(s.technicalDetails?.code);if(s.phase==='login-required')return 'loginRequired';if(s.phase==='authorizing')return 'authorizing';return Object.keys(s.providerNeeds??{}).length?'loginUnknown':'complete';}
|
|
82
82
|
function createWorkbenchController(call,subscribe){
|
|
83
|
-
let state={value:undefined,error:''},disposed=false,started=false,off,revision=-1,instance,eventVersion=0;let download;const retiredDownloads=new Set(),retired=new Set(),listeners=new Set();
|
|
83
|
+
let state={value:undefined,error:''},disposed=false,started=false,off,revision=-1,instance,eventVersion=0;let download,componentSource;const retiredSources=new Set(),retiredDownloads=new Set(),retired=new Set(),listeners=new Set();
|
|
84
84
|
const publish=next=>{state=next;for(const fn of listeners)fn();};
|
|
85
|
-
const accept=(value,source='event')=>{if(disposed)return;const next=value?.startup?.revision,id=value?.startup?.instanceId;if(typeof id!=='string'||!id||!Number.isSafeInteger(next)||next<0){publish({value:undefined,error:'CONNECTION_UNKNOWN'});return;}if(id!==instance){if(instance&&(source==='http'||retired.has(id)))return;if(instance)retired.add(instance);instance=id;revision=-1;}if(next<revision)return;revision=next;const incoming=value.loginComponent?.download;if(incoming){if(typeof incoming.instanceId!=='string'||!Number.isSafeInteger(incoming.revision)||incoming.revision<0||retiredDownloads.has(incoming.instanceId)||(download&&incoming.instanceId===download.instanceId&&incoming.revision<download.revision)||(download&&incoming.instanceId!==download.instanceId&&source==='http')){value={...value,loginComponent:{...value.loginComponent,download}};}else{if(download&&incoming.instanceId!==download.instanceId)retiredDownloads.add(download.instanceId);download=incoming;}}publish({value,error:value.actionErrorCode||value.enrollment?.need?.code||(value.actionError?'ACTION_FAILED':'')});};
|
|
85
|
+
const accept=(value,source='event')=>{if(disposed)return;const next=value?.startup?.revision,id=value?.startup?.instanceId;if(typeof id!=='string'||!id||!Number.isSafeInteger(next)||next<0){publish({value:undefined,error:'CONNECTION_UNKNOWN'});return;}if(id!==instance){if(instance&&(source==='http'||retired.has(id)))return;if(instance)retired.add(instance);instance=id;revision=-1;}if(next<revision)return;revision=next;const incoming=value.loginComponent?.download;if(incoming){if(typeof incoming.instanceId!=='string'||!Number.isSafeInteger(incoming.revision)||incoming.revision<0||retiredDownloads.has(incoming.instanceId)||(download&&incoming.instanceId===download.instanceId&&incoming.revision<download.revision)||(download&&incoming.instanceId!==download.instanceId&&source==='http')){value={...value,loginComponent:{...value.loginComponent,download}};}else{if(download&&incoming.instanceId!==download.instanceId)retiredDownloads.add(download.instanceId);download=incoming;}}const choice=value.loginComponent?.source;if(choice){if(typeof choice.instanceId!=='string'||!Number.isSafeInteger(choice.revision)||choice.revision<0||retiredSources.has(choice.instanceId)||(componentSource&&choice.instanceId===componentSource.instanceId&&choice.revision<componentSource.revision)||(componentSource&&choice.instanceId!==componentSource.instanceId&&source==='http')){if(componentSource)value={...value,loginComponent:{...value.loginComponent,source:componentSource}};}else{if(componentSource&&choice.instanceId!==componentSource.instanceId)retiredSources.add(componentSource.instanceId);componentSource=choice;}}publish({value,error:value.actionErrorCode||value.enrollment?.need?.code||(value.actionError?'ACTION_FAILED':'')});};
|
|
86
86
|
const failure=()=>{if(!disposed)publish({value:undefined,error:'CONNECTION_UNKNOWN'});};
|
|
87
87
|
return {getSnapshot:()=>state,subscribe:fn=>{listeners.add(fn);return()=>listeners.delete(fn);},async action(path='/check',body){const at=eventVersion;try{const value=await call(path,'POST',body);if(at===eventVersion)accept(value,'http');}catch{if(at===eventVersion)failure();}},start(){if(disposed||started)return;started=true;off=subscribe(value=>{eventVersion++;accept(value);},()=>{eventVersion++;failure();});const at=eventVersion;void call('/status','GET').then(value=>{if(at===eventVersion)accept(value,'http');},()=>{if(at===eventVersion)failure();});},dispose(){disposed=true;off?.();listeners.clear();}};
|
|
88
88
|
}
|
|
@@ -112,8 +112,9 @@ window.__ModuleLoader__.load({id:'@ai-workbench/connect',factory:(require)=>{
|
|
|
112
112
|
const visible=item=>{if(!sent.current.has(item.notice.noticeId)){sent.current.add(item.notice.noticeId);void action('/workspace-notice/'+item.notice.noticeId+'/displayed');}};
|
|
113
113
|
return h('div',null,...items.map(item=>h(WorkspaceNoticeRow,{key:item.notice.noticeId,item,onVisible:visible})));
|
|
114
114
|
}
|
|
115
|
-
function
|
|
116
|
-
function
|
|
115
|
+
function ComponentSource({state,action}){const c=state?.loginComponent?.source;if(!c)return null;const busy=c.changing||c.phase!=='ready'||['downloading','verifying','unpacking'].includes(state?.loginComponent?.download?.phase)||state?.startup?.phase==='authorizing';return h('div',null,h('p',null,c.phase==='ready'?'登录组件下载源:'+c.selectedLabel:c.phase==='loading'?'正在读取登录组件下载源设置…':'登录组件下载源设置不可用;未切换到其他来源。'),h('p',null,c.reason),c.selectionErrorCode?h('p',{role:'alert'},c.selectionErrorCode==='COMPONENT_PIN_MISMATCH'?'下载源未更改:当前组件版本或 SHA256 与预设不符,请查看技术详情。':'下载源未更改,请查看技术详情。'):null,...(c.choices??[]).map(choice=>h('button',{key:choice.id,type:'button',disabled:busy||c.selectedId===choice.id,onClick:()=>void action('/component-source/'+choice.id)},'使用 '+choice.label)),h('p',null,'Electron '+c.version+';两项预设使用同一固定 SHA256,不会因下载失败自动换源。'));}
|
|
116
|
+
function DownloadProgress({state,action}){const d=state?.loginComponent?.download;const [,refresh]=React.useState(0),interval=state?.loginComponent?.observationIntervalMs,observing=d?.phase==='downloading'&&Number.isFinite(d.lastProgressAt);React.useEffect(()=>{if(!observing||!Number.isSafeInteger(interval)||interval<1||interval>2147483647)return;const timer=setInterval(()=>refresh(x=>x+1),interval);return()=>clearInterval(timer);},[observing,interval,d?.attemptId]);if(!d)return null;const seconds=Number.isFinite(d.lastProgressAt)?Math.max(0,Math.floor((Date.now()-d.lastProgressAt)/1000)):null;const text=d.phase==='downloading'?'正在从'+(d.sourceLabel||'所选地址')+'下载登录组件:已下载 '+d.receivedBytes+' 字节'+(d.totalBytes===null?'(总大小未知)':',共 '+d.totalBytes+' 字节')+'。'+(seconds===null?'':'已 '+seconds+' 秒无进展;仅观察,不会自动重试或换源。')+'登录页面仍经远程主机连接。':d.phase==='verifying'?'正在校验登录组件…':d.phase==='unpacking'?'正在解压登录组件…':d.phase==='failed'?'登录组件准备失败,请查看技术详情;未重试或换源。':d.phase==='cancelled'?'登录组件下载已取消。':null;return text?h(React.Fragment,null,h('p',{role:'status'},text),['downloading','verifying','unpacking'].includes(d.phase)&&d.attemptId?h('button',{type:'button',onClick:()=>void action('/component-download/cancel/'+d.attemptId)},'取消组件下载'):null):null;}
|
|
117
|
+
function Panel({workbench,t}){const {state,error,action}=useWorkbench(workbench),message=messageKey(state?.startup),errorKey=error==='ENROLLMENT_REQUIRED'&&state?.enrollment?.phase==='blocked'?'enrollmentKeyMissing':error?needKey(error):undefined,bodyVisible=!['downloading','verifying','unpacking'].includes(state?.loginComponent?.download?.phase)&&(!error||!!state?.startup)&&!(state?.enrollment?.phase==='blocked'&&state.enrollment.need);return h('section',{'aria-label':t('title')},h('h2',null,t('title')),h(ComponentSource,{state,action}),h(DownloadProgress,{state,action}),h(Enrollment,{state,action,t}),bodyVisible?h('p',{role:errorKey===message?'alert':undefined},t(message)):null,errorKey&&(!bodyVisible||errorKey!==message)?h('p',{role:'alert'},t(errorKey)):null,state?h(React.Fragment,null,...['codex','claude'].map(provider=>{const brand=provider==='claude'?'Claude':'Codex',checking=state.startup?.checkingProviders?.includes(provider),unknown=state.startup?.providerNeeds?.[provider]||state.startup?.logins?.[provider]===undefined;return h('div',{key:provider},h('p',null,t('provider',{brand})+': '+t(checking?'statusChecking':unknown?'statusUnknown':state.startup.logins[provider]?'statusSignedIn':'statusSignIn')),state.startup?.logins?.[provider]===false&&!checking&&!unknown&&(!isLoginQuestion(state)||state.startup.provider!==provider)?h('button',{type:'button',disabled:state.startup.phase==='authorizing',onClick:()=>void action('/start-login/'+provider+'/'+state.startup.instanceId+'/'+state.startup.revision)},t('signIn',{brand})):null);}),h(Choices,{state,action,t}),h(WorkspaceNotices,{state,action}),h('p',null,t('workspaceLimit'))):null,h(Details,{state,error,t}),h('button',{type:'button',onClick:()=>void action()},t('recheck')));}
|
|
117
118
|
return {name:'ai-workbench-connect-client',inject:['slots','locale'],createWorkbenchController,Panel,dictionaries,observeNotice,apply(ctx){
|
|
118
119
|
ctx.effect(()=>ctx.locale.register(NS,dictionaries));const t=ctx.locale.bind(NS);
|
|
119
120
|
const workbench=createWorkbenchController(async(path,method,body)=>{const response=await fetch('/ai-workbench-connect'+path,{method,...body===undefined?{}:{headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}});const value=await response.json();if(!response.ok&&!value?.startup)throw Error('connection-unavailable');return value;},(accept,failure)=>{const source=new EventSource('/ai-workbench-connect/events');source.onmessage=event=>{try{accept(JSON.parse(event.data));}catch{failure();}};source.onerror=failure;return()=>source.close();});
|
|
@@ -9,11 +9,18 @@ export interface ComponentDownloadState extends ComponentProgress {
|
|
|
9
9
|
instanceId: string;
|
|
10
10
|
revision: number;
|
|
11
11
|
attemptId: string | null;
|
|
12
|
+
lastProgressAt?: number;
|
|
13
|
+
sourceLabel?: string;
|
|
14
|
+
sourceId?: string;
|
|
12
15
|
}
|
|
13
|
-
export declare function createComponentProgress(): {
|
|
16
|
+
export declare function createComponentProgress(now?: () => number): {
|
|
14
17
|
getState: () => ComponentDownloadState;
|
|
15
18
|
onState(fn: () => void): () => void;
|
|
16
|
-
|
|
19
|
+
cancel(attemptId: string): boolean;
|
|
20
|
+
begin(source?: {
|
|
21
|
+
id: string;
|
|
22
|
+
label: string;
|
|
23
|
+
}, cancel?: () => void): {
|
|
17
24
|
attemptId: `${string}-${string}-${string}-${string}-${string}`;
|
|
18
25
|
update: (progress: ComponentProgress) => void;
|
|
19
26
|
};
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import https from 'node:https';
|
|
2
2
|
import { Readable } from 'node:stream';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
-
export function createComponentProgress() {
|
|
4
|
+
export function createComponentProgress(now = Date.now) {
|
|
5
5
|
let state = { instanceId: randomUUID(), revision: 0, attemptId: null, phase: 'idle', receivedBytes: 0, totalBytes: null };
|
|
6
6
|
const listeners = new Set();
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
let cancelDownload;
|
|
8
|
+
return { getState: () => structuredClone(state), onState(fn) { listeners.add(fn); return () => { listeners.delete(fn); }; }, cancel(attemptId) { if (state.attemptId !== attemptId || !['downloading', 'verifying', 'unpacking'].includes(state.phase) || !cancelDownload)
|
|
9
|
+
return false; const cancel = cancelDownload; cancelDownload = undefined; cancel(); return true; }, begin(source, cancel) { cancelDownload = cancel; const attemptId = randomUUID(); const update = (progress) => { if (state.attemptId !== attemptId)
|
|
10
|
+
return; if (['ready', 'failed', 'cancelled'].includes(progress.phase))
|
|
11
|
+
cancelDownload = undefined; state = { ...progress, lastProgressAt: progress.receivedBytes > state.receivedBytes ? now() : state.lastProgressAt, ...(source ? { sourceLabel: source.label, sourceId: source.id } : {}), instanceId: state.instanceId, revision: state.revision + 1, attemptId }; for (const fn of listeners)
|
|
12
|
+
fn(); }; state = { ...state, attemptId, receivedBytes: 0, lastProgressAt: now() }; update({ phase: 'downloading', receivedBytes: 0, totalBytes: null }); return { attemptId, update }; } };
|
|
10
13
|
}
|
|
11
14
|
export class ComponentDownloadError extends Error {
|
|
12
15
|
constructor(code) { super(code); this.name = 'ComponentDownloadError'; }
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export declare const COMPONENT_SOURCES: Readonly<{
|
|
2
|
+
github: {
|
|
3
|
+
label: string;
|
|
4
|
+
source: "https://github.com/electron/electron/releases/download/v44.0.0/electron-v44.0.0-darwin-arm64.zip";
|
|
5
|
+
};
|
|
6
|
+
npmmirror: {
|
|
7
|
+
label: string;
|
|
8
|
+
source: string;
|
|
9
|
+
};
|
|
10
|
+
}>;
|
|
11
|
+
export type ComponentSourceId = keyof typeof COMPONENT_SOURCES;
|
|
12
|
+
export declare const DEFAULT_SOURCE_REASON = "\u9ED8\u8BA4\u9009\u62E9 npmmirror\uFF1A\u4E2D\u56FD\u955C\u50CF\u53EF\u907F\u5F00\u90E8\u5206\u7F51\u7EDC\u4E0B\u65E0\u6CD5\u8BBF\u95EE\u7684 GitHub \u53D1\u5E03\u6587\u4EF6 CDN\uFF1B\u672A\u81EA\u52A8\u6D4B\u901F\u6216\u5931\u8D25\u6362\u6E90\u3002";
|
|
13
|
+
export declare const preset: (id: unknown) => id is ComponentSourceId;
|
|
14
|
+
interface Options {
|
|
15
|
+
profileDir?: string;
|
|
16
|
+
metadataMaxBytes: number;
|
|
17
|
+
source?: string;
|
|
18
|
+
sha256?: string;
|
|
19
|
+
reason: string;
|
|
20
|
+
busy: () => boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare function createComponentSources(options: Options): {
|
|
23
|
+
ready: Promise<void>;
|
|
24
|
+
getState: () => {
|
|
25
|
+
changing: boolean;
|
|
26
|
+
instanceId: `${string}-${string}-${string}-${string}-${string}`;
|
|
27
|
+
revision: number;
|
|
28
|
+
phase: "loading" | "ready" | "blocked";
|
|
29
|
+
selectedId: string;
|
|
30
|
+
selectedLabel: string;
|
|
31
|
+
reason: string;
|
|
32
|
+
version: string;
|
|
33
|
+
sha256: string | null;
|
|
34
|
+
errorCode: string | null;
|
|
35
|
+
selectionErrorCode: string | null;
|
|
36
|
+
choices: {
|
|
37
|
+
id: string;
|
|
38
|
+
label: string;
|
|
39
|
+
}[];
|
|
40
|
+
};
|
|
41
|
+
onState(fn: () => void): () => void;
|
|
42
|
+
resolve: () => Promise<{
|
|
43
|
+
source: string;
|
|
44
|
+
id: string;
|
|
45
|
+
label: string;
|
|
46
|
+
reason: string;
|
|
47
|
+
}>;
|
|
48
|
+
select: (id: unknown) => Promise<void>;
|
|
49
|
+
dispose(): Promise<void>;
|
|
50
|
+
};
|
|
51
|
+
export type ComponentSources = ReturnType<typeof createComponentSources>;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { join, isAbsolute, resolve } from 'node:path';
|
|
2
|
+
import { lstat, realpath, open, rename, rm } from 'node:fs/promises';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { privateDirectory, readPrivate } from "./private-files.js";
|
|
5
|
+
import { ELECTRON_PIN } from "./defaults.js";
|
|
6
|
+
import { ComponentDownloadError } from "./component-download.js";
|
|
7
|
+
export const COMPONENT_SOURCES = Object.freeze({
|
|
8
|
+
github: { label: 'GitHub 官方源', source: ELECTRON_PIN.source },
|
|
9
|
+
npmmirror: { label: 'npmmirror 镜像(Electron 官方文档列出)', source: 'https://npmmirror.com/mirrors/electron/44.0.0/electron-v44.0.0-darwin-arm64.zip' },
|
|
10
|
+
});
|
|
11
|
+
export const DEFAULT_SOURCE_REASON = '默认选择 npmmirror:中国镜像可避开部分网络下无法访问的 GitHub 发布文件 CDN;未自动测速或失败换源。';
|
|
12
|
+
export const preset = (id) => typeof id === 'string' && Object.hasOwn(COMPONENT_SOURCES, id);
|
|
13
|
+
export function createComponentSources(options) {
|
|
14
|
+
let selected = Object.entries(COMPONENT_SOURCES).find(([, v]) => v.source === options.source)?.[0];
|
|
15
|
+
let state = { instanceId: randomUUID(), revision: 0, phase: 'loading', selectedId: selected ?? 'custom', selectedLabel: selected ? COMPONENT_SOURCES[selected].label : '管理员自定义源', reason: options.reason, version: '44.0.0', sha256: options.sha256 ?? null, errorCode: null, selectionErrorCode: null, choices: Object.entries(COMPONENT_SOURCES).map(([id, v]) => ({ id, label: v.label })) };
|
|
16
|
+
let source = options.source, disposed = false, changing = false;
|
|
17
|
+
const listeners = new Set();
|
|
18
|
+
const pending = new Set();
|
|
19
|
+
function error(code) { throw new ComponentDownloadError(code); }
|
|
20
|
+
const notify = () => { if (!disposed) {
|
|
21
|
+
state = { ...state, revision: state.revision + 1 };
|
|
22
|
+
for (const fn of listeners)
|
|
23
|
+
fn();
|
|
24
|
+
} };
|
|
25
|
+
const blocked = (code) => { state = { ...state, phase: 'blocked', errorCode: code }; notify(); };
|
|
26
|
+
const directory = options.profileDir ? join(options.profileDir, 'ai-workbench-connect') : undefined;
|
|
27
|
+
const file = directory ? join(directory, 'component-source.json') : undefined;
|
|
28
|
+
const checkPin = () => { if (options.sha256 !== ELECTRON_PIN.sha256)
|
|
29
|
+
error('COMPONENT_PIN_MISMATCH'); };
|
|
30
|
+
async function canonical() { if (!options.profileDir || !directory || !isAbsolute(options.profileDir) || await realpath(options.profileDir) !== resolve(options.profileDir))
|
|
31
|
+
error('COMPONENT_SOURCE_PROFILE_REQUIRED'); await privateDirectory(directory); if (await realpath(directory) !== resolve(directory))
|
|
32
|
+
error('COMPONENT_SOURCE_STATE_INVALID'); }
|
|
33
|
+
const ready = (async () => {
|
|
34
|
+
try {
|
|
35
|
+
if (!file)
|
|
36
|
+
error('COMPONENT_SOURCE_PROFILE_REQUIRED');
|
|
37
|
+
if (!source)
|
|
38
|
+
error('COMPONENT_SOURCE_INVALID');
|
|
39
|
+
const initialUrl = new URL(source);
|
|
40
|
+
if (initialUrl.protocol !== 'https:' || initialUrl.username || initialUrl.password)
|
|
41
|
+
error('COMPONENT_SOURCE_INVALID');
|
|
42
|
+
await canonical();
|
|
43
|
+
try {
|
|
44
|
+
const stat = await lstat(file);
|
|
45
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1)
|
|
46
|
+
error('COMPONENT_SOURCE_STATE_INVALID');
|
|
47
|
+
const saved = JSON.parse(await readPrivate(file, options.metadataMaxBytes));
|
|
48
|
+
if (saved.schemaVersion !== 1 || !preset(saved.sourceId))
|
|
49
|
+
error('COMPONENT_SOURCE_STATE_INVALID');
|
|
50
|
+
const savedId = saved.sourceId;
|
|
51
|
+
selected = savedId;
|
|
52
|
+
source = COMPONENT_SOURCES[savedId].source;
|
|
53
|
+
state = { ...state, selectedId: savedId, selectedLabel: COMPONENT_SOURCES[savedId].label, reason: '沿用你在本设置节保存的下载源;未自动切源。' };
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
if (e.code !== 'ENOENT')
|
|
57
|
+
throw e;
|
|
58
|
+
}
|
|
59
|
+
if (!source)
|
|
60
|
+
error('COMPONENT_SOURCE_INVALID');
|
|
61
|
+
const url = new URL(source);
|
|
62
|
+
if (url.protocol !== 'https:' || url.username || url.password)
|
|
63
|
+
error('COMPONENT_SOURCE_INVALID');
|
|
64
|
+
state = { ...state, phase: 'ready' };
|
|
65
|
+
notify();
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
blocked(e instanceof ComponentDownloadError ? e.message : 'COMPONENT_SOURCE_STATE_INVALID');
|
|
69
|
+
}
|
|
70
|
+
})();
|
|
71
|
+
async function resolveSource() { await ready; if (disposed)
|
|
72
|
+
error('COMPONENT_SOURCE_DISPOSED'); if (state.phase === 'blocked')
|
|
73
|
+
error(state.errorCode); if (changing)
|
|
74
|
+
error('COMPONENT_SOURCE_BUSY'); if (selected)
|
|
75
|
+
checkPin(); return { source: source, id: state.selectedId, label: state.selectedLabel, reason: state.reason }; }
|
|
76
|
+
function select(id) {
|
|
77
|
+
const task = (async () => {
|
|
78
|
+
if (!preset(id))
|
|
79
|
+
error('COMPONENT_SOURCE_INVALID');
|
|
80
|
+
if (disposed)
|
|
81
|
+
error('COMPONENT_SOURCE_DISPOSED');
|
|
82
|
+
if (changing || options.busy())
|
|
83
|
+
error('COMPONENT_SOURCE_BUSY');
|
|
84
|
+
changing = true;
|
|
85
|
+
notify();
|
|
86
|
+
let temp;
|
|
87
|
+
try {
|
|
88
|
+
await ready;
|
|
89
|
+
if (disposed)
|
|
90
|
+
error('COMPONENT_SOURCE_DISPOSED');
|
|
91
|
+
if (state.phase === 'blocked')
|
|
92
|
+
error(state.errorCode);
|
|
93
|
+
if (options.busy())
|
|
94
|
+
error('COMPONENT_SOURCE_BUSY');
|
|
95
|
+
checkPin();
|
|
96
|
+
await canonical();
|
|
97
|
+
const data = JSON.stringify({ schemaVersion: 1, sourceId: id });
|
|
98
|
+
if (!Number.isSafeInteger(options.metadataMaxBytes) || Buffer.byteLength(data) > options.metadataMaxBytes)
|
|
99
|
+
error('COMPONENT_SOURCE_STATE_INVALID');
|
|
100
|
+
temp = join(directory, '.component-source-' + randomUUID());
|
|
101
|
+
const handle = await open(temp, 'wx', 0o600);
|
|
102
|
+
try {
|
|
103
|
+
await handle.writeFile(data);
|
|
104
|
+
await handle.sync();
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
await handle.close();
|
|
108
|
+
}
|
|
109
|
+
if (disposed)
|
|
110
|
+
error('COMPONENT_SOURCE_DISPOSED');
|
|
111
|
+
if (options.busy())
|
|
112
|
+
error('COMPONENT_SOURCE_BUSY');
|
|
113
|
+
await rename(temp, file);
|
|
114
|
+
temp = undefined;
|
|
115
|
+
const parent = await open(directory, 'r');
|
|
116
|
+
try {
|
|
117
|
+
await parent.sync();
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
await parent.close();
|
|
121
|
+
}
|
|
122
|
+
selected = id;
|
|
123
|
+
source = COMPONENT_SOURCES[id].source;
|
|
124
|
+
state = { ...state, selectedId: id, selectedLabel: COMPONENT_SOURCES[id].label, reason: '你在本设置节显式选择并保存了此下载源;未自动切源。', errorCode: null, selectionErrorCode: null };
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
if (!(e instanceof ComponentDownloadError)) {
|
|
128
|
+
blocked('COMPONENT_SOURCE_STATE_INVALID');
|
|
129
|
+
error('COMPONENT_SOURCE_STATE_INVALID');
|
|
130
|
+
}
|
|
131
|
+
state = { ...state, selectionErrorCode: e.message };
|
|
132
|
+
throw e;
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
if (temp)
|
|
136
|
+
await rm(temp, { force: true });
|
|
137
|
+
changing = false;
|
|
138
|
+
notify();
|
|
139
|
+
}
|
|
140
|
+
})();
|
|
141
|
+
pending.add(task);
|
|
142
|
+
void task.then(() => pending.delete(task), () => pending.delete(task));
|
|
143
|
+
return task;
|
|
144
|
+
}
|
|
145
|
+
return { ready, getState: () => structuredClone({ ...state, changing }), onState(fn) { listeners.add(fn); return () => { listeners.delete(fn); }; }, resolve: resolveSource, select, async dispose() { disposed = true; listeners.clear(); await ready; await Promise.allSettled([...pending]); } };
|
|
146
|
+
}
|
package/lib/components.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ComponentSources } from './component-source.ts';
|
|
1
2
|
import type { ComponentProgressStore } from './component-download.ts';
|
|
2
3
|
import type { Context } from '@deepseek-ai/cordis';
|
|
3
4
|
export interface ComponentInventory {
|
|
@@ -6,4 +7,4 @@ export interface ComponentInventory {
|
|
|
6
7
|
claude: string;
|
|
7
8
|
codex?: string;
|
|
8
9
|
}
|
|
9
|
-
export declare function registerComponents(ctx: Context, inventory?: ComponentInventory, temporaryDataParent?: string, component?: ComponentProgressStore): void;
|
|
10
|
+
export declare function registerComponents(ctx: Context, inventory?: ComponentInventory, temporaryDataParent?: string, component?: ComponentProgressStore, sources?: ComponentSources, observationIntervalMs?: number): void;
|
package/lib/components.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { ComponentDownloadError } from "./component-download.js";
|
|
2
|
+
import { L_DEFAULTS } from "./defaults.js";
|
|
1
3
|
import { fail } from "./needs.js";
|
|
2
4
|
import { employeeMessage } from "./startup.js";
|
|
3
|
-
export function registerComponents(ctx, inventory, temporaryDataParent, component) {
|
|
5
|
+
export function registerComponents(ctx, inventory, temporaryDataParent, component, sources, observationIntervalMs = L_DEFAULTS.componentObservationIntervalMs) {
|
|
4
6
|
const api = () => ctx.get('aiWorkbenchConnect');
|
|
5
7
|
const getState = () => {
|
|
6
8
|
const network = ctx.get('aiWorkbenchNetwork'), connect = api(), state = connect?.getState(), networkState = network?.getState();
|
|
7
|
-
return { workspaceBindings: connect?.getWorkspaceBindingState?.() ?? null, loginComponent: { temporaryDataParent: temporaryDataParent ?? null, download: component?.getState() ?? null }, startup: connect?.getLoginPromptState?.() ?? null, inventory: inventory ?? null, network: { serviceProvided: !!network, phase: networkState?.phase ?? 'missing', needCode: networkState?.error?.code ?? networkState?.need?.code }, enrollment: connect?.getEnrollmentState?.() ?? null, connection: connect?.getConnectionState?.() ?? null, connect: { serviceProvided: !!connect, phase: state?.phase ?? 'missing', message: state?.need ? employeeMessage(state.need.code) : '连接服务等待就绪' }, codex: inventory ? { installed:
|
|
9
|
+
return { workspaceBindings: connect?.getWorkspaceBindingState?.() ?? null, loginComponent: { temporaryDataParent: temporaryDataParent ?? null, download: component?.getState() ?? null, source: sources?.getState() ?? null, observationIntervalMs }, startup: connect?.getLoginPromptState?.() ?? null, inventory: inventory ?? null, network: { serviceProvided: !!network, phase: networkState?.phase ?? 'missing', needCode: networkState?.error?.code ?? networkState?.need?.code }, enrollment: connect?.getEnrollmentState?.() ?? null, connection: connect?.getConnectionState?.() ?? null, connect: { serviceProvided: !!connect, phase: state?.phase ?? 'missing', message: state?.need ? employeeMessage(state.need.code) : '连接服务等待就绪' }, codex: inventory?.codex ? { installed: true, message: 'Codex 组件已安装' } : { installed: null, message: '组件清单未装配' } };
|
|
8
10
|
};
|
|
9
11
|
ctx.provide('aiWorkbenchComponents', { getState });
|
|
10
12
|
ctx.inject(['webServer', 'connection'], host => {
|
|
@@ -25,9 +27,9 @@ export function registerComponents(ctx, inventory, temporaryDataParent, componen
|
|
|
25
27
|
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
|
26
28
|
res.statusCode = 200;
|
|
27
29
|
let closed = false;
|
|
28
|
-
let unprompt = () => { }, unconnection = () => { }, unenrollment = () => { }, unnative = () => { }, undownload = () => { };
|
|
30
|
+
let unprompt = () => { }, unconnection = () => { }, unenrollment = () => { }, unnative = () => { }, undownload = () => { }, unsource = () => { };
|
|
29
31
|
const close = () => { if (closed)
|
|
30
|
-
return; closed = true; unprompt(); unconnection(); unenrollment(); unnative(); undownload(); streams.delete(close); res.end(); };
|
|
32
|
+
return; closed = true; unprompt(); unconnection(); unenrollment(); unnative(); undownload(); unsource(); streams.delete(close); res.end(); };
|
|
31
33
|
const send = () => { if (closed)
|
|
32
34
|
return; try {
|
|
33
35
|
if (!res.write('data: ' + JSON.stringify(getState()) + '\n\n'))
|
|
@@ -41,11 +43,42 @@ export function registerComponents(ctx, inventory, temporaryDataParent, componen
|
|
|
41
43
|
unenrollment = service.onEnrollmentState?.(send) ?? (() => { });
|
|
42
44
|
unnative = service.onWorkspaceBindingState?.(send) ?? (() => { });
|
|
43
45
|
undownload = component?.onState(send) ?? (() => { });
|
|
46
|
+
unsource = sources?.onState(send) ?? (() => { });
|
|
44
47
|
streams.add(close);
|
|
45
48
|
res.once('close', close);
|
|
46
49
|
send();
|
|
47
50
|
return;
|
|
48
51
|
}
|
|
52
|
+
if (req.method === 'POST' && /^\/ai-workbench-connect\/component-download\/cancel\/[a-zA-Z0-9-]+$/.test(path) && component) {
|
|
53
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
54
|
+
if (Number(req.headers?.['content-length'] ?? 0) > 0 || req.headers?.['transfer-encoding']) {
|
|
55
|
+
res.statusCode = 400;
|
|
56
|
+
res.end(JSON.stringify({ error: 'no-input-accepted' }));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const cancelled = component.cancel(path.split('/').at(-1));
|
|
60
|
+
res.statusCode = cancelled ? 200 : 409;
|
|
61
|
+
res.end(JSON.stringify({ ...getState(), ...(!cancelled ? { actionErrorCode: 'COMPONENT_DOWNLOAD_NOT_ACTIVE', actionError: '该组件下载已经结束或更新,请使用当前状态。' } : {}) }));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (req.method === 'POST' && /^\/ai-workbench-connect\/component-source\/(github|npmmirror)$/.test(path) && sources) {
|
|
65
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
66
|
+
if (Number(req.headers?.['content-length'] ?? 0) > 0 || req.headers?.['transfer-encoding']) {
|
|
67
|
+
res.statusCode = 400;
|
|
68
|
+
res.end(JSON.stringify({ error: 'no-input-accepted' }));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
await sources.select(path.split('/').at(-1));
|
|
73
|
+
res.statusCode = 200;
|
|
74
|
+
res.end(JSON.stringify(getState()));
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
res.statusCode = 409;
|
|
78
|
+
res.end(JSON.stringify({ ...getState(), actionErrorCode: e instanceof ComponentDownloadError ? e.message : 'COMPONENT_SOURCE_STATE_INVALID', actionError: '下载源未更改;请结束当前下载或授权后重选,或检查技术详情。' }));
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
49
82
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
50
83
|
let actionError, actionErrorCode;
|
|
51
84
|
if (req.method === 'POST' && service) {
|
package/lib/defaults.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export declare const L_DEFAULTS: Readonly<{
|
|
|
3
3
|
commandOutputMaxBytes: 1048576;
|
|
4
4
|
remoteKillGraceMs: 1000;
|
|
5
5
|
componentDownloadMaxRedirects: 3;
|
|
6
|
+
componentObservationIntervalMs: 1000;
|
|
6
7
|
maxDownloadBytes: 200000000;
|
|
7
8
|
maxExtractedBytes: 600000000;
|
|
8
9
|
loginOutputMaxBytes: 1048576;
|
package/lib/defaults.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// Owner-approved technical settings (2026-10-20, part two). One source for schema and composition.
|
|
2
|
-
export const L_DEFAULTS = Object.freeze({ enrollmentMetadataMaxBytes: 1048576, commandOutputMaxBytes: 1048576, remoteKillGraceMs: 1000, componentDownloadMaxRedirects: 3, maxDownloadBytes: 200000000, maxExtractedBytes: 600000000, loginOutputMaxBytes: 1048576, ipcMaxBytes: 1048576 });
|
|
2
|
+
export const L_DEFAULTS = Object.freeze({ enrollmentMetadataMaxBytes: 1048576, commandOutputMaxBytes: 1048576, remoteKillGraceMs: 1000, componentDownloadMaxRedirects: 3, componentObservationIntervalMs: 1000, maxDownloadBytes: 200000000, maxExtractedBytes: 600000000, loginOutputMaxBytes: 1048576, ipcMaxBytes: 1048576 });
|
|
3
3
|
export const ELECTRON_PIN = Object.freeze({ source: 'https://github.com/electron/electron/releases/download/v44.0.0/electron-v44.0.0-darwin-arm64.zip', sha256: '076d79742986e1b100b69ebecc691cb07368045e54c9087cef631b8622b76a80', executable: 'Electron.app/Contents/MacOS/Electron' });
|
|
4
4
|
export const NETWORK_DEFAULTS = Object.freeze({ proxyListen: '127.0.0.1:0', logUpload: false, statusRefreshIntervalSeconds: 2, dialTimeoutSeconds: 20, headerTimeoutSeconds: 10, enrollmentObservationSeconds: 30, httpMaxHeaderBytes: 1048576, httpMaxResponseHeaderBytes: 1048576, startupObservationSeconds: 35, shutdownObservationSeconds: 10, controlResponseMaxBytes: 65536, downloadMaxRedirects: 3 });
|
package/lib/index.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface Config {
|
|
|
19
19
|
commandOutputMaxBytes?: number;
|
|
20
20
|
network?: (NetworkSettings & AssemblySettings);
|
|
21
21
|
helper?: {
|
|
22
|
+
componentDownloadSource?: 'github' | 'npmmirror';
|
|
23
|
+
componentObservationIntervalMs?: number;
|
|
22
24
|
componentDownloadMaxRedirects?: number;
|
|
23
25
|
remoteKillGraceMs?: number;
|
|
24
26
|
maxRedirects?: number;
|
|
@@ -33,7 +35,7 @@ export interface Config {
|
|
|
33
35
|
};
|
|
34
36
|
}
|
|
35
37
|
export declare const Config: z<Config>;
|
|
36
|
-
export declare function register(ctx: Context, ports: Ports,
|
|
38
|
+
export declare function register(ctx: Context, ports: Ports, _inventory?: ComponentInventory, extend?: (api: import('./api.ts').ConnectApi) => void): void;
|
|
37
39
|
export declare function apply(ctx: Context, config?: Config): void;
|
|
38
40
|
export { createProfileEnrollment } from './profile-enrollment.ts';
|
|
39
41
|
export { createWorkspaceBindings } from './workspace-bindings.ts';
|
package/lib/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createConnect } from "./connect.js";
|
|
|
3
3
|
import { configuredPorts } from "./integration.js";
|
|
4
4
|
import { attachProfileRuntime } from "./profile-runtime.js";
|
|
5
5
|
import { profileSettings } from "./profile-settings.js";
|
|
6
|
+
import { createComponentSources, DEFAULT_SOURCE_REASON } from "./component-source.js";
|
|
6
7
|
import { createComponentProgress } from "./component-download.js";
|
|
7
8
|
import { L_DEFAULTS, NETWORK_DEFAULTS } from "./defaults.js";
|
|
8
9
|
import { registerComponents } from "./components.js";
|
|
@@ -23,25 +24,32 @@ export const Config = z.object({
|
|
|
23
24
|
commandOutputMaxBytes: positive().default(L_DEFAULTS.commandOutputMaxBytes).description('CLI检查输出内存上限(字节);owner已定1048576,管理员可改'),
|
|
24
25
|
network: z.object({ executable: z.string(), runtimeDir: z.string(), stateDir: z.string(), configFile: z.string(), helperLogFile: z.string(), nodeName: z.string(), startupObservationSeconds: z.number().min(0.001).max(2147483.647).default(NETWORK_DEFAULTS.startupObservationSeconds), shutdownObservationSeconds: z.number().min(0.001).max(2147483.647).default(NETWORK_DEFAULTS.shutdownObservationSeconds), controlResponseMaxBytes: positive().default(NETWORK_DEFAULTS.controlResponseMaxBytes), downloadMaxRedirects: z.number().step(1).min(0).default(NETWORK_DEFAULTS.downloadMaxRedirects), proxyListen: z.string().default(NETWORK_DEFAULTS.proxyListen), logUpload: z.boolean().default(NETWORK_DEFAULTS.logUpload), statusRefreshIntervalSeconds: z.number().min(0.001).max(2147483.647).default(NETWORK_DEFAULTS.statusRefreshIntervalSeconds), dialTimeoutSeconds: z.number().min(1e-9).default(NETWORK_DEFAULTS.dialTimeoutSeconds), headerTimeoutSeconds: z.number().min(0.001).max(2147483.647).default(NETWORK_DEFAULTS.headerTimeoutSeconds), enrollmentObservationSeconds: z.number().min(1e-9).default(NETWORK_DEFAULTS.enrollmentObservationSeconds), httpMaxHeaderBytes: positive().default(NETWORK_DEFAULTS.httpMaxHeaderBytes), httpMaxResponseHeaderBytes: positive().default(NETWORK_DEFAULTS.httpMaxResponseHeaderBytes), controlMaxBytes: positive() }).description('N v1 交接设置;沿owner已定技术值自动装配,管理员可改;路径由当前用户/profile派生,员工不填'),
|
|
25
26
|
helper: z.object({
|
|
27
|
+
componentDownloadSource: z.union([z.const('github'), z.const('npmmirror')]).description('未保存设置节选择时的初始下载源;未设且无旧source时默认npmmirror(编排leo实测可达),可选github;不自动测速/换源,版本与SHA固定'),
|
|
28
|
+
componentObservationIntervalMs: z.number().step(1).min(1).max(2147483647).default(L_DEFAULTS.componentObservationIntervalMs).description('无进展观察刷新间隔毫秒,默认1000;只刷新秒数,不超时/重试/换源'),
|
|
26
29
|
componentDownloadMaxRedirects: z.number().step(1).min(0).default(L_DEFAULTS.componentDownloadMaxRedirects).description('组件直接HTTPS下载的重定向次数上限;默认3,0拒绝重定向;管理员可改,不影响OAuth主机出口'),
|
|
27
30
|
remoteKillGraceMs: z.number().step(1).min(0).max(2147483647).default(L_DEFAULTS.remoteKillGraceMs).description('远端本轮登录进程组结束宽限毫秒;owner已定1000,0立即KILL,管理员可改'),
|
|
28
31
|
maxRedirects: z.number().step(1).min(0).description('兼容旧配置;0.5默认路径改由N插件downloadMaxRedirects控制,此项不再生效'),
|
|
29
|
-
source: z.string().description('
|
|
32
|
+
source: z.string().description('兼容初始管理员显式URL;无已保存的设置节选择且未设componentDownloadSource时生效;无隐式换源'), sha256: z.string().description('darwin-arm64默认已定44.0.0下载SHA256;管理员可改,必须校验'),
|
|
30
33
|
cacheDir: z.string().description('默认从当前profile自动派生ai-workbench-connect/login-component-cache;管理员可改,员工不填路径'), maxDownloadBytes: positive().default(L_DEFAULTS.maxDownloadBytes).description('下载字节上限;owner已定200000000,管理员可改'), maxExtractedBytes: positive().default(L_DEFAULTS.maxExtractedBytes).description('解压字节上限;owner已定600000000,管理员可改'),
|
|
31
34
|
executable: z.string().description('darwin-arm64默认Electron.app/Contents/MacOS/Electron;其他平台无默认,管理员可改'), loginOutputMaxBytes: positive().default(L_DEFAULTS.loginOutputMaxBytes).description('登录进程输出字节上限;owner已定1048576,管理员可改'), ipcMaxBytes: positive().default(L_DEFAULTS.ipcMaxBytes).description('私有IPC字节上限;owner已定1048576,管理员可改'),
|
|
32
35
|
}),
|
|
33
36
|
});
|
|
34
|
-
export function register(ctx, ports,
|
|
35
|
-
|
|
37
|
+
export function register(ctx, ports, _inventory, extend) {
|
|
38
|
+
// 当前独立插件始终检查两品牌;保留旧inventory参数兼容调用,但不以交付清单开关能力。
|
|
39
|
+
const api = createConnect(ports, state => ctx.emit('ai-workbench/connect-state', state), { onConnectionState: state => ctx.emit('ai-workbench/connection-state', state), onPromptState: state => ctx.emit('ai-workbench/login-prompt-state', state), onPromptResult: result => ctx.emit('ai-workbench/login-prompt-result', result) });
|
|
36
40
|
extend?.(api);
|
|
37
41
|
ctx.provide('aiWorkbenchConnect', api);
|
|
38
42
|
ctx.effect(() => { void api.refreshLoginPrompts(); return () => api.dispose(); });
|
|
39
43
|
}
|
|
40
44
|
export function apply(ctx, config = {}) {
|
|
45
|
+
const sourceReason = config.helper?.componentDownloadSource !== undefined ? '管理员通过 helper.componentDownloadSource 指定此下载源。' : config.helper?.source !== undefined ? '管理员通过 helper.source 指定此下载源。' : DEFAULT_SOURCE_REASON;
|
|
41
46
|
config = profileSettings(config, ctx.get('profileContext')?.dir);
|
|
42
|
-
const network = ctx.get('aiWorkbenchNetwork'), component = createComponentProgress()
|
|
47
|
+
const network = ctx.get('aiWorkbenchNetwork'), component = createComponentProgress();
|
|
48
|
+
const sources = createComponentSources({ profileDir: ctx.get('profileContext')?.dir, metadataMaxBytes: config.enrollmentMetadataMaxBytes ?? L_DEFAULTS.enrollmentMetadataMaxBytes, source: config.helper?.source, sha256: config.helper?.sha256, reason: sourceReason, busy: () => ['downloading', 'verifying', 'unpacking'].includes(component.getState().phase) || ctx.get('aiWorkbenchConnect')?.getLoginPromptState().phase === 'authorizing' });
|
|
49
|
+
ctx.effect(() => () => sources.dispose());
|
|
50
|
+
const ports = configuredPorts(config, network, component, sources);
|
|
43
51
|
register(ctx, ports, config.inventory, api => attachProfileRuntime(ctx, api, ports, config, network));
|
|
44
|
-
registerComponents(ctx, config.inventory, config.helper?.cacheDir, component);
|
|
52
|
+
registerComponents(ctx, config.inventory, config.helper?.cacheDir, component, sources, config.helper?.componentObservationIntervalMs);
|
|
45
53
|
}
|
|
46
54
|
export { createProfileEnrollment } from "./profile-enrollment.js";
|
|
47
55
|
export { createWorkspaceBindings } from "./workspace-bindings.js";
|
package/lib/integration.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { ComponentSources } from './component-source.ts';
|
|
1
2
|
import type { Config } from './index.ts';
|
|
2
3
|
import type { Ports } from './ports.ts';
|
|
3
4
|
import { type NetworkProvider } from './provider.ts';
|
|
4
5
|
import { type ComponentProgressStore } from './component-download.ts';
|
|
5
|
-
export declare function configuredPorts(config: Config, provider?: NetworkProvider, component?: ComponentProgressStore): Ports;
|
|
6
|
+
export declare function configuredPorts(config: Config, provider?: NetworkProvider, component?: ComponentProgressStore, sources?: ComponentSources): Ports;
|
package/lib/integration.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ComponentDownloadError } from "./component-download.js";
|
|
1
2
|
import { fileURLToPath } from 'node:url';
|
|
2
3
|
import { readEnrollment } from "./enrollment.js";
|
|
3
4
|
import { adaptNetwork } from "./provider.js";
|
|
@@ -10,7 +11,7 @@ import { ensureHelper } from "./cache.js";
|
|
|
10
11
|
import { launchElectron } from "./electron.js";
|
|
11
12
|
import { need, fail } from "./needs.js";
|
|
12
13
|
import { createComponentProgress, downloadComponent } from "./component-download.js";
|
|
13
|
-
export function configuredPorts(config, provider, component = createComponentProgress()) {
|
|
14
|
+
export function configuredPorts(config, provider, component = createComponentProgress(), sources) {
|
|
14
15
|
const ports = {};
|
|
15
16
|
const network = provider ? adaptNetwork(provider) : undefined;
|
|
16
17
|
if (config.enrollmentPackageDir && config.trustDir)
|
|
@@ -30,8 +31,16 @@ export function configuredPorts(config, provider, component = createComponentPro
|
|
|
30
31
|
const h = config.helper;
|
|
31
32
|
if (!h || !Number.isSafeInteger(h.remoteKillGraceMs) || h.remoteKillGraceMs < 0 || h.remoteKillGraceMs > 2147483647 || ![h.source, h.sha256, h.cacheDir, h.executable].every(x => typeof x === 'string' && x) || !Number.isSafeInteger(h.componentDownloadMaxRedirects) || h.componentDownloadMaxRedirects < 0 || ![h.maxDownloadBytes, h.maxExtractedBytes, h.loginOutputMaxBytes, h.ipcMaxBytes].every(x => Number.isSafeInteger(x) && x > 0))
|
|
32
33
|
return { ok: false, error: need('SETTINGS_REQUIRED') };
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
let selection;
|
|
35
|
+
try {
|
|
36
|
+
selection = await sources?.resolve();
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
return { ok: false, error: { ...need('SETTINGS_REQUIRED'), officialReason: e instanceof ComponentDownloadError ? e.message : 'COMPONENT_SOURCE_STATE_INVALID' } };
|
|
40
|
+
}
|
|
41
|
+
const cancellation = new AbortController(), downloadSignal = AbortSignal.any([signal, cancellation.signal]);
|
|
42
|
+
const attempt = component.begin(selection, () => cancellation.abort());
|
|
43
|
+
const result = await ensureHelper({ ...h, source: selection?.source ?? h.source }, { download: source => downloadComponent(source, downloadSignal, h.componentDownloadMaxRedirects), signal: downloadSignal, onProgress: attempt.update });
|
|
35
44
|
if (!result.ok)
|
|
36
45
|
return result;
|
|
37
46
|
if (signal.aborted)
|
package/lib/profile-settings.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { isAbsolute, join } from 'node:path';
|
|
2
|
+
import { COMPONENT_SOURCES, preset } from "./component-source.js";
|
|
2
3
|
import { L_DEFAULTS, ELECTRON_PIN } from "./defaults.js";
|
|
3
4
|
// Owner approved technical values, 2026-10-20 settings decision, part two.
|
|
4
5
|
// 组件直接来自自己的官方地址;OAuth网页仍通过N主机出口。
|
|
5
6
|
export function profileSettings(config, profileDir, platform = process.platform, arch = process.arch) {
|
|
6
7
|
if (typeof profileDir !== 'string' || !isAbsolute(profileDir))
|
|
7
8
|
return config;
|
|
8
|
-
const runtime = platform === 'darwin' && arch === 'arm64' ? ELECTRON_PIN : {};
|
|
9
|
-
|
|
9
|
+
const runtime = platform === 'darwin' && arch === 'arm64' ? { ...ELECTRON_PIN, source: COMPONENT_SOURCES.npmmirror.source } : {};
|
|
10
|
+
const chosen = config.helper?.componentDownloadSource;
|
|
11
|
+
const selected = chosen === undefined ? {} : { source: preset(chosen) ? COMPONENT_SOURCES[chosen].source : '' };
|
|
12
|
+
return { ...config, commandOutputMaxBytes: config.commandOutputMaxBytes === undefined ? L_DEFAULTS.commandOutputMaxBytes : config.commandOutputMaxBytes, helper: { ...runtime, componentDownloadMaxRedirects: L_DEFAULTS.componentDownloadMaxRedirects, componentObservationIntervalMs: L_DEFAULTS.componentObservationIntervalMs, maxDownloadBytes: L_DEFAULTS.maxDownloadBytes, maxExtractedBytes: L_DEFAULTS.maxExtractedBytes, loginOutputMaxBytes: L_DEFAULTS.loginOutputMaxBytes, ipcMaxBytes: L_DEFAULTS.ipcMaxBytes, remoteKillGraceMs: L_DEFAULTS.remoteKillGraceMs, ...config.helper, ...selected, cacheDir: config.helper?.cacheDir === undefined ? join(profileDir, 'ai-workbench-connect', 'login-component-cache') : config.helper.cacheDir } };
|
|
10
13
|
}
|