@josephyulei/summon-foundation 0.1.4 → 0.1.6
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 +9 -5
- package/bin/summon.mjs +62 -33
- package/lib/cli-options.mjs +8 -1
- package/lib/journey-control.mjs +68 -0
- package/lib/maintenance-handoff.mjs +77 -0
- package/lib/manager-step.mjs +60 -0
- package/lib/operation-result.mjs +2 -1
- package/lib/progress-page.mjs +42 -9
- package/lib/resume-handoff.mjs +24 -0
- package/lib/single-page-style.mjs +4 -0
- package/lib/single-page-view.mjs +56 -0
- package/lib/skill-handoff.mjs +53 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
# summon foundation
|
|
2
2
|
|
|
3
|
-
0.1.
|
|
3
|
+
0.1.6 单页试用入口与 Foundation 0.2.12 起的运行引擎共同支持同一页面内的目录选择、程序和 Skill 独立确认;旧发行版本不具备本次单页修订。真实安装、新任务发现和主动告知仍需独立验收。
|
|
4
|
+
|
|
5
|
+
首次 URL 持续显示本次流程;确认仅转交本次受控子进程的当前计划,不新开manager页。--acquire仍只读获取;--resume仅在用户明确要求后恢复未完成Skill,不重放程序安装。
|
|
4
6
|
|
|
5
7
|
Foundation 的薄获取入口,不包含完整运行包,没有 npm 安装生命周期脚本。
|
|
6
8
|
只有明确执行才查询/获取;安装和 Skill 注册继续各自需要用户确认。
|
|
7
9
|
|
|
8
10
|
可用性以 npm registry 的实际版本和公共 GitHub Release 为准;本目录或说明存在不是发布回执。只使用本项目已核验的包名,缺包时不换同名来源。
|
|
9
11
|
|
|
12
|
+
确认 registry 中存在本版本及对应不可变 Release 后,在具备安装权限的新 Codex 对话粘贴:
|
|
13
|
+
|
|
10
14
|
```text
|
|
11
|
-
npx --yes --package @josephyulei/summon-foundation@0.1.
|
|
15
|
+
npx --yes --package @josephyulei/summon-foundation@0.1.6 summon foundation
|
|
12
16
|
```
|
|
13
17
|
|
|
14
|
-
|
|
18
|
+
旧版退出推荐入口;支持边界见[现行支持策略](https://github.com/y15801172513-lang/foundation/blob/main/docs/install-platform-024.md#现行支持策略)。旧不可变版本不修改,不自动迁移或清除旧安装。
|
|
15
19
|
|
|
16
20
|
Codex 需要打开返回的内置浏览器网址,并分段等待同次操作结果。npm 不能自行操控或唤醒 Codex,对话主动告知不是程序退出码保证;新对话接续仍待真人验收。系统浏览器只是明确告知后的备用,不自动打开。
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
联网前建立操作记录,网络失败保留脱敏类别和退出码;--status只读恢复同次结果,不重试或重放。程序健康核验后结果留在本页,工作台由用户另行打开。工具yield不是任务完成,Codex须继续分段观察;HTML不能唤醒已结束任务。
|
|
19
23
|
|
|
20
|
-
显式
|
|
24
|
+
显式 --inspect 只查询发行。默认命令先在页面选择目录与Skill意向,再下载核验;最终精确计划仍需本人确认。--prepare传入的目录只是意向;--acquire只准备更新输入。更新/卸载仅使用支持单页的已安装稳定入口,不回退旧页面。
|
|
21
25
|
|
|
22
26
|
需要 macOS arm64、Node.js 22.9+ 与 npm。无 TTY 不会卡在终端提问;目录选择在页面进行,关闭页面不算确认或取消。
|
|
23
27
|
固定版本准备阶段匿名校验 GitHub 仓库身份、不可变 Release、签名证明、时间戳、源码提交和资产字节,随后使用随包运行时进入现有 Foundation 确认页面。
|
package/bin/summon.mjs
CHANGED
|
@@ -6,19 +6,29 @@ import {inspectRelease,plainPath} from '../lib/acquire.mjs';
|
|
|
6
6
|
|
|
7
7
|
import {acquireInWorker} from '../lib/acquisition-task.mjs';
|
|
8
8
|
import {startProgressPage} from '../lib/progress-page.mjs';
|
|
9
|
+
import {followSelectedSkill} from '../lib/skill-handoff.mjs';
|
|
10
|
+
import {followMaintenance} from '../lib/maintenance-handoff.mjs';
|
|
11
|
+
import {installedClient} from '../lib/manager-step.mjs';
|
|
12
|
+
import {resumeSkillHandoff} from '../lib/resume-handoff.mjs';
|
|
9
13
|
import {parseSummonArgs} from '../lib/cli-options.mjs';
|
|
10
14
|
import {createOperation,saveOperation,readOperation,runtimeObservation} from '../lib/operation-result.mjs';
|
|
15
|
+
import {createJourneyControl} from '../lib/journey-control.mjs';
|
|
11
16
|
|
|
12
17
|
const help=`summon foundation [--version x.y.z] [--destination /absolute/folder]
|
|
13
18
|
summon foundation --inspect [--version x.y.z]
|
|
14
19
|
summon foundation --prepare --version x.y.z --destination /absolute/folder
|
|
15
20
|
summon foundation --acquire --version x.y.z
|
|
16
21
|
summon foundation --status /absolute/acquisition/operation-result.json
|
|
22
|
+
summon foundation --update --root /absolute/installed-folder --version x.y.z
|
|
23
|
+
summon foundation --uninstall --root /absolute/installed-folder
|
|
24
|
+
summon foundation --resume /absolute/acquisition/operation-result.json
|
|
17
25
|
默认命令下载并验证正式发行,进入安装准备;未给目录时在安装页选择。不会静默安装或注册 Skill。
|
|
18
26
|
--inspect 才只读查询发行,不下载运行归档。
|
|
19
27
|
--prepare 下载并核验固定发行,打开 Foundation 本人确认流程;不会代替确认。
|
|
20
28
|
--acquire 仅下载并核验更新材料,返回候选与回执;不启动安装或执行更新。
|
|
21
29
|
--status 只读本次结果记录;中间记录不证明进程仍在运行,不重试或重放确认。
|
|
30
|
+
--update / --uninstall 从已安装稳定入口组织必要步骤;每一步仍需页面独立确认,不自动注册原本没有的 Skill。
|
|
31
|
+
--resume 明确恢复程序已完成后的所选 Skill;核验当前安装与旧记录后仅准备未完成项的新确认,不重复安装。
|
|
22
32
|
仅 macOS arm64。npm 入口需要 Node/npm;已安装 Foundation 使用随包运行时。
|
|
23
33
|
普通终端不能控制 Codex 对话或内置浏览器。无需 npm 的引导见公共仓库安装说明。`;
|
|
24
34
|
function fail(message){throw Error(message);}
|
|
@@ -40,24 +50,8 @@ function stageDirectory(){
|
|
|
40
50
|
return fs.mkdtempSync(path.join(cache,'acquisition.'));
|
|
41
51
|
}
|
|
42
52
|
let operation=null,stage=null,progressPage=null;
|
|
53
|
+
const journeyControl=createJourneyControl(()=>operation,()=>progressPage?.publish());
|
|
43
54
|
function updateOperation(patch){if(!operation)return;Object.assign(operation,patch,{updatedAt:new Date().toISOString()});const file=saveOperation(operation,stage);if(file)operation.resultFile=file;progressPage?.publish();console.log(JSON.stringify({status:'FOUNDATION_INSTALL_OPERATION',...operation,resultFile:file,recordSaved:!!file}));}
|
|
44
|
-
async function openInstalledWorkbench(root,env){
|
|
45
|
-
let child;
|
|
46
|
-
try{
|
|
47
|
-
plainPath(root);const launcher=path.join(root,'bin/foundation-kit');plainPath(launcher);
|
|
48
|
-
const stdout=path.join(stage,'workbench.stdout.log'),stderr=path.join(stage,'workbench.stderr.log');
|
|
49
|
-
const out=fs.openSync(stdout,'wx',0o600);let err;
|
|
50
|
-
try{err=fs.openSync(stderr,'wx',0o600);child=spawn(launcher,['workbench','open','--root',root],{detached:true,stdio:['ignore',out,err],env});}finally{fs.closeSync(out);if(err!==undefined)fs.closeSync(err);}
|
|
51
|
-
await new Promise((resolve,reject)=>{child.once('spawn',resolve);child.once('error',reject);});child.unref();
|
|
52
|
-
for(let n=0;n<100;n++){
|
|
53
|
-
if(child.exitCode!==null)throw Error('工作台进程提前退出');
|
|
54
|
-
let ready;try{ready=JSON.parse(fs.readFileSync(stdout,'utf8'));}catch{}
|
|
55
|
-
if(ready?.url){const url=new URL(ready.url);if(url.protocol!=='http:'||url.hostname!=='127.0.0.1'||url.username||url.password)throw Error('工作台网址无效');const response=await fetch(url,{signal:AbortSignal.timeout(5000),redirect:'error'});if(!response.ok)throw Error('工作台未就绪');return{state:'ready',url:url.href,pid:child.pid,requiredHostAction:'open-returned-loopback-url-in-codex-browser'};}
|
|
56
|
-
await new Promise(r=>setTimeout(r,100));
|
|
57
|
-
}
|
|
58
|
-
throw Error('工作台启动超时');
|
|
59
|
-
}catch{if(child&&child.exitCode===null)child.kill('SIGTERM');return{state:'not-ready',reason:'安装结果保留;工作台未能就绪,可从已安装入口重新打开'};}
|
|
60
|
-
}
|
|
61
55
|
try{
|
|
62
56
|
const args=parseSummonArgs(process.argv.slice(2));
|
|
63
57
|
if(args.help){console.log(help);process.exit(0);}
|
|
@@ -65,10 +59,39 @@ try{
|
|
|
65
59
|
const [major,minor]=process.versions.node.split('.').map(Number);
|
|
66
60
|
if(major<22||(major===22&&minor<9))fail('获取入口需要 Node.js 22.9 或以上;尚未安装,不自动安装依赖');
|
|
67
61
|
if(process.platform!=='darwin'||process.arch!=='arm64')fail('当前仅支持 macOS arm64;尚未安装');
|
|
68
|
-
|
|
69
|
-
if(args.prepare||args.acquire)
|
|
62
|
+
const maintenance=args.update||args.uninstall;
|
|
63
|
+
if(args.prepare||args.acquire||maintenance||args.resume)sourceGuard();
|
|
64
|
+
if(args.prepare||args.acquire||maintenance){operation=createOperation();stage=stageDirectory();updateOperation({});if(!saveOperation(operation,stage))fail('无法在已披露的获取目录保存操作记录;尚未联网或安装');}
|
|
70
65
|
if(args.prepare)console.log('正在准备 Foundation 安装:将查询并固定本次正式版本,说明缓存位置后下载核验。未选目录会在安装页选择;之后仍需本人确认精确计划。请保持当前 Codex 任务等待并打开返回的内置浏览器网址;不会自动改用系统浏览器。');
|
|
71
|
-
if(
|
|
66
|
+
if(args.resume){
|
|
67
|
+
operation=readOperation(args.resume);stage=path.dirname(plainPath(args.resume));
|
|
68
|
+
progressPage=await startProgressPage(()=>operation,{control:journeyControl});
|
|
69
|
+
console.log(JSON.stringify({status:'FOUNDATION_SINGLE_PAGE',url:progressPage.url,operationId:operation.operationId}));
|
|
70
|
+
const env={...process.env};for(const key of ['NODE_OPTIONS','NODE_PATH','NODE_V8_COVERAGE','NODE_REDIRECT_WARNINGS','NODE_COMPILE_CACHE','NODE_COMPILE_CACHE_PORTABLE','NODE_PRESERVE_SYMLINKS'])delete env[key];
|
|
71
|
+
const result=await resumeSkillHandoff({file:args.resume,env,journeyControl,onChange:updateOperation});
|
|
72
|
+
updateOperation(result);process.exitCode=result.state==='completed'?0:1;
|
|
73
|
+
}else if(maintenance){
|
|
74
|
+
plainPath(args.root);
|
|
75
|
+
const env={...process.env};for(const key of ['NODE_OPTIONS','NODE_PATH','NODE_V8_COVERAGE','NODE_REDIRECT_WARNINGS','NODE_COMPILE_CACHE','NODE_COMPILE_CACHE_PORTABLE','NODE_PRESERVE_SYMLINKS'])delete env[key];
|
|
76
|
+
const installed=installedClient(args.root,env),current=installed.call(['manager','inspect','--root',args.root]).installation?.current;
|
|
77
|
+
if(!current?.identity?.installId)fail('当前安装身份无法核实;尚未获取或执行维护');
|
|
78
|
+
const capability=spawnSync(installed.launcher,['--help'],{encoding:'utf8',env,timeout:15000});
|
|
79
|
+
if(capability.status!==0||!capability.stdout.includes('--journey-channel'))fail('当前安装不受新版单页流程支持。请另行确认卸载旧版并重新安装新版;本次不删除、不迁移、不回退旧页面');
|
|
80
|
+
updateOperation({kind:args.update?'update':'uninstall',installationRoot:args.root,currentVersion:current.version,installId:current.identity.installId});
|
|
81
|
+
progressPage=await startProgressPage(()=>operation,{control:journeyControl});updateOperation({progressUrl:progressPage.url});
|
|
82
|
+
console.log(JSON.stringify({status:'FOUNDATION_SINGLE_PAGE',url:progressPage.url,operationId:operation.operationId}));
|
|
83
|
+
let candidate;
|
|
84
|
+
if(args.update){
|
|
85
|
+
const {context,receipt}=await acquireInWorker({version:args.version,stage,operationId:operation.operationId,onContext:context=>updateOperation({version:context.version,sourceCommit:context.sourceCommit}),onProgress:download=>updateOperation({download}),onPhase:phase=>updateOperation({phase})});
|
|
86
|
+
const directory=plainPath(path.dirname(receipt.launcher)),manifest=JSON.parse(fs.readFileSync(plainPath(path.join(directory,'manifest.json'))));
|
|
87
|
+
if(manifest.productVersion!==context.version)fail('获取版本与候选不一致');
|
|
88
|
+
candidate={path:directory,manifestHash:manifest.candidateHash,version:manifest.productVersion,bytes:manifest.totalBytes,runtimeHash:manifest.files.find(f=>f.path===manifest.runtime.path)?.sha256};
|
|
89
|
+
updateOperation({phase:'acquired'});
|
|
90
|
+
}
|
|
91
|
+
updateOperation(await followMaintenance({operation,kind:operation.kind,candidate,env,onChange:updateOperation,journeyControl}));
|
|
92
|
+
process.exitCode=operation.state==='completed'?0:1;
|
|
93
|
+
await progressPage.close();progressPage=null;
|
|
94
|
+
}else if(!args.prepare&&!args.acquire){
|
|
72
95
|
const context=inspectRelease(args.version);
|
|
73
96
|
const guide=`https://github.com/${context.repository.full_name}/blob/${context.documentationCommit}/docs/install-with-codex.md`;
|
|
74
97
|
console.log(JSON.stringify({schemaVersion:'1.0.0',status:'RELEASE_DISCOVERED_NOT_ACQUIRED',product:'Foundation',version:context.version,sourceCommit:context.sourceCommit,documentationCommit:context.documentationCommit,repository:context.repository.full_name,guide,installationPerformed:false,skillRegistered:false,
|
|
@@ -76,35 +99,41 @@ try{
|
|
|
76
99
|
terminalBoundary:'普通终端无法创建或唤醒 Codex 对话;请在有相应权限的 Codex 任务发起安装。',
|
|
77
100
|
acquisition:'尚未下载或验证运行归档。默认入口或 --prepare 执行匿名 GitHub 证明与完整性核验。'},null,2));
|
|
78
101
|
}else{
|
|
79
|
-
|
|
80
|
-
progressPage=await startProgressPage(()=>operation);
|
|
102
|
+
console.log(`本次独占获取目录:${stage}`);
|
|
103
|
+
progressPage=await startProgressPage(()=>operation,args.acquire?{}:{control:journeyControl});
|
|
81
104
|
updateOperation({progressUrl:progressPage.url});
|
|
82
|
-
console.log(JSON.stringify({status:'FOUNDATION_ACQUISITION_PROGRESS_PAGE',url:progressPage.url,operationId:operation.operationId,next:'在 Codex
|
|
105
|
+
console.log(JSON.stringify({status:'FOUNDATION_ACQUISITION_PROGRESS_PAGE',url:progressPage.url,operationId:operation.operationId,next:'在 Codex 内置浏览器打开本次页面,所有所需确认在原页展开;保持任务等待,不自动打开系统浏览器。'}));
|
|
106
|
+
if(!args.acquire){
|
|
107
|
+
const metadata=inspectRelease(args.version);args.version=metadata.version;
|
|
108
|
+
updateOperation({phase:'choosing-intent',version:metadata.version,sourceCommit:metadata.sourceCommit,acquisitionRoot:stage,destinationIntent:args.destination||'',journeyContext:{id:operation.operationId,skillChoice:'undecided'}});
|
|
109
|
+
const choice=await journeyControl.waitChoice();
|
|
110
|
+
updateOperation({phase:'discovering',intentSelected:true,destinationIntent:choice.destination,journeyContext:{id:operation.operationId,skillChoice:choice.skillChoice}});
|
|
111
|
+
}
|
|
83
112
|
const {context,receipt}=await acquireInWorker({version:args.version,stage,operationId:operation.operationId,onContext:context=>updateOperation({version:context.version,sourceCommit:context.sourceCommit}),onProgress:download=>updateOperation({download}),onPhase:phase=>updateOperation({phase})});console.log(JSON.stringify({status:'GITHUB_ACQUISITION_VERIFIED',...receipt,destinationIntent:args.destination}));
|
|
84
113
|
updateOperation({phase:'acquired',version:context.version,sourceCommit:context.sourceCommit});
|
|
85
114
|
if(args.acquire){updateOperation({state:'update-input-ready',terminal:true});console.log(JSON.stringify({status:'UPDATE_INPUT_READY_NOT_APPLIED',candidate:path.dirname(receipt.launcher),receipt:path.join(stage,'acquisition.json'),next:'使用当前健康安装的稳定入口生成独立更新计划;本人确认后才更新。获取回执不是删除授权。'}));await progressPage.close();progressPage=null;process.exit(0);}
|
|
86
115
|
const env={...process.env};for(const key of ['NODE_OPTIONS','NODE_PATH','NODE_V8_COVERAGE','NODE_REDIRECT_WARNINGS','NODE_COMPILE_CACHE','NODE_COMPILE_CACHE_PORTABLE','NODE_PRESERVE_SYMLINKS'])delete env[key];
|
|
87
116
|
const supported=spawnSync(receipt.launcher,['--help'],{encoding:'utf8',env,timeout:15000});
|
|
88
|
-
if(
|
|
89
|
-
if(supported.status!==0||!supported.stdout.includes('--choose-destination'))fail('本次发行尚不支持页面选择目录;未启动安装。请由当前对话取得目录后使用明确的 --destination,或等待新版发行,不静默采用默认位置');
|
|
90
|
-
}
|
|
117
|
+
if(supported.status!==0||!supported.stdout.includes('--journey-channel'))fail('该发行不支持新版单页流程;未安装,不回退旧页面。请使用支持单页的新版发行');
|
|
91
118
|
updateOperation({phase:'starting-confirmation-service',installationWrites:'unknown'});
|
|
92
|
-
const child=spawn(receipt.launcher,['install'
|
|
119
|
+
const child=spawn(receipt.launcher,['install','--journey-id',operation.operationId,'--choose-destination','--journey-channel','--browser','codex'],{stdio:['inherit','pipe','inherit','ipc'],env});
|
|
120
|
+
journeyControl.attach(child);
|
|
93
121
|
child.stdout.setEncoding('utf8');
|
|
94
122
|
let pending='',json='';
|
|
95
123
|
child.stdout.on('data',bytes=>{process.stdout.write(bytes);pending+=bytes.toString();if(pending.length>1_000_000){pending='';json='';return;}let end;while((end=pending.indexOf('\n'))>=0){const line=pending.slice(0,end);pending=pending.slice(end+1);if(!json&&!line.trim().startsWith('{'))continue;json+=line+'\n';if(json.length>1_000_000){json='';continue;}let e;try{e=JSON.parse(json)}catch{continue}json='';
|
|
96
124
|
const observed=runtimeObservation(e);if(observed)updateOperation(observed);
|
|
125
|
+
journeyControl.observe(child,e).catch(error=>{updateOperation({state:'verification-required',terminal:true,next:error.message});child.kill('SIGTERM');});
|
|
97
126
|
}});
|
|
98
127
|
const interrupt=signal=>{child.kill(signal);};
|
|
99
128
|
const int=()=>interrupt('SIGINT'),term=()=>interrupt('SIGTERM');
|
|
100
129
|
process.once('SIGINT',int);process.once('SIGTERM',term);
|
|
101
130
|
const ended=await new Promise((resolve,reject)=>{child.once('error',reject);child.once('close',(code,signal)=>resolve({code,signal}));}).finally(()=>{process.off('SIGINT',int);process.off('SIGTERM',term);});
|
|
102
131
|
if(ended.signal||ended.code===null)fail(`确认服务未正常返回;结果待核实,保留 ${stage},不要自动重试安装`);
|
|
103
|
-
if(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
updateOperation({phase:'finished',workbench,next:workbench.state==='ready'?'在 Codex 内置浏览器打开已就绪工作台;Skill 与项目仍未接入,按用户偏好另行询问':'安装已完成,但工作台未就绪;保留安装结果并只读检查'});
|
|
132
|
+
if(operation.state==='awaiting-skill'){
|
|
133
|
+
try{updateOperation(await followSelectedSkill({operation,env,onChange:updateOperation,journeyControl}));}
|
|
134
|
+
catch(error){updateOperation({state:'partial',phase:'finished',terminal:true,skillRegistered:false,handoffFailure:{code:error.code||'SKILL_HANDOFF_REQUIRES_VERIFICATION',command:error.command||null,exitCode:error.exitCode??null},next:'程序已安装,Skill 接续未完成;保留当前计划与结果,仅核实未完成步骤,不重装程序或重放确认。'});}
|
|
107
135
|
}
|
|
108
|
-
|
|
136
|
+
if(!operation.terminal)updateOperation({state:'verification-required',terminal:true,childExitCode:ended.code,next:'服务退出不等于成功或取消;只读核验原 session 与 installed 状态,不自动重试'});
|
|
137
|
+
process.exitCode=operation.state==='partial'?1:ended.code;
|
|
109
138
|
}
|
|
110
|
-
}catch(e){updateOperation({state:operation&&operation.installationWrites!=='none'?'verification-required':'failed',terminal:true,errorCode:['ACQUISITION_TRANSPORT_FAILED','ACQUISITION_VALIDATION_FAILED'].includes(e.code)?e.code:'INSTALL_ENTRY_FAILED',diagnostic:e.diagnostic||null,next:'
|
|
139
|
+
}catch(e){const message=String(e.message).replace(/https?:\/\/\S+/g,'[已脱敏网址]').replace(/(?:github_pat_|ghp_|npm_)[A-Za-z0-9_]+/g,'[已脱敏凭据]');updateOperation({state:e.code==='ENTRY_CHOICE_CANCELLED'?'cancelled-no-install':e.code==='ENTRY_CHOICE_EXPIRED'?'expired-no-install':operation?.programState==='completed'?'partial':operation&&operation.installationWrites!=='none'?'verification-required':'failed',terminal:true,...(operation?.programState==='completed'?{failedPhase:operation.phase,phase:'finished'}:{}),errorCode:['ACQUISITION_TRANSPORT_FAILED','ACQUISITION_VALIDATION_FAILED','ENTRY_CHOICE_CANCELLED','ENTRY_CHOICE_EXPIRED'].includes(e.code)?e.code:'INSTALL_ENTRY_FAILED',diagnostic:e.diagnostic||null,next:message+'。先核验旧工具进程已结束及同次结果;保留材料,不自动重试或重放安装确认'});console.error(`错误:${message}`);process.exitCode=1;}finally{if(progressPage)await progressPage.close();}
|
package/lib/cli-options.mjs
CHANGED
|
@@ -8,11 +8,18 @@ export function parseSummonArgs(args){
|
|
|
8
8
|
if(flag==='--prepare')result.prepare=true;
|
|
9
9
|
else if(flag==='--acquire')result.acquire=true;
|
|
10
10
|
else if(flag==='--inspect')result.inspect=true;
|
|
11
|
-
else if(flag==='--
|
|
11
|
+
else if(flag==='--update'||flag==='--uninstall')result[flag.slice(2)]=true;
|
|
12
|
+
else if(['--version','--destination','--status','--root','--resume'].includes(flag)){if(!args.length||args[0].startsWith('--'))fail('参数缺值');result[flag.slice(2)]=args.shift();}
|
|
12
13
|
else fail('未知参数;请查看 --help');
|
|
13
14
|
}
|
|
14
15
|
if((result.prepare||result.acquire||result.destination)&&result.inspect||result.prepare&&result.acquire||result.destination&&result.acquire)fail('参数组合不支持');
|
|
15
16
|
if(seen.has('--status')){if(!result.status||seen.size!==1)fail('结果查询需要非空路径且不能与其他操作组合');return result;}
|
|
17
|
+
if(seen.has('--resume')){if(!result.resume||seen.size!==1)fail('恢复需要单一结果路径,不接受新目标或额外操作');return result;}
|
|
18
|
+
if(result.update||result.uninstall){
|
|
19
|
+
if(result.update&&result.uninstall||!result.root||result.inspect||result.acquire||result.prepare||result.destination||result.uninstall&&result.version||result.update&&!result.version)fail('维护需要单一动作、明确 --root;更新还需要固定 --version');
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
if(result.root)fail('--root 仅用于明确的更新或卸载');
|
|
16
23
|
if(!result.inspect&&!result.acquire)result.prepare=true;
|
|
17
24
|
if(result.acquire&&!result.version)fail('获取更新材料前须固定版本');
|
|
18
25
|
return result;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
// Only the exact child created by this invocation may supply an active view.
|
|
4
|
+
// Browser input cannot name a URL, route, transport token or future plan.
|
|
5
|
+
export function createJourneyControl(readRecord, changed) {
|
|
6
|
+
const children=new Map();let active=null,origin=null,choice=null,resolveChoice,rejectChoice,choiceTimer,viewRevision=0;
|
|
7
|
+
const headers=owner=>({origin,'content-type':'application/json','x-foundation-journey':owner.token,'x-foundation-operation':readRecord().operationId});
|
|
8
|
+
const request=async(owner,route,body)=>{
|
|
9
|
+
if(owner.child.exitCode!==null||owner.child.signalCode)throw Error('确认服务已退出,结果待核实');
|
|
10
|
+
const response=await fetch(owner.url+route,{method:body?'POST':'GET',headers:headers(owner),...(body?{body:JSON.stringify(body)}:{}),redirect:'error',signal:AbortSignal.timeout(120000)});
|
|
11
|
+
return {status:response.status,body:await response.json()};
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
setOrigin(value){if(origin)throw Error('单页来源不能重绑定');origin=value;},
|
|
15
|
+
attach(child){
|
|
16
|
+
const owner={child,token:crypto.randomBytes(32).toString('hex'),url:null};children.set(child,owner);
|
|
17
|
+
child.on('message',message=>{if(message?.type==='foundation-journey-ready-v1')child.send({type:'foundation-journey-bind-v1',origin,operationId:readRecord().operationId,token:owner.token});});
|
|
18
|
+
child.once('close',()=>{children.delete(child);if(active?.owner===owner){active=null;changed();}});
|
|
19
|
+
},
|
|
20
|
+
async observe(child,event){
|
|
21
|
+
if(event.status==='FOUNDATION_OPERATION_STATE'&&active?.view?.session?.sessionId===event.sessionId){active.view.session.state=event.state;changed();return;}
|
|
22
|
+
if(!['AWAITING_FOUNDATION_DIRECTORY_SELECTION','AWAITING_FOUNDATION_UI_CONFIRMATION'].includes(event.status))return;
|
|
23
|
+
const owner=children.get(child);if(!owner)throw Error('未绑定的确认进程');
|
|
24
|
+
const revision=++viewRevision;
|
|
25
|
+
const url=new URL(event.url);if(url.protocol!=='http:'||url.hostname!=='127.0.0.1'||url.pathname!=='/'||url.search||url.hash||url.username||url.password)throw Error('确认服务地址无效');
|
|
26
|
+
owner.url=url.href;
|
|
27
|
+
const selection=event.status==='AWAITING_FOUNDATION_DIRECTORY_SELECTION';
|
|
28
|
+
const result=await request(owner,selection?'__foundation/install/view':'__foundation/manager/view');
|
|
29
|
+
if(children.get(child)!==owner||revision!==viewRevision)return;
|
|
30
|
+
if(result.status!==200||result.body.operationId!==readRecord().operationId)throw Error('当前引擎不支持单页确认;旧版本需要另行重新安装,不降级到旧页面');
|
|
31
|
+
if(!selection&&result.body.session.sessionId!==event.sessionId)throw Error('确认会话不匹配');
|
|
32
|
+
active={owner,type:selection?'selection':'manager',view:result.body};changed();
|
|
33
|
+
if(selection&&choice){
|
|
34
|
+
const selected=await request(owner,'__foundation/install/selection',{nonce:result.body.nonce,action:'select',destination:choice.destination,skillChoice:choice.skillChoice});
|
|
35
|
+
if(selected.status!==200){active.error=selected.body.message;choice=null;changed();}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
view(){return active?{type:active.type,...active.view,error:active.error||null}:null;},
|
|
39
|
+
waitChoice({timeoutMs=600000}={}){return new Promise((resolve,reject)=>{
|
|
40
|
+
if(resolveChoice)throw Error('目录选择已在等待');
|
|
41
|
+
resolveChoice=resolve;rejectChoice=reject;
|
|
42
|
+
choiceTimer=setTimeout(()=>{resolveChoice=null;rejectChoice=null;reject(Object.assign(Error('目录选择已到期,尚未下载或安装;重新发起需要新的选择'),{code:'ENTRY_CHOICE_EXPIRED'}));changed();},timeoutMs);
|
|
43
|
+
changed();
|
|
44
|
+
});},
|
|
45
|
+
async submit(body){
|
|
46
|
+
if(body.operationId!==readRecord().operationId)throw Error('流程身份不匹配');
|
|
47
|
+
if(body.action==='cancel-intent'&&rejectChoice){
|
|
48
|
+
clearTimeout(choiceTimer);const reject=rejectChoice;resolveChoice=null;rejectChoice=null;
|
|
49
|
+
reject(Object.assign(Error('已取消目录选择,尚未下载或安装'),{code:'ENTRY_CHOICE_CANCELLED'}));changed();return {status:200,body:{state:'cancelled-no-install'}};
|
|
50
|
+
}
|
|
51
|
+
if(body.action==='choose'&&resolveChoice){
|
|
52
|
+
if(typeof body.destination!=='string'||!body.destination.trim()||!['selected','skipped'].includes(body.skillChoice))throw Error('请选择完整目录和 Skill 意向');
|
|
53
|
+
choice={destination:body.destination,skillChoice:body.skillChoice};const resolve=resolveChoice;clearTimeout(choiceTimer);resolveChoice=null;rejectChoice=null;resolve(choice);changed();return {status:200,body:{state:'intent-selected'}};
|
|
54
|
+
}
|
|
55
|
+
if(!active)throw Error('当前没有可确认的计划;请等待或查询结果');
|
|
56
|
+
const a=active;
|
|
57
|
+
if(a.type==='selection'){
|
|
58
|
+
if(!['select','cancel'].includes(body.action))throw Error('目录操作无效');
|
|
59
|
+
const result=await request(a.owner,'__foundation/install/selection',{nonce:a.view.nonce,action:body.action,destination:body.destination,skillChoice:body.skillChoice});
|
|
60
|
+
if(result.status!==200)a.error=result.body.message;changed();return result;
|
|
61
|
+
}
|
|
62
|
+
const s=a.view.session;
|
|
63
|
+
if(body.sessionId!==s.sessionId||body.planHash!==s.planHash||body.managerNonce!==a.view.managerNonce||!['cancel-no-change',a.view.action].includes(body.action))throw Error('当前计划已变化或请求无效');
|
|
64
|
+
const result=await request(a.owner,'__foundation/manager/confirm',{operationId:body.operationId,sessionId:s.sessionId,planHash:s.planHash,managerNonce:body.managerNonce,action:body.action});
|
|
65
|
+
if(active===a&&result.body.state)a.view.session={...s,...result.body};changed();return result;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {isDeepStrictEqual} from 'node:util';
|
|
4
|
+
import {plainPath} from './acquire.mjs';
|
|
5
|
+
import {installedClient,observePlan} from './manager-step.mjs';
|
|
6
|
+
import {followSelectedSkill} from './skill-handoff.mjs';
|
|
7
|
+
|
|
8
|
+
// The running owned engine emits the result body; disk additionally stores its
|
|
9
|
+
// integrity envelope. Bind every body field to that live result, not to the
|
|
10
|
+
// receipt's self-asserted signature (the uninstall may remove its local key).
|
|
11
|
+
export function matchesUninstallResult(receipt,payload,installId) {
|
|
12
|
+
const {integrity,...body}=receipt||{};
|
|
13
|
+
return body.state==='uninstalled'&&body.installId===installId&&
|
|
14
|
+
typeof body.operationId==='string'&&body.operationId===payload?.operationId&&
|
|
15
|
+
integrity?.algorithm==='hmac-sha256'&&/^[a-f0-9]{64}$/.test(integrity.hash||'')&&
|
|
16
|
+
isDeepStrictEqual(body,payload);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Only coordinates the installed exact-plan API. Every mutation waits for its
|
|
20
|
+
// own manager confirmation, executed by the current owned stable launcher.
|
|
21
|
+
export async function followMaintenance({operation,kind,candidate,env,onChange,journeyControl=null}) {
|
|
22
|
+
if(!['update','uninstall'].includes(kind))throw Error('维护类型无效');
|
|
23
|
+
const client=installedClient(operation.installationRoot,env,journeyControl),{root,call}=client;
|
|
24
|
+
const before=call(['manager','inspect','--root',root]),current=before.installation?.current;
|
|
25
|
+
if(!current?.identity?.installId||before.bridge?.currentVersion!==current.version)throw Error('当前安装无法核实');
|
|
26
|
+
if(kind==='update'&&(!candidate||candidate.version===current.version))throw Error('目标版本无效或已经安装;不重复更新或解除 Skill');
|
|
27
|
+
const manifest=plainPath(path.join(root,current.appPath,'artifacts/skills/ai-product-foundation-kit/capability.json'));
|
|
28
|
+
if(!manifest.startsWith(root+path.sep))throw Error('当前材料路径越界');
|
|
29
|
+
// File presence only requests an ownership-checked plan; the running engine
|
|
30
|
+
// validates its receipt and files. It does not authorize removal.
|
|
31
|
+
const registration=plainPath(path.join(root,'state/codex-skill-registration.json'));
|
|
32
|
+
const hadSkill=fs.existsSync(registration);
|
|
33
|
+
let previousPlanRef=null;
|
|
34
|
+
const steps={};
|
|
35
|
+
onChange({kind,currentVersion:current.version,installId:current.identity.installId,previousCandidateHash:current.candidateHash,journeyContext:{id:operation.operationId,skillChoice:kind==='update'&&hadSkill?'selected':'skipped'},next:hadSkill?(kind==='update'?'先确认程序更新,再确认刷新完整归属的 Codex 文件;未知或修改文件保留。':'先核对并确认解除已关联的 Codex 副本,再继续程序卸载;未知或修改文件保留。'):'核对程序操作;本次不会新增 Codex Skill。'});
|
|
36
|
+
const run=async(key,op,parameters)=>{
|
|
37
|
+
const now=call(['manager','inspect','--root',root]).installation?.current;
|
|
38
|
+
if(now?.identity?.installId!==current.identity.installId||now.candidateHash!==current.candidateHash)throw Error('维护期间安装身份变化,停止接续');
|
|
39
|
+
const requested=call(['manager','request-plan','--operation',op,'--parameters-json',JSON.stringify(parameters),...(previousPlanRef?['--previous-plan-ref',previousPlanRef]:[])]);
|
|
40
|
+
steps[key]={state:'pending',evidence:{planRef:requested.planRef}};
|
|
41
|
+
onChange({phase:key==='program'?'awaiting-confirmation':'skill-confirmation',currentPlanRef:requested.planRef,maintenanceSteps:{...steps},terminal:false});
|
|
42
|
+
const event=await observePlan(client,requested,observation=>onChange({phase:observation.state==='pending'?(key==='program'?'awaiting-confirmation':'skill-confirmation'):(key==='program'?'executing':'skill-executing'),...(observation.state!=='pending'?{installationWrites:'possible'}:{}),...(observation.temporaryManager?{temporaryManager:observation.temporaryManager}:{}),...(observation.confirmationUrl?{confirmationUrl:observation.confirmationUrl}:{}),maintenanceSteps:{...steps,[key]:{state:observation.state,evidence:{planRef:requested.planRef,sessionId:observation.sessionId}}}}));
|
|
43
|
+
// Uninstall removes the executable, but its result and external receipt are
|
|
44
|
+
// emitted by the already-running manager. Other steps are reread normally.
|
|
45
|
+
const result=kind==='uninstall'&&key==='program'?event:call(['manager','status','--plan-ref',requested.planRef]);
|
|
46
|
+
if(result.sessionId!==event.sessionId||result.state!==event.state)throw Error('维护结果与会话不一致');
|
|
47
|
+
steps[key]={state:result.state,evidence:{planRef:requested.planRef,sessionId:result.sessionId,recordLocation:result.recordLocation},result:result.result};
|
|
48
|
+
previousPlanRef=requested.planRef;
|
|
49
|
+
onChange({maintenanceSteps:{...steps},previousPlanRef});
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
if(hadSkill&&kind==='uninstall'){
|
|
53
|
+
const removed=await run('remove','capability-uninstall',{installationRoot:root,manifestFile:manifest});
|
|
54
|
+
if(removed.state!=='completed')return {state:'partial',terminal:true,phase:'finished',programState:'not-started',next:'Codex 解除未完成,程序未更新或卸载;保留原入口和文件。'};
|
|
55
|
+
if(fs.existsSync(registration))throw Error('解除后归属记录仍存在,不继续程序操作');
|
|
56
|
+
}
|
|
57
|
+
const parameters={targetRoot:root,currentVersion:current.version,targetVersion:kind==='update'?candidate?.version:current.version};
|
|
58
|
+
if(kind==='uninstall')parameters.mode='full'; // Owned program/runtime/state only; user data remains protected by the exact plan.
|
|
59
|
+
if(kind==='update'){
|
|
60
|
+
if(!candidate||candidate.version===current.version)throw Error('目标版本无效或已经安装;不重复更新');
|
|
61
|
+
parameters.candidate=candidate;
|
|
62
|
+
if(before.supportedLifecycleOptions?.update?.includes('cleanupAcquisition'))parameters.cleanupAcquisition=true;
|
|
63
|
+
}
|
|
64
|
+
const result=await run('program',kind,parameters);
|
|
65
|
+
const payload=result.result?.lifecycleResult||result.result||{};
|
|
66
|
+
onChange({programState:result.state,sessionId:result.sessionId,runtimeHealth:payload.stableLauncherHealth||'unknown',programResult:payload,phase:'runtime-ended'});
|
|
67
|
+
if(result.state!=='completed')return {state:'partial',terminal:true,phase:'finished',next:'程序操作未完成;保留结果和恢复记录,不自动补做。先核验程序与 Codex 各自状态。'};
|
|
68
|
+
if(kind==='uninstall'){
|
|
69
|
+
const file=plainPath(path.join(root,'uninstall-result.json')),receipt=JSON.parse(fs.readFileSync(file));
|
|
70
|
+
if(!matchesUninstallResult(receipt,payload,current.identity.installId))throw Error('卸载独立回执与本次已安装引擎结果不一致');
|
|
71
|
+
return {state:'completed',terminal:true,phase:'finished',skillRegistered:false,uninstallReceipt:file,next:'卸载完成;保留项目资料、用户修改文件及卸载回执。缓存清理说明见独立回执,不自动清理其他缓存。'};
|
|
72
|
+
}
|
|
73
|
+
const after=call(['manager','inspect','--root',root]).installation?.current;
|
|
74
|
+
if(after?.identity?.installId!==current.identity.installId||after.version!==candidate.version||after.candidateHash!==candidate.manifestHash||payload.stableLauncherHealth!=='passed')throw Error('更新后的稳定入口身份或健康待核实');
|
|
75
|
+
if(hadSkill)return followSelectedSkill({operation:{...operation,kind:'update',installationRoot:root,programState:'completed',sessionId:null,previousPlanRef,journeyContext:{id:operation.operationId,skillChoice:'selected'}},env,onChange,journeyControl});
|
|
76
|
+
return {state:'completed',terminal:true,phase:'finished',skillRegistered:false,next:'更新及稳定入口健康核验完成;未新增 Skill,项目与用户数据按计划保留。'};
|
|
77
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {spawn,spawnSync} from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {plainPath} from './acquire.mjs';
|
|
4
|
+
|
|
5
|
+
// A client of the existing installed manager, never a confirmation authority.
|
|
6
|
+
export function installedClient(root,env,journeyControl=null) {
|
|
7
|
+
root=plainPath(root);
|
|
8
|
+
const launcher=plainPath(path.join(root,'bin/foundation-kit'));
|
|
9
|
+
const call=args=>{
|
|
10
|
+
const r=spawnSync(launcher,args,{env,encoding:'utf8',timeout:30000,maxBuffer:4*1024*1024});
|
|
11
|
+
if(r.status!==0){const e=Error('已安装入口检查失败;保留已核实结果,不自动重放');e.code='INSTALLED_COMMAND_FAILED';e.command=args.slice(0,2);e.exitCode=r.status;throw e;}
|
|
12
|
+
return JSON.parse(r.stdout);
|
|
13
|
+
};
|
|
14
|
+
return {root,launcher,call,env,journeyControl};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function observePlan(client,requested,onChange) {
|
|
18
|
+
if(!/^foundation-plan-[a-f0-9]{64}$/.test(requested.planRef))throw Error('计划标识无效');
|
|
19
|
+
const child=spawn(client.launcher,['manager','open-manager','--plan-ref',requested.planRef,...(client.journeyControl?['--journey-channel']:[])],{env:client.env,stdio:['ignore','pipe','pipe',...(client.journeyControl?['ipc']:[])]});
|
|
20
|
+
client.journeyControl?.attach(child);
|
|
21
|
+
let pending='',json='',finished=false;
|
|
22
|
+
return new Promise((resolve,reject)=>{
|
|
23
|
+
const stop=()=>child.kill('SIGTERM');
|
|
24
|
+
const finish=(error,value)=>{if(finished)return;finished=true;clearTimeout(timer);process.off('SIGINT',stop);process.off('SIGTERM',stop);
|
|
25
|
+
if(!error&&value?.responseFinished!==true)error=Error('当前引擎未核实确认响应结束;保留结果,不降级到旧页面');
|
|
26
|
+
child.kill('SIGTERM');
|
|
27
|
+
error?reject(error):resolve(value);
|
|
28
|
+
};
|
|
29
|
+
const timer=setTimeout(()=>finish(Error('确认等待到期;保留同次计划记录')),Math.max(1,Math.min(16*60*1000,requested.expiresAt-Date.now()+1000)));
|
|
30
|
+
process.once('SIGINT',stop);process.once('SIGTERM',stop);
|
|
31
|
+
child.once('error',e=>finish(e));
|
|
32
|
+
child.once('close',()=>{if(!finished)finish(Error('页面进程退出,结果待核实;不自动重放'));});
|
|
33
|
+
child.stderr.on('data',()=>{});
|
|
34
|
+
child.stdout.setEncoding('utf8');
|
|
35
|
+
child.stdout.on('data',chunk=>{
|
|
36
|
+
pending+=chunk;
|
|
37
|
+
if(pending.length+json.length>4*1024*1024)return finish(Error('状态输出超过限制'));
|
|
38
|
+
let end;
|
|
39
|
+
while((end=pending.indexOf('\n'))>=0){
|
|
40
|
+
const line=pending.slice(0,end);pending=pending.slice(end+1);
|
|
41
|
+
if(!json&&!line.trim().startsWith('{'))continue;
|
|
42
|
+
json+=line+'\n';let event;try{event=JSON.parse(json);}catch{continue;}json='';
|
|
43
|
+
if(event.planRef!==requested.planRef)continue;
|
|
44
|
+
client.journeyControl?.observe(child,event).catch(error=>finish(error));
|
|
45
|
+
if(event.status==='AWAITING_FOUNDATION_UI_CONFIRMATION'){
|
|
46
|
+
const url=new URL(event.url);if(url.protocol!=='http:'||url.hostname!=='127.0.0.1'||url.username||url.password)return finish(Error('确认网址无效'));
|
|
47
|
+
onChange({state:'pending',confirmationUrl:url.href,sessionId:event.sessionId});
|
|
48
|
+
}
|
|
49
|
+
if(event.status==='FOUNDATION_OPERATION_STATE')onChange({state:event.state,sessionId:event.sessionId});
|
|
50
|
+
if(event.status==='FOUNDATION_OPERATION_RESULT'){
|
|
51
|
+
// The result comes from this exact installed process/session. Uninstall
|
|
52
|
+
// may already have removed the launcher, so its external manager receipt
|
|
53
|
+
// must remain the durable recovery source; do not execute deleted code.
|
|
54
|
+
if(!event.sessionId)return finish(Error('操作结果缺少会话身份'));
|
|
55
|
+
finish(null,event);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
package/lib/operation-result.mjs
CHANGED
|
@@ -14,7 +14,8 @@ export function runtimeObservation(event) {
|
|
|
14
14
|
if(event.status==='FOUNDATION_OPERATION_STATE'&&['executing','consumed'].includes(event.state))return {phase:'executing',sessionId:event.sessionId,installationWrites:'possible'};
|
|
15
15
|
if(event.status==='BOOTSTRAP_OPERATION_ENDED'){
|
|
16
16
|
const state=['completed','failed','cancelled','expired'].includes(event.state)?event.state:'verification-required';
|
|
17
|
-
|
|
17
|
+
const selectedPending=state==='completed'&&event.journeyContext?.skillChoice==='selected';
|
|
18
|
+
return {phase:'runtime-ended',programState:state,state:selectedPending?'awaiting-skill':state,terminal:state!=='verification-required'&&!selectedPending,sessionId:event.sessionId,installationRoot:event.installationRoot,installationWrites:state==='completed'?'runtime-reported-installed':'unknown',runtimeHealth:event.result?.stableLauncherHealth||event.result?.lifecycleResult?.stableLauncherHealth||event.result?.executableHealth||'unknown',journeyContext:event.journeyContext,next:selectedPending?'程序已完成,正在准备所选 Codex 接入的独立确认':'只读核验稳定 installed launcher 或同次 session 结果,不重放确认'};
|
|
18
19
|
}
|
|
19
20
|
if(['cancelled-no-install','expired-no-install','shutdown-no-install'].includes(event.state))return {phase:'directory-selection-ended',state:event.state,terminal:true,installationWrites:'none'};
|
|
20
21
|
return null;
|
package/lib/progress-page.mjs
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
3
|
import {journeyView} from './journey-view.mjs';
|
|
4
|
+
import {singlePageDocument} from './single-page-view.mjs';
|
|
5
|
+
|
|
6
|
+
export function downloadText(download) {
|
|
7
|
+
if(!download||!Number.isFinite(download.downloadedBytes)||download.downloadedBytes<0)return '';
|
|
8
|
+
const amount=n=>(n/1048576).toFixed(2)+' MiB';
|
|
9
|
+
const known=Number.isFinite(download.totalBytes)&&download.totalBytes>0;
|
|
10
|
+
return '已接收 '+amount(download.downloadedBytes)+(known?' / '+amount(download.totalBytes)+(download.downloadedBytes<=download.totalBytes?' · '+Math.floor(download.downloadedBytes/download.totalBytes*100)+'%':'(长度待核实)'):'(总量未知)');
|
|
11
|
+
}
|
|
4
12
|
|
|
5
13
|
export function acquisitionProgress(record) {
|
|
6
14
|
const phases={discovering:'查询正式发行','verifying-trust':'核验发行来源','fetching-and-verifying-catalog':'获取并核验发行清单','fetching-and-verifying-runtime':'下载并核验运行文件','extracting-and-validating':'展开并检查文件','acquired':'获取验证完成','starting-confirmation-service':'准备目录选择与确认页','selecting-directory':'等待你选择目录','awaiting-confirmation':'等待你确认安装',executing:'正在安装','runtime-ended':'读取安装结果',finished:'安装结果已返回'};
|
|
@@ -12,27 +20,52 @@ export function acquisitionProgress(record) {
|
|
|
12
20
|
if(['awaiting-confirmation','executing','runtime-ended','finished'].includes(record.phase))observations.select={state:'completed',evidence:record.selectionId};
|
|
13
21
|
if(record.phase==='awaiting-confirmation')observations.program={state:'pending',evidence:record.sessionId};
|
|
14
22
|
if(record.phase==='executing')observations.program={state:'running',evidence:record.sessionId};
|
|
15
|
-
if(['runtime-ended','finished'].includes(record.phase))observations.program={state:record.state,evidence:record.sessionId};
|
|
23
|
+
if(['runtime-ended','finished'].includes(record.phase))observations.program={state:record.programState||record.state,evidence:record.sessionId};
|
|
16
24
|
if(record.runtimeHealth==='passed')observations.health={state:'completed',evidence:record.sessionId};
|
|
17
|
-
|
|
18
|
-
|
|
25
|
+
if(record.programState==='completed'){
|
|
26
|
+
observations.acquire={state:'completed',evidence:record.resultFile};
|
|
27
|
+
observations.select={state:'completed',evidence:record.selectionId};
|
|
28
|
+
observations.program={state:'completed',evidence:record.sessionId};
|
|
29
|
+
}
|
|
30
|
+
for(const key of ['material','register'])if(record.skillSteps?.[key])observations[key]=record.skillSteps[key];
|
|
31
|
+
for(const key of ['remove','program'])if(record.maintenanceSteps?.[key])observations[key]=record.maintenanceSteps[key];
|
|
32
|
+
const kind=record.kind||'install';
|
|
33
|
+
const journey=journeyView({id:record.operationId,kind,includeAcquisition:kind!=='uninstall',includeSelection:kind==='install',includeHealth:kind!=='uninstall',includeSkillRemoval:Boolean(observations.remove),observations,skillChoice:record.journeyContext?.skillChoice||'undecided'});
|
|
34
|
+
if(record.terminal&&record.state==='partial'){journey.ended=false;journey.title=record.programState==='completed'?'程序已完成,Codex 接入未完成':'本次操作尚未全部完成';journey.summary=record.next;}
|
|
35
|
+
const verb=kind==='uninstall'?'卸载':kind==='update'?'更新':'安装';
|
|
36
|
+
Object.assign(phases,{'awaiting-confirmation':'等待你确认'+verb,executing:'正在'+verb,'runtime-ended':'读取'+verb+'结果',finished:verb+'结果已返回'});
|
|
37
|
+
if(record.terminal&&record.state==='partial'&&record.programState==='completed'&&kind==='update')journey.title='程序已更新,Skill 尚未更新';
|
|
38
|
+
return {journey,operationId:record.operationId,title:journey.title,phase:({'skill-confirmation':'等待你确认 Codex 操作','skill-executing':'正在处理 Codex 对话能力'})[record.phase]||phases[record.phase]||'正在核实本次操作',state:record.state,version:record.currentVersion&&kind==='update'?record.currentVersion+' → '+record.version:record.version||record.currentVersion||null,download:record.download||null,program:record.programState==='completed'?'程序'+verb+'已完成;'+(kind==='uninstall'?'结果由独立回执保留':record.runtimeHealth==='passed'?'稳定入口健康已核验':'健康待核实'):record.installationWrites==='none'?'尚未执行程序操作':'程序结果以同次记录为准',skill:kind==='uninstall'?(observations.remove?.state==='completed'?'已解除已归属 Codex 注册;修改和未知文件按计划保留':'本次没有已核实的注册移除结果'):record.skillRegistered===true?'Skill 文件与归属已核验;新对话识别另验':record.journeyContext?.skillChoice==='skipped'?'本次不接入 Codex':record.journeyContext?.skillChoice==='selected'?'已选择接入 Codex;仅在分别确认后执行':'Codex 接入可选,尚未选择',project:'未自动接入项目',installationRoot:record.installationRoot||null,skillDestination:record.skillDestination||null,next:record.next||(record.terminal&&record.state==='completed'&&record.journeyContext?.skillChoice==='skipped'?'本次已跳过 Codex 接入;程序操作已完成,项目另行确认。':journey.summary),nextUrl:record.terminal?(kind!=='uninstall'&&record.workbench?.state==='ready'?record.workbench.url:null):record.confirmationUrl||null,nextLabel:record.terminal?'打开已安装工作台':record.phase?.startsWith('skill-')?'查看 Codex 精确确认':kind==='install'?'继续到目录选择 / 本人确认页':'查看'+verb+'确认',resultFile:record.resultFile||null,terminal,diagnostic:record.diagnostic||null};
|
|
19
39
|
}
|
|
20
40
|
|
|
21
41
|
// A bounded, read-only loopback view over the existing in-memory operation.
|
|
22
42
|
// No POST, launch endpoint, confirmation authority or downloaded executable.
|
|
23
|
-
export async function startProgressPage(readRecord) {
|
|
43
|
+
export async function startProgressPage(readRecord, {control = null} = {}) {
|
|
24
44
|
const token=crypto.randomBytes(24).toString('hex'),base=`/${token}`,clients=new Set();
|
|
25
|
-
const html=`<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Foundation 获取与安装进度</title><style>body{font:16px/1.6 system-ui;max-width:760px;margin:24px auto;padding:0 20px;color:#18181b;background:#fafafa;overflow-wrap:anywhere}section{background:white;border:1px solid #e4e4e7;border-radius:12px;padding:16px;margin:16px 0}h1{font-size:24px}h2{font-size:18px}a{display:inline-block;padding:12px;border:1px solid #71717a;border-radius:8px;color:inherit}pre{white-space:pre-wrap}a:focus-visible,summary:focus-visible{outline:3px solid #2563eb}[hidden]{display:none}</style><body><h1>正在准备 Foundation</h1><section role="status" aria-live="polite"><h2 id="phase">正在读取同次操作</h2><div id="journey-steps"></div><p id="journey-summary"></p><p id="download"></p><p id="program"></p><p id="skill"></p><p id="project"></p><p id="next"></p><a id="continue" hidden>继续到目录选择 / 本人确认页</a><p id="connection"></p></section><p>此页面只读,不批准安装;程序、对话能力与项目分别确认。未知长度不显示百分比。临时服务结束后,可从原对话读取保留的操作结果。</p><details><summary>操作与恢复详情</summary><pre id="raw"></pre></details><script>let last=null;function show(p){last=p;document.querySelector('h1').textContent=p.journey.title;document.getElementById('journey-summary').textContent=p.journey.summary;const steps=document.getElementById('journey-steps');steps.replaceChildren();for(const step of p.journey.steps){const line=document.createElement('p');line.textContent=step.title+':'+step.label;steps.append(line);}for(const key of ['phase','program','skill','project','next'])document.getElementById(key).textContent=p[key]||'';
|
|
45
|
+
const html=`<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Foundation 获取与安装进度</title><style>body{font:16px/1.6 system-ui;max-width:760px;margin:24px auto;padding:0 20px;color:#18181b;background:#fafafa;overflow-wrap:anywhere}section{background:white;border:1px solid #e4e4e7;border-radius:12px;padding:16px;margin:16px 0}h1{font-size:24px}h2{font-size:18px}a{display:inline-block;padding:12px;border:1px solid #71717a;border-radius:8px;color:inherit}pre{white-space:pre-wrap}a:focus-visible,summary:focus-visible{outline:3px solid #2563eb}[hidden]{display:none}.steps{display:flex;flex-wrap:wrap;gap:8px}.steps p{margin:0;background:#f4f4f5;padding:6px 10px;border-radius:6px;font-size:13px}header{font-size:14px;font-weight:600}#continue{background:#18181b;color:white;font-weight:600;text-decoration:none}#target{overflow-wrap:anywhere}@media(max-width:480px){body{padding:0 16px}.steps p{flex:1 1 110px}}</style><body><header>Foundation</header><h1>正在准备 Foundation</h1><section role="status" aria-live="polite"><h2 id="phase">正在读取同次操作</h2><div id="journey-steps" class="steps"></div><p id="journey-summary"></p><p id="version"></p><p id="download"></p><p id="target"></p><p id="program"></p><p id="skill"></p><p id="project"></p><p id="next"></p><a id="continue" hidden>继续到目录选择 / 本人确认页</a><p id="connection"></p></section><p>此页面只读,不批准安装;程序、对话能力与项目分别确认。未知长度不显示百分比。临时服务结束后,可从原对话读取保留的操作结果。</p><details><summary>操作与恢复详情</summary><pre id="raw"></pre></details><script>const downloadText=${downloadText.toString()};let last=null;function show(p){last=p;document.querySelector('h1').textContent=p.journey.title;document.getElementById('journey-summary').textContent=p.journey.summary;const steps=document.getElementById('journey-steps');steps.replaceChildren();for(const step of p.journey.steps){const line=document.createElement('p');line.textContent=step.title+':'+step.label;steps.append(line);}for(const key of ['phase','program','skill','project','next'])document.getElementById(key).textContent=p[key]||'';document.getElementById('download').textContent=downloadText(p.download);document.getElementById('version').textContent=p.version?'版本:'+p.version:'';document.getElementById('target').textContent=p.installationRoot?'程序目录:'+p.installationRoot:'';const link=document.getElementById('continue');link.hidden=!p.nextUrl;link.textContent=p.nextLabel;if(p.nextUrl)link.href=p.nextUrl;document.getElementById('raw').textContent=JSON.stringify(p,null,2);}const events=new EventSource(location.pathname+'/events');events.onmessage=e=>show(JSON.parse(e.data));events.onerror=()=>{events.close();document.getElementById('connection').textContent=last&&last.terminal?'临时进度服务已结束,上方保留最后收到的结果。':'连接已中断,当前状态待核实;不自动重试安装。'};window.addEventListener('pagehide',()=>events.close(),{once:true});</script></body></html>`;
|
|
26
46
|
let origin;
|
|
47
|
+
let terminalObserved=false,terminalRead=null;
|
|
27
48
|
const current=()=>JSON.stringify(acquisitionProgress(readRecord()));
|
|
28
|
-
const server=http.createServer((req,res)=>{
|
|
29
|
-
if(req.
|
|
49
|
+
const server=http.createServer(async(req,res)=>{
|
|
50
|
+
if(req.headers.host!==origin||req.headers.origin&&req.headers.origin!==`http://${origin}`){res.writeHead(403).end();return;}
|
|
51
|
+
if(control&&req.method==='GET'&&req.url===base+'/state'){const record=readRecord();if(record.terminal)res.once('finish',()=>{terminalObserved=true;terminalRead?.();});res.writeHead(200,{'content-type':'application/json','cache-control':'no-store'}).end(JSON.stringify({record,progress:acquisitionProgress(record),view:control.view()}));return;}
|
|
52
|
+
if(control&&req.method==='POST'&&req.url===base+'/action'){
|
|
53
|
+
if(req.headers.origin!==`http://${origin}`||req.headers['content-type']!=='application/json'){res.writeHead(403).end();return;}
|
|
54
|
+
try{let body='',size=0;for await(const chunk of req){size+=chunk.length;if(size>16384)throw Error('请求过大');body+=chunk;}const input=JSON.parse(body);if(input.csrf!==token)throw Error('页面确认身份不符');const result=await control.submit(input);res.writeHead(result.status,{'content-type':'application/json','cache-control':'no-store'}).end(JSON.stringify(result.body));}catch(error){res.writeHead(409,{'content-type':'application/json','cache-control':'no-store'}).end(JSON.stringify({message:error.message}));}return;
|
|
55
|
+
}
|
|
56
|
+
if(req.method!=='GET'){res.writeHead(403).end();return;}
|
|
30
57
|
const headers={'cache-control':'no-store','x-content-type-options':'nosniff','content-security-policy':"default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'"};
|
|
31
|
-
if(req.url===base){res.writeHead(200,{...headers,'content-type':'text/html; charset=utf-8'}).end(html);}
|
|
58
|
+
if(req.url===base){res.writeHead(200,{...headers,'content-type':'text/html; charset=utf-8'}).end(control?singlePageDocument(token):html);}
|
|
32
59
|
else if(req.url===base+'/events'){res.writeHead(200,{...headers,'content-type':'text/event-stream'});res.write(`data: ${current()}\n\n`);clients.add(res);req.on('close',()=>clients.delete(res));}
|
|
33
60
|
else{res.writeHead(404).end();}
|
|
34
61
|
});
|
|
35
62
|
await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(0,'127.0.0.1',resolve);});
|
|
36
63
|
origin=`127.0.0.1:${server.address().port}`;
|
|
37
|
-
|
|
64
|
+
control?.setOrigin(`http://${origin}`);
|
|
65
|
+
return {url:`http://${origin}${base}`,publish(){for(const res of clients)res.write(`data: ${current()}\n\n`);},async close(){
|
|
66
|
+
// Allow the same page to receive the terminal snapshot before this bounded
|
|
67
|
+
// service exits. No polling or page acknowledgement authorizes a mutation.
|
|
68
|
+
if(control&&readRecord().terminal&&!terminalObserved)await new Promise(resolve=>{const timer=setTimeout(resolve,15000);terminalRead=()=>{clearTimeout(timer);resolve();};});
|
|
69
|
+
for(const res of clients)res.end();clients.clear();return new Promise(resolve=>server.close(resolve));
|
|
70
|
+
}};
|
|
38
71
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {readOperation} from './operation-result.mjs';
|
|
2
|
+
import {installedClient} from './manager-step.mjs';
|
|
3
|
+
import {followSelectedSkill} from './skill-handoff.mjs';
|
|
4
|
+
|
|
5
|
+
// Explicit user-requested recovery, never an automatic replay of approval.
|
|
6
|
+
export async function resumeSkillHandoff({file,env,onChange,journeyControl=null}) {
|
|
7
|
+
const previous=readOperation(file);
|
|
8
|
+
if(previous.pid&&previous.pid!==process.pid){
|
|
9
|
+
try{process.kill(previous.pid,0);throw Error('原进程仍存在,先在原任务查询;不并发恢复');}
|
|
10
|
+
catch(e){if(e.code!=='ESRCH')throw e;}
|
|
11
|
+
}
|
|
12
|
+
if(previous.programState!=='completed'||previous.journeyContext?.skillChoice!=='selected'||!previous.installationRoot)throw Error('该记录不是程序完成后的 Skill 待续;先只读核实原操作,不重跑程序');
|
|
13
|
+
const client=installedClient(previous.installationRoot,env),current=client.call(['manager','inspect','--root',previous.installationRoot]).installation?.current;
|
|
14
|
+
if(!previous.installId||current?.identity?.installId!==previous.installId||previous.installedCandidateHash&¤t.candidateHash!==previous.installedCandidateHash)throw Error('恢复记录与当前安装不一致;需重新核实');
|
|
15
|
+
if(previous.currentPlanRef){
|
|
16
|
+
const status=client.call(['manager','status','--plan-ref',previous.currentPlanRef]);
|
|
17
|
+
if(['executing','consumed'].includes(status.state))throw Error('旧计划仅有执行中记录;先核实或按原恢复机制处理,不能自动重试');
|
|
18
|
+
// Completed material is reused by capability status. Cancelled/expired
|
|
19
|
+
// confirmation is not reused: followSelectedSkill requests a fresh plan.
|
|
20
|
+
}
|
|
21
|
+
const operation={...previous,pid:process.pid,state:'awaiting-skill',terminal:false};
|
|
22
|
+
onChange(operation);
|
|
23
|
+
return followSelectedSkill({operation,env,onChange,journeyControl});
|
|
24
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Product-area CSS transplanted from the approved 040 reference.
|
|
2
|
+
// Source SHA-256: e9949cc0d67e979b0297ca654eda1f163f2178b3338d4c4cf3ea9363e3cd8644
|
|
3
|
+
// Demo-only selectors remain inert; there are no demo controls in production.
|
|
4
|
+
export const singlePageStyle = "#foundation-flow-demo{--fp-bg:light-dark(#fff,#191b20);--fp-soft:light-dark(#f7f8fa,#23262c);--fp-text:light-dark(#20252d,#eff1f5);--fp-muted:light-dark(#606a78,#adb5c3);--fp-line:light-dark(#e5e8ee,#383d46);--fp-accent:light-dark(#315ee8,#98b3ff);--fp-active:light-dark(#f3f6ff,#232f48);--fp-good:light-dark(#23784c,#8ddcb1);--fp-error:light-dark(#b33434,#ffadad);font-family:Inter,-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;color:var(--fp-text);font-size:14px;line-height:1.6;color-scheme:light dark}\n#foundation-flow-demo *{box-sizing:border-box}#foundation-flow-demo [hidden]{display:none!important}#foundation-flow-demo button,#foundation-flow-demo input,#foundation-flow-demo select{font:inherit;color:inherit}#foundation-flow-demo button,#foundation-flow-demo select{cursor:pointer}#foundation-flow-demo button{border:1px solid var(--fp-line);background:var(--fp-bg);border-radius:7px;padding:9px 15px;min-height:42px}#foundation-flow-demo button:disabled{opacity:.55;cursor:default}#foundation-flow-demo .demo-tools{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-bottom:14px;color:var(--fp-muted);font-size:12px}#foundation-flow-demo .demo-tools label{margin-left:auto;display:flex;align-items:center;gap:8px}#foundation-flow-demo select{padding:7px;border:1px solid var(--fp-line);border-radius:6px;background:var(--fp-bg)}#foundation-flow-demo .product{background:var(--fp-bg);border:1px solid var(--fp-line);border-radius:14px;overflow:hidden}#foundation-flow-demo .brand{display:flex;align-items:center;gap:9px;padding:18px 26px;border-bottom:1px solid var(--fp-line);font-weight:500}#foundation-flow-demo .brand-mark{display:grid;place-items:center;width:26px;height:26px;border-radius:6px;background:var(--fp-text);color:var(--fp-bg)}#foundation-flow-demo .private-label{margin-left:auto;font-size:12px;color:var(--fp-muted);font-weight:400}#foundation-flow-demo .overview{padding:24px 26px 20px}#foundation-flow-demo h1{font-size:25px;font-weight:500;line-height:1.3;margin:5px 0 8px}#foundation-flow-demo p{margin:6px 0}#foundation-flow-demo .eyebrow{color:var(--fp-accent);font-size:12px;font-weight:500}#foundation-flow-demo #flow-description{color:var(--fp-muted)}#foundation-flow-demo .metadata{display:flex;flex-direction:column;gap:4px;margin:15px 0;color:var(--fp-muted);font-size:12px;overflow-wrap:anywhere}#foundation-flow-demo .progress-label{display:flex;justify-content:space-between;gap:10px;flex-wrap:wrap;font-size:12px;margin-bottom:8px}#foundation-flow-demo #flow-status{color:var(--fp-accent)}#foundation-flow-demo .track{height:5px;background:var(--fp-soft);border-radius:4px;overflow:hidden}#foundation-flow-demo #flow-bar{height:100%;background:var(--fp-accent);width:0;transition:width .2s}#foundation-flow-demo .steps{list-style:none;padding:0 26px;margin:0}#foundation-flow-demo .step{border-top:1px solid var(--fp-line);padding:15px 0}#foundation-flow-demo .step.current{background:var(--fp-active);border:1px solid var(--fp-line);border-radius:10px;padding:16px;margin:4px -16px 8px}#foundation-flow-demo .step-top{display:flex;align-items:center;gap:12px}#foundation-flow-demo .step-number{flex:none;border:1px solid var(--fp-line);border-radius:50%;width:25px;height:25px;display:grid;place-items:center;font-size:12px;color:var(--fp-muted)}#foundation-flow-demo .done .step-number{color:var(--fp-good);border-color:transparent;background:var(--fp-soft)}#foundation-flow-demo .current .step-number{background:var(--fp-accent);color:var(--fp-bg);border-color:transparent}#foundation-flow-demo .step-title{font-weight:500;flex:1}#foundation-flow-demo .step-state{font-size:12px;color:var(--fp-muted)}#foundation-flow-demo .done .step-state{color:var(--fp-good)}#foundation-flow-demo .step-body{margin:12px 0 0 37px;color:var(--fp-muted)}#foundation-flow-demo .field{display:block;margin:13px 0 5px;color:var(--fp-text)}#foundation-flow-demo input[type=text]{width:100%;min-width:0;font-size:16px;background:var(--fp-bg);border:1px solid var(--fp-line);border-radius:7px;padding:10px}#foundation-flow-demo .check{display:flex;align-items:flex-start;gap:9px;margin:14px 0;color:var(--fp-text)}#foundation-flow-demo .check input{margin-top:6px;accent-color:var(--fp-accent)}#foundation-flow-demo .hint{font-size:12px;color:var(--fp-muted);overflow-wrap:anywhere}#foundation-flow-demo .actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:17px}#foundation-flow-demo .primary{background:var(--fp-accent);border-color:var(--fp-accent);color:var(--fp-bg);font-weight:500}#foundation-flow-demo .danger{background:var(--fp-error);border-color:var(--fp-error);color:var(--fp-bg)}#foundation-flow-demo .error{color:var(--fp-error);font-size:13px;margin-top:6px}#foundation-flow-demo .facts{border-left:2px solid var(--fp-line);padding-left:12px;margin:12px 0;overflow-wrap:anywhere}#foundation-flow-demo .result{margin:14px 26px 24px;padding:18px;background:var(--fp-soft);border-radius:10px}#foundation-flow-demo .result h2{font-size:18px;font-weight:500;margin:0 0 8px}#foundation-flow-demo footer{padding:17px 26px;border-top:1px solid var(--fp-line);color:var(--fp-muted);font-size:12px}#foundation-flow-demo summary{cursor:pointer}#foundation-flow-demo .completed-detail{margin-left:37px;font-size:12px;color:var(--fp-muted)}\n@media(max-width:480px){#foundation-flow-demo .overview,#foundation-flow-demo .brand,#foundation-flow-demo footer{padding-left:17px;padding-right:17px}#foundation-flow-demo .steps{padding:0 17px}#foundation-flow-demo .step.current{margin-left:-8px;margin-right:-8px;padding:12px 8px}#foundation-flow-demo .step-body{margin-left:0}#foundation-flow-demo .step-top{gap:8px}#foundation-flow-demo .step-state{max-width:82px;text-align:right}#foundation-flow-demo .demo-tools label{margin-left:0}#foundation-flow-demo .result{margin:14px 17px}#foundation-flow-demo h1{font-size:23px}}\n@media(prefers-reduced-motion:reduce){#foundation-flow-demo #flow-bar{transition:none}}";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {singlePageStyle} from './single-page-style.mjs';
|
|
2
|
+
|
|
3
|
+
// The approved product DOM/CSS; the client consumes facts, not demo timers.
|
|
4
|
+
export function singlePageDocument(csrf) {
|
|
5
|
+
return `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Foundation 安装与维护</title><style>body{margin:0}${singlePageStyle}</style><body><div id="foundation-flow-demo" aria-label="Foundation 安装与维护"><main class="product"><header class="brand"><span class="brand-mark">F</span><span>Foundation</span><span class="private-label">安装与维护</span></header><section class="overview"><div class="eyebrow" id="flow-eyebrow">正在核实</div><h1 id="flow-title">Foundation</h1><p id="flow-description"></p><div class="metadata"><span id="flow-version"></span><span id="flow-destination"></span></div><div class="progress-label"><span id="flow-count"></span><span id="flow-status" role="status" aria-live="polite"></span></div><div class="track" role="progressbar" aria-label="已完成步骤" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><div id="flow-bar"></div></div></section><ol id="flow-steps" class="steps"></ol><section id="flow-result" class="result" hidden aria-live="polite"></section><footer><p id="connection" role="status"></p><details><summary>技术详情</summary><pre id="raw" style="white-space:pre-wrap;overflow-wrap:anywhere"></pre></details></footer></main></div><script>(${singlePageClient.toString()})(${JSON.stringify(csrf)});</script></body></html>`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function singlePageClient(csrf) {
|
|
9
|
+
const q=s=>document.querySelector(s),esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
10
|
+
let last=null,busy=false,revision=null,stopped=false;
|
|
11
|
+
const verbs={install:'安装',update:'更新',uninstall:'卸载'};
|
|
12
|
+
function cleanupText(session){const c=session?.hostCleanup;if(!c)return '';return '<div class="facts"><p>更新成功并通过健康检查后,清理本次下载的归档和展开候选。</p><p>清理位置:'+esc(c.root)+'</p><p>范围:'+esc(c.files?.length??'待核实')+' 个文件;'+(Number.isFinite(c.bytes)?(c.bytes/1048576).toFixed(2)+' MiB':'大小待核实')+'</p><p>保留获取回执、验证材料、回退版本、项目和未知或已修改文件;仍在使用的材料不会强删。</p></div>';}
|
|
13
|
+
function show(p){
|
|
14
|
+
last=p;const r=p.record,v=p.view,kind=r.kind||'install',verb=verbs[kind],choice=r.phase==='choosing-intent';
|
|
15
|
+
const titles={select:'选择安装位置',acquire:kind==='update'?'下载并检查新版':'下载并检查文件',program:verb+' Foundation',material:'准备安装 Skill',register:kind==='update'?'更新 Codex 中的 Skill':'将 Skill 安装到 Codex',remove:'移除 Codex 中的 Skill',health:'检查'+verb+'结果','optional-skill':'Foundation Skill'};
|
|
16
|
+
const stateLabels={failed:'本步失败',cancelled:'已取消',expired:'已过期','verification-required':'结果待核实',running:'正在执行',executing:'正在执行',consumed:'正在执行',undecided:'可选,待选择'};
|
|
17
|
+
const order=kind==='uninstall'?['remove','program']:['select','acquire','program','material','register','optional-skill','health'];
|
|
18
|
+
const steps=p.progress.journey.steps.map(s=>({...s})).sort((a,b)=>order.indexOf(a.key)-order.indexOf(b.key));
|
|
19
|
+
if(choice)for(const s of steps)s.state=s.key==='select'?'pending':'planned';
|
|
20
|
+
if(r.intentSelected){const s=steps.find(s=>s.key==='select');if(s)s.state='completed';}
|
|
21
|
+
let active=choice?'select':v?.type==='selection'?'select':v?.session?.capabilityId?(v.session.operation==='install'?'material':v.session.operation==='uninstall'?'remove':'register'):v?.session?'program':steps.find(s=>!['completed','skipped','retained'].includes(s.state))?.key;
|
|
22
|
+
// The final result check must not look finished while a chosen Skill step
|
|
23
|
+
// still awaits confirmation. Program health remains visible in its receipt.
|
|
24
|
+
const health=steps.find(s=>s.key==='health');
|
|
25
|
+
if(health?.state==='completed'&&!r.terminal)health.state='planned';
|
|
26
|
+
if(r.terminal)active=null;
|
|
27
|
+
const executing=['executing','consumed'].includes(v?.session?.state)||['executing','skill-executing'].includes(r.phase);
|
|
28
|
+
const count=steps.filter(s=>['completed','skipped','retained'].includes(s.state)).length;
|
|
29
|
+
q('#flow-title').textContent=r.terminal?(r.state==='completed'?'Foundation '+verb+'完成':'本次'+verb+'未全部完成'):verb+' Foundation';
|
|
30
|
+
q('#flow-eyebrow').textContent=r.terminal?'本次结果':executing?'正在执行':busy?'正在提交请求':v||choice?'等待你的操作':'正在进行';
|
|
31
|
+
q('#flow-description').textContent=kind==='install'?'安装程序和 Skill,让 Codex 可以使用 Foundation。':kind==='update'?'更新程序和已有 Skill,保留你的项目与制作资料。':'移除 Foundation 和它的 Skill,保留你的项目与制作资料。';
|
|
32
|
+
q('#flow-version').textContent='版本:'+(p.progress.version||'待核实');q('#flow-destination').textContent='程序位置:'+(r.installationRoot||r.destinationIntent||'等待选择');
|
|
33
|
+
q('#flow-count').textContent='已处理 '+count+' / '+steps.length+' 步';q('#flow-status').textContent=executing?'正在执行当前步骤':busy?'正在提交请求':r.terminal?(r.state==='completed'?'本次流程已完成':'查看未完成项'):('当前:'+(titles[active]||p.progress.phase));
|
|
34
|
+
const percent=steps.length?count/steps.length*100:0;q('#flow-bar').style.width=percent+'%';q('.track').setAttribute('aria-valuenow',String(Math.round(percent)));
|
|
35
|
+
const key=JSON.stringify([active,v?.session?.sessionId,v?.session?.state,v?.selectionId,r.terminal,steps.map(s=>s.state),v?.error]);
|
|
36
|
+
if(key!==revision&&!busy){
|
|
37
|
+
revision=key;
|
|
38
|
+
q('#flow-steps').innerHTML=steps.map((s,i)=>{
|
|
39
|
+
const done=['completed','retained'].includes(s.state),skip=s.state==='skipped',current=s.key===active;
|
|
40
|
+
let body='';
|
|
41
|
+
if(current&&(choice||v?.type==='selection'))body='<form id="current-form"><label class="field" for="install-path">安装到哪里?</label><input id="install-path" name="destination" type="text" value="'+esc(r.destinationIntent||v?.suggestion||'')+'" spellcheck="false" required><p class="hint">请输入完整路径,可以包含成对引号。不会覆盖已有内容,最终目录仍由程序检查。</p><label class="check"><input id="include-skill" name="includeSkill" type="checkbox" '+(r.journeyContext?.skillChoice==='skipped'?'':'checked')+'><span>同时安装 Foundation Skill<br><span class="hint">让 Codex 能识别 Foundation 指令,并按 Foundation 规范工作。</span></span></label><p class="hint">获取缓存:'+esc(r.acquisitionRoot||'以当前任务已披露位置为准')+'。选择不是安装批准。</p><div id="step-error" class="error" role="alert">'+esc(v?.error||'')+'</div><div class="actions"><button class="primary" name="action" value="'+(choice?'choose':'select')+'">使用这个位置</button><button name="action" value="'+(choice?'cancel-intent':'cancel')+'" formnovalidate>取消本步</button></div></form>';
|
|
42
|
+
else if(current&&v?.session){const f=v.feedback;body='<p>'+esc(f.detail)+'</p><div class="facts">'+f.namedTargets.map(t=>'<p>'+esc(t.label)+':'+esc(t.path||'待核实')+'</p>').join('')+'<p>保留:'+esc(f.preserves.join(';'))+'</p><p>'+esc(f.notDone)+'</p>'+cleanupText(v.session)+((v.session.deletes||[]).length?'<details open><summary>删除范围</summary>'+v.session.deletes.map(x=>'<p>'+esc(x==='foundation-installation:full'?'本安装中经所有权核验的程序、运行时和状态文件;未知或用户修改的文件保留。':typeof x==='string'?x:JSON.stringify(x))+'</p>').join('')+'</details>':'')+'</div><p class="hint">每次确认只针对当前计划,不批准后续步骤。Skill 文件就位不等于新对话已识别。</p><form id="current-form"><div id="step-error" class="error" role="alert"></div><div class="actions">'+(v.session.state==='pending'?'<button class="'+(kind==='uninstall'?'danger':'primary')+'" name="action" value="'+esc(v.action)+'">确认'+esc(titles[s.key])+'</button><button name="action" value="cancel-no-change">取消本步</button>':'<p>本步已开始或结束,不能重复确认。</p>')+'</div></form>';}
|
|
43
|
+
else if(current)body='<p role="status">'+esc(p.progress.phase)+'</p><p class="hint">下一步会在本页展开,不需要查找其他网址。</p>';
|
|
44
|
+
return '<li class="step '+(current?'current':done||skip?'done':'later')+'"><div class="step-top"><span class="step-number">'+(skip?'−':done?'✓':i+1)+'</span><span class="step-title">'+esc(titles[s.key]||s.title)+'</span><span class="step-state">'+esc(skip?'已跳过':done?'已完成':stateLabels[s.state]||(current?(choice?'等待选择':p.progress.phase):r.terminal?'本次未执行':'等待前序步骤'))+'</span></div>'+(current?'<div class="step-body">'+body+'</div>':done||skip?'<details class="completed-detail"><summary>查看结果</summary><p>'+esc(skip?'本次未执行此步骤。':s.evidence?JSON.stringify(s.evidence):'结果来自本次操作记录')+'</p></details>':'')+'</li>';
|
|
45
|
+
}).join('');
|
|
46
|
+
}
|
|
47
|
+
if(busy&&executing){const live=q('.current .step-state');if(live)live.textContent='正在执行';}
|
|
48
|
+
q('#flow-result').hidden=!r.terminal;
|
|
49
|
+
if(r.terminal)q('#flow-result').innerHTML='<h2>'+esc(r.state==='completed'?verb+'完成':'部分完成 / 待核实')+'</h2><p>'+esc(p.progress.program)+'</p><p>Skill:'+esc(p.progress.skill)+'</p><p>项目:'+esc(p.progress.project)+'</p><p class="hint">下一步:'+esc(p.progress.next)+'</p>';
|
|
50
|
+
const safe=JSON.parse(JSON.stringify(p));if(safe.view){delete safe.view.managerNonce;delete safe.view.nonce;}q('#raw').textContent=JSON.stringify(safe,null,2);
|
|
51
|
+
}
|
|
52
|
+
document.addEventListener('submit',async e=>{if(e.target.id!=='current-form')return;e.preventDefault();if(busy||!last)return;busy=true;const form=e.target;form.querySelectorAll('button').forEach(b=>b.disabled=true);q('#flow-status').textContent='正在提交请求';const action=e.submitter.value,v=last.view;const body={csrf,operationId:last.record.operationId,action,...(['choose','select'].includes(action)?{destination:form.elements.destination.value,skillChoice:form.elements.includeSkill.checked?'selected':'skipped'}:{managerNonce:v?.managerNonce,sessionId:v?.session?.sessionId,planHash:v?.session?.planHash})};
|
|
53
|
+
try{const response=await fetch(location.pathname+'/action',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(body)});const result=await response.json();if(!response.ok){q('#step-error').textContent=result.message||result.code||'本步未完成,请核实';busy=false;form.querySelectorAll('button').forEach(b=>b.disabled=false);return;}busy=false;revision=null;await refresh();}catch{q('#connection').textContent='请求结果待核实,不自动重试。已收到的结果保留;刷新查询同次流程。';}});
|
|
54
|
+
async function refresh(){try{const response=await fetch(location.pathname+'/state');if(!response.ok)throw Error();show(await response.json());q('#connection').textContent='';}catch{q('#connection').textContent=last?.record?.terminal?'结果服务已结束,上方保留最后核实结果。可在对话中按本次操作记录再次查询。':'连接已中断,保留最后核实结果;不自动重试操作。';}}
|
|
55
|
+
async function poll(){if(stopped)return;await refresh();if(!stopped)setTimeout(poll,750);}poll();window.addEventListener('pagehide',()=>{stopped=true},{once:true});
|
|
56
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import {installedClient,observePlan} from './manager-step.mjs';
|
|
4
|
+
import {plainPath} from './acquire.mjs';
|
|
5
|
+
|
|
6
|
+
// Coordinates existing exact-plan APIs. It never sends a confirmation request.
|
|
7
|
+
// The installed launcher resolves current on every call; downloaded code is not
|
|
8
|
+
// reused as installed authority. onChange is observation, not authorization.
|
|
9
|
+
export async function followSelectedSkill({operation,env,onChange,journeyControl=null}) {
|
|
10
|
+
if(operation.programState!=='completed'||operation.journeyContext?.skillChoice!=='selected')throw Error('Skill 接续需要已完成程序和明确所选意向');
|
|
11
|
+
const client=installedClient(operation.installationRoot,env,journeyControl),{root,call}=client;
|
|
12
|
+
const initial=call(['manager','inspect','--root',root]);
|
|
13
|
+
const identity=initial.installation?.current;
|
|
14
|
+
if(!identity?.identity?.installId||initial.bridge?.currentVersion!==identity.version)throw Error('当前安装身份无法核实,停止 Skill 接续');
|
|
15
|
+
if(operation.installId&&operation.installId!==identity.identity.installId)throw Error('恢复记录属于另一安装,停止接续');
|
|
16
|
+
onChange({installId:identity.identity.installId,installedCandidateHash:identity.candidateHash});
|
|
17
|
+
const manifestFile=plainPath(path.join(root,identity.appPath,'artifacts/skills/ai-product-foundation-kit/capability.json'));
|
|
18
|
+
if(!manifestFile.startsWith(root+path.sep))throw Error('Skill 材料必须属于当前安装');
|
|
19
|
+
if(initial.codexSkill?.state==='files-installed-host-discovery-unverified'){
|
|
20
|
+
const destination=plainPath(initial.codexSkill.destination);
|
|
21
|
+
const binding=JSON.parse(fs.readFileSync(plainPath(path.join(destination,'foundation-installation.json'))));
|
|
22
|
+
if(binding.installId===identity.identity.installId&&binding.installationRoot===root&&fs.readFileSync(plainPath(path.join(destination,'SKILL.md'))).equals(fs.readFileSync(path.join(path.dirname(manifestFile),'SKILL.md'))))return {state:'completed',terminal:true,phase:'finished',skillRegistered:true,skillDestination:destination,hostDiscoveryVerified:false,skillSteps:{material:{state:'completed',evidence:{source:'installed-owned-current-bytes'}},register:{state:'completed',evidence:{source:'installed-owned-current-bytes'}}},next:'当前版本 Skill 文件和归属已核实,无需重复注册;新任务识别另验。'};
|
|
23
|
+
}
|
|
24
|
+
const checkIdentity=()=>{
|
|
25
|
+
const current=call(['manager','inspect','--root',root]);
|
|
26
|
+
if(current.installation?.current?.identity?.installId!==identity.identity.installId||current.installation.current.candidateHash!==identity.candidateHash)throw Error('接续期间安装发生变化,需重新核实');
|
|
27
|
+
return current;
|
|
28
|
+
};
|
|
29
|
+
let previousPlanRef=operation.previousPlanRef||null;
|
|
30
|
+
const steps={};
|
|
31
|
+
const material=call(['capability','status','--root',root,'--manifest',manifestFile]);
|
|
32
|
+
const reusable=['CAPABILITY_NOT_REGISTERED','CAPABILITY_INACTIVE','PROJECT_NOT_ENABLED','CAPABILITY_READY'].includes(material.code);
|
|
33
|
+
const ownedUpdate=operation.kind==='update'&&initial.codexSkill?.state==='files-installed-host-discovery-unverified'&&material.code==='CAPABILITY_IDENTITY_MISMATCH';
|
|
34
|
+
if(!reusable&&material.code!=='CAPABILITY_NOT_INSTALLED'&&!ownedUpdate)throw Error('已有 Skill 材料不兼容或归属待核实;不自动覆盖');
|
|
35
|
+
if(reusable)steps.material={state:'completed',evidence:{source:'installed-capability-status',code:material.code}};
|
|
36
|
+
for(const kind of [...(!reusable?['material']:[]),'register']) {
|
|
37
|
+
checkIdentity();
|
|
38
|
+
const requested=call(['manager','request-plan','--operation',kind==='material'?'capability-install':'capability-register','--parameters-json',JSON.stringify({installationRoot:root,manifestFile,...(kind==='register'?{connectCodex:true}:{})}),...(previousPlanRef?['--previous-plan-ref',previousPlanRef]:operation.sessionId?['--previous-session-id',operation.sessionId]:[])]);
|
|
39
|
+
if(!/^foundation-plan-[a-f0-9]{64}$/.test(requested.planRef))throw Error('接续计划标识无效');
|
|
40
|
+
steps[kind]={state:'pending',evidence:{planRef:requested.planRef}};
|
|
41
|
+
onChange({state:'awaiting-skill',phase:'skill-confirmation',terminal:false,skillSteps:{...steps},currentPlanRef:requested.planRef,next:kind==='material'?'确认准备对话能力材料;随后继续注册确认':'确认将 Foundation Skill 接入 Codex'});
|
|
42
|
+
const event=await observePlan(client,requested,observation=>onChange({phase:observation.state==='pending'?'skill-confirmation':'skill-executing',...(observation.temporaryManager?{temporaryManager:observation.temporaryManager}:{}),...(observation.confirmationUrl?{confirmationUrl:observation.confirmationUrl}:{}),skillSteps:{...steps,[kind]:{state:observation.state,evidence:{planRef:requested.planRef,sessionId:observation.sessionId}}}}));
|
|
43
|
+
const result=call(['manager','status','--plan-ref',requested.planRef]);
|
|
44
|
+
if(result.sessionId!==event.sessionId||result.state!==event.state)throw Error('Skill 记录与事件不一致');
|
|
45
|
+
steps[kind]={state:result.state,evidence:{planRef:requested.planRef,sessionId:result.sessionId,recordLocation:result.recordLocation}};
|
|
46
|
+
onChange({skillSteps:{...steps}});
|
|
47
|
+
if(result.state!=='completed')return {state:'partial',phase:'finished',terminal:true,skillRegistered:false,skillSteps:steps,next:'程序已安装;本次 Skill 未完成。保留同次计划结果,需要时仅继续未完成步骤。'};
|
|
48
|
+
previousPlanRef=requested.planRef;
|
|
49
|
+
}
|
|
50
|
+
const verified=checkIdentity().codexSkill;
|
|
51
|
+
if(verified?.state!=='files-installed-host-discovery-unverified')throw Error('Skill 注册文件与归属未通过核验;程序成功保留');
|
|
52
|
+
return {state:'completed',phase:'finished',terminal:true,skillRegistered:true,skillDestination:verified.destination,skillSteps:steps,hostDiscoveryVerified:false,next:'程序与所选 Skill 文件已就位;新开 Codex 对话验证识别,项目接入另行确认。'};
|
|
53
|
+
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@josephyulei/summon-foundation",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@josephyulei/summon-foundation",
|
|
9
|
-
"version": "0.1.
|
|
9
|
+
"version": "0.1.6",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@sigstore/bundle": "4.0.0",
|
package/package.json
CHANGED