@josephyulei/summon-foundation 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,15 +1,19 @@
1
1
  # summon foundation
2
2
 
3
+ 0.1.5 配合 Foundation 0.2.11。选中 Skill 后由程序接续精确确认;`--update --root <安装根> --version <固定版本>`、`--uninstall --root <安装根>` 组织维护步骤。`--resume <同次结果文件>` 仅恢复程序成功后的所选 Skill,不重装或重放批准。
4
+
5
+ 0.1.5 提供获取期间只读进度页。获取成功后同一页面关联运行程序返回的目录/确认页,仍由 Codex 内置浏览器打开;配合 Foundation 0.2.11 展示本次流程、当前目标和剩余动作。临时服务只随本次任务运行,不是守护进程,不执行未核验归档,也不替本人确认。原操作回执仍可独立只读查询,页断开不改写已核实终态。
6
+
3
7
  Foundation 的薄获取入口,不包含完整运行包,没有 npm 安装生命周期脚本。
4
8
  只有明确执行才查询/获取;安装和 Skill 注册继续各自需要用户确认。
5
9
 
6
10
  可用性以 npm registry 的实际版本和公共 GitHub Release 为准;本目录或说明存在不是发布回执。只使用本项目已核验的包名,缺包时不换同名来源。
7
11
 
8
12
  ```text
9
- npx --yes --package @josephyulei/summon-foundation@0.1.3 summon foundation
13
+ npx --yes --package @josephyulei/summon-foundation@0.1.5 summon foundation
10
14
  ```
11
15
 
12
- 0.1.3 入口配合 Foundation 0.2.8 或后续支持目录选择的正式发行使用。默认命令实际查询、固定版本、披露缓存、下载并校验,然后保持运行等待安装页目录选择。选择目录后还要亲自确认精确安装计划;不会注册 Skill 或接入项目。实际获取仍须通过 registry 与 Release 校验,本文不替代发布证明。
16
+ 0.1.5 入口配合 Foundation 0.2.11 正式发行使用。默认命令实际查询、固定版本、披露缓存、下载并校验,然后保持运行等待安装页目录选择。选择目录后还要亲自确认精确安装计划;不会自动注册 Skill 或接入项目。实际获取仍须通过 registry 与 Release 校验,本文不替代发布证明。
13
17
 
14
18
  Codex 需要打开返回的内置浏览器网址,并分段等待同次操作结果。npm 不能自行操控或唤醒 Codex,对话主动告知不是程序退出码保证;新对话接续仍待真人验收。系统浏览器只是明确告知后的备用,不自动打开。
15
19
 
package/bin/summon.mjs CHANGED
@@ -2,8 +2,13 @@
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import {spawn,spawnSync} from 'node:child_process';
5
- import {inspectRelease,acquireRelease,plainPath} from '../lib/acquire.mjs';
5
+ import {inspectRelease,plainPath} from '../lib/acquire.mjs';
6
6
 
7
+ import {acquireInWorker} from '../lib/acquisition-task.mjs';
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 {resumeSkillHandoff} from '../lib/resume-handoff.mjs';
7
12
  import {parseSummonArgs} from '../lib/cli-options.mjs';
8
13
  import {createOperation,saveOperation,readOperation,runtimeObservation} from '../lib/operation-result.mjs';
9
14
 
@@ -12,11 +17,16 @@ summon foundation --inspect [--version x.y.z]
12
17
  summon foundation --prepare --version x.y.z --destination /absolute/folder
13
18
  summon foundation --acquire --version x.y.z
14
19
  summon foundation --status /absolute/acquisition/operation-result.json
20
+ summon foundation --update --root /absolute/installed-folder --version x.y.z
21
+ summon foundation --uninstall --root /absolute/installed-folder
22
+ summon foundation --resume /absolute/acquisition/operation-result.json
15
23
  默认命令下载并验证正式发行,进入安装准备;未给目录时在安装页选择。不会静默安装或注册 Skill。
16
24
  --inspect 才只读查询发行,不下载运行归档。
17
25
  --prepare 下载并核验固定发行,打开 Foundation 本人确认流程;不会代替确认。
18
26
  --acquire 仅下载并核验更新材料,返回候选与回执;不启动安装或执行更新。
19
27
  --status 只读本次结果记录;中间记录不证明进程仍在运行,不重试或重放确认。
28
+ --update / --uninstall 从已安装稳定入口组织必要步骤;每一步仍需页面独立确认,不自动注册原本没有的 Skill。
29
+ --resume 明确恢复程序已完成后的所选 Skill;核验当前安装与旧记录后仅准备未完成项的新确认,不重复安装。
20
30
  仅 macOS arm64。npm 入口需要 Node/npm;已安装 Foundation 使用随包运行时。
21
31
  普通终端不能控制 Codex 对话或内置浏览器。无需 npm 的引导见公共仓库安装说明。`;
22
32
  function fail(message){throw Error(message);}
@@ -37,8 +47,8 @@ function stageDirectory(){
37
47
  const stat=fs.lstatSync(cache);if(stat.uid!==process.getuid()||(stat.mode&0o777)!==0o700)fail('获取缓存必须由当前用户独占 0700;不会自动 chmod 已有目录');
38
48
  return fs.mkdtempSync(path.join(cache,'acquisition.'));
39
49
  }
40
- let operation=null,stage=null;
41
- function updateOperation(patch){if(!operation)return;Object.assign(operation,patch,{updatedAt:new Date().toISOString()});const file=saveOperation(operation,stage);console.log(JSON.stringify({status:'FOUNDATION_INSTALL_OPERATION',...operation,resultFile:file,recordSaved:!!file}));}
50
+ let operation=null,stage=null,progressPage=null;
51
+ 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}));}
42
52
  async function openInstalledWorkbench(root,env){
43
53
  let child;
44
54
  try{
@@ -63,28 +73,51 @@ try{
63
73
  const [major,minor]=process.versions.node.split('.').map(Number);
64
74
  if(major<22||(major===22&&minor<9))fail('获取入口需要 Node.js 22.9 或以上;尚未安装,不自动安装依赖');
65
75
  if(process.platform!=='darwin'||process.arch!=='arm64')fail('当前仅支持 macOS arm64;尚未安装');
66
- if(args.prepare||args.acquire)sourceGuard();
67
- if(args.prepare||args.acquire){operation=createOperation();stage=stageDirectory();updateOperation({});if(!saveOperation(operation,stage))fail('无法在已披露的获取目录保存操作记录;尚未联网或安装');}
76
+ const maintenance=args.update||args.uninstall;
77
+ if(args.prepare||args.acquire||maintenance||args.resume)sourceGuard();
78
+ if(args.prepare||args.acquire||maintenance){operation=createOperation();stage=stageDirectory();updateOperation({});if(!saveOperation(operation,stage))fail('无法在已披露的获取目录保存操作记录;尚未联网或安装');}
68
79
  if(args.prepare)console.log('正在准备 Foundation 安装:将查询并固定本次正式版本,说明缓存位置后下载核验。未选目录会在安装页选择;之后仍需本人确认精确计划。请保持当前 Codex 任务等待并打开返回的内置浏览器网址;不会自动改用系统浏览器。');
69
- const context=inspectRelease(args.version);
70
- const guide=`https://github.com/${context.repository.full_name}/blob/${context.documentationCommit}/docs/install-with-codex.md`;
71
- if(!args.prepare&&!args.acquire){
80
+ if(args.resume){
81
+ 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];
82
+ const result=await resumeSkillHandoff({file:args.resume,env,onChange:patch=>{if(!operation){operation=patch;stage=path.dirname(plainPath(args.resume));}updateOperation(patch);}});
83
+ updateOperation(result);process.exitCode=result.state==='completed'?0:1;
84
+ }else if(maintenance){
85
+ plainPath(args.root);
86
+ 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
+ updateOperation({kind:args.update?'update':'uninstall',installationRoot:args.root});
88
+ progressPage=await startProgressPage(()=>operation);updateOperation({progressUrl:progressPage.url});
89
+ let candidate;
90
+ if(args.update){
91
+ 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})});
92
+ const directory=plainPath(path.dirname(receipt.launcher)),manifest=JSON.parse(fs.readFileSync(plainPath(path.join(directory,'manifest.json'))));
93
+ if(manifest.productVersion!==context.version)fail('获取版本与候选不一致');
94
+ candidate={path:directory,manifestHash:manifest.candidateHash,version:manifest.productVersion,bytes:manifest.totalBytes,runtimeHash:manifest.files.find(f=>f.path===manifest.runtime.path)?.sha256};
95
+ }
96
+ updateOperation(await followMaintenance({operation,kind:operation.kind,candidate,env,onChange:updateOperation}));
97
+ process.exitCode=operation.state==='completed'?0:1;
98
+ await progressPage.close();progressPage=null;
99
+ }else if(!args.prepare&&!args.acquire){
100
+ const context=inspectRelease(args.version);
101
+ const guide=`https://github.com/${context.repository.full_name}/blob/${context.documentationCommit}/docs/install-with-codex.md`;
72
102
  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,
73
103
  conversationNextStep:'本次 --inspect 仅查询;未下载、未安装,不自动接续写入。需要安装时由用户发起默认安装入口,并核对当前任务权限。',
74
104
  terminalBoundary:'普通终端无法创建或唤醒 Codex 对话;请在有相应权限的 Codex 任务发起安装。',
75
105
  acquisition:'尚未下载或验证运行归档。默认入口或 --prepare 执行匿名 GitHub 证明与完整性核验。'},null,2));
76
106
  }else{
77
107
  if(args.destination)plainPath(args.destination);console.log(`本次独占获取目录:${stage}`);
78
- const receipt=await acquireRelease(context,stage,{operationId:operation.operationId,onProgress:download=>updateOperation({download}),onPhase:phase=>updateOperation({phase,version:context.version,sourceCommit:context.sourceCommit})});console.log(JSON.stringify({status:'GITHUB_ACQUISITION_VERIFIED',...receipt,destinationIntent:args.destination}));
108
+ progressPage=await startProgressPage(()=>operation);
109
+ updateOperation({progressUrl:progressPage.url});
110
+ console.log(JSON.stringify({status:'FOUNDATION_ACQUISITION_PROGRESS_PAGE',url:progressPage.url,operationId:operation.operationId,next:'在 Codex 内置浏览器打开同次只读进度页,保持任务等待;不会自动打开系统浏览器。'}));
111
+ 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}));
79
112
  updateOperation({phase:'acquired',version:context.version,sourceCommit:context.sourceCommit});
80
- 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:'使用当前健康安装的稳定入口生成独立更新计划;本人确认后才更新。获取回执不是删除授权。'}));process.exit(0);}
113
+ 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);}
81
114
  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];
115
+ const supported=spawnSync(receipt.launcher,['--help'],{encoding:'utf8',env,timeout:15000});
82
116
  if(!args.destination){
83
- const supported=spawnSync(receipt.launcher,['--help'],{encoding:'utf8',env,timeout:15000});
84
117
  if(supported.status!==0||!supported.stdout.includes('--choose-destination'))fail('本次发行尚不支持页面选择目录;未启动安装。请由当前对话取得目录后使用明确的 --destination,或等待新版发行,不静默采用默认位置');
85
118
  }
86
119
  updateOperation({phase:'starting-confirmation-service',installationWrites:'unknown'});
87
- const child=spawn(receipt.launcher,['install',...(args.destination?['--destination',args.destination]:['--choose-destination']),'--browser','codex'],{stdio:['inherit','pipe','inherit'],env});
120
+ const child=spawn(receipt.launcher,['install',...(!args.destination&&supported.stdout?.includes('--journey-id')?['--journey-id',operation.operationId]:[]),...(args.destination?['--destination',args.destination]:['--choose-destination']),'--browser','codex'],{stdio:['inherit','pipe','inherit'],env});
88
121
  child.stdout.setEncoding('utf8');
89
122
  let pending='',json='';
90
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='';
@@ -95,11 +128,15 @@ try{
95
128
  process.once('SIGINT',int);process.once('SIGTERM',term);
96
129
  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);});
97
130
  if(ended.signal||ended.code===null)fail(`确认服务未正常返回;结果待核实,保留 ${stage},不要自动重试安装`);
131
+ if(operation.state==='awaiting-skill'){
132
+ try{updateOperation(await followSelectedSkill({operation,env,onChange:updateOperation}));}
133
+ 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 接续未完成;保留当前计划与结果,仅核实未完成步骤,不重装程序或重放确认。'});}
134
+ }
98
135
  if(!operation.terminal)updateOperation({state:'verification-required',terminal:true,childExitCode:ended.code,next:'服务退出不等于成功或取消;只读核验原 session 与 installed 状态,不自动重试'});
99
136
  if(operation.state==='completed'&&operation.installationRoot){
100
137
  const workbench=await openInstalledWorkbench(operation.installationRoot,env);
101
- updateOperation({phase:'finished',workbench,next:workbench.state==='ready'?'在 Codex 内置浏览器打开已就绪工作台;Skill 与项目仍未接入,按用户偏好另行询问':'安装已完成,但工作台未就绪;保留安装结果并只读检查'});
138
+ updateOperation({phase:'finished',workbench,next:workbench.state==='ready'?(operation.skillRegistered?'打开已安装工作台;Skill 文件已注册,新对话识别与项目接入另验':'打开已安装工作台;本次未注册 Skill,项目接入另行确认'):'安装已完成,但工作台未就绪;保留安装结果并只读检查'});
102
139
  }
103
- process.exitCode=ended.code;
140
+ process.exitCode=operation.state==='partial'?1:ended.code;
104
141
  }
105
- }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:'先核验旧工具进程已结束及同次结果;保留材料,不自动重试或重放安装确认'});const message=String(e.message).replace(/https?:\/\/\S+/g,'[已脱敏网址]').replace(/(?:github_pat_|ghp_|npm_)[A-Za-z0-9_]+/g,'[已脱敏凭据]');console.error(`错误:${message}`);process.exitCode=1;}
142
+ }catch(e){const message=String(e.message).replace(/https?:\/\/\S+/g,'[已脱敏网址]').replace(/(?:github_pat_|ghp_|npm_)[A-Za-z0-9_]+/g,'[已脱敏凭据]');updateOperation({state: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'].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();}
@@ -0,0 +1,24 @@
1
+ import {Worker} from 'node:worker_threads';
2
+
3
+ export function acquireInWorker({version,stage,operationId,onContext=()=>{},onPhase=()=>{},onProgress=()=>{}}) {
4
+ return new Promise((resolve,reject)=>{
5
+ const worker=new Worker(new URL('./acquisition-worker.mjs',import.meta.url),{workerData:{version,stage,operationId}});
6
+ let receipt,context,failure;
7
+ const interrupt=()=>{failure=Object.assign(Error('获取任务被中断;保留本次材料'),{code:'ACQUISITION_INTERRUPTED'});void worker.terminate();};
8
+ process.once('SIGINT',interrupt);process.once('SIGTERM',interrupt);
9
+ worker.on('message',event=>{
10
+ if(event.type==='context'){context=event.context;onContext(context);}
11
+ else if(event.type==='phase')onPhase(event.phase);
12
+ else if(event.type==='progress')onProgress(event.download);
13
+ else if(event.type==='complete')receipt=event.receipt;
14
+ else if(event.type==='failure')failure=Object.assign(Error(event.message),{code:event.code,diagnostic:event.diagnostic});
15
+ });
16
+ worker.on('error',error=>{failure=error;});
17
+ worker.once('exit',code=>{
18
+ process.off('SIGINT',interrupt);process.off('SIGTERM',interrupt);
19
+ if(failure)reject(failure);
20
+ else if(code!==0||!receipt||!context)reject(Error('获取进程未提供已验证结果;不启动运行文件'));
21
+ else resolve({context,receipt});
22
+ });
23
+ });
24
+ }
@@ -0,0 +1,17 @@
1
+ // Trusted npm code only. Network/proof routines contain synchronous calls;
2
+ // isolate those calls so the read-only progress server remains responsive.
3
+ import {parentPort, workerData} from 'node:worker_threads';
4
+ import {inspectRelease, acquireRelease} from './acquire.mjs';
5
+
6
+ try {
7
+ const context = inspectRelease(workerData.version);
8
+ parentPort.postMessage({type:'context',context});
9
+ const receipt = await acquireRelease(context, workerData.stage, {
10
+ operationId:workerData.operationId,
11
+ onPhase:phase=>parentPort.postMessage({type:'phase',phase}),
12
+ onProgress:download=>parentPort.postMessage({type:'progress',download}),
13
+ });
14
+ parentPort.postMessage({type:'complete',receipt});
15
+ } catch(error) {
16
+ parentPort.postMessage({type:'failure',code:error.code,diagnostic:error.diagnostic||null,message:String(error.message).replace(/https?:\/\/\S+/g,'[已脱敏网址]').replace(/(?:github_pat_|ghp_|npm_)[A-Za-z0-9_]+/g,'[已脱敏凭据]')});
17
+ }
@@ -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==='--version'||flag==='--destination'||flag==='--status'){if(!args.length||args[0].startsWith('--'))fail('参数缺值');result[flag.slice(2)]=args.shift();}
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,12 @@
1
+ // Presentation only. Planned steps never grant execution authority.
2
+ export function journeyView({id,kind='install',observations={},skillChoice='undecided',includeAcquisition=false,includeSelection=false,includeHealth=true,includeCleanup=false,includeSkillRemoval=false}) {
3
+ const labels={acquire:'获取并核验运行文件',select:'选择安装目录',program:({install:'安装程序',update:'更新程序',uninstall:'移除程序'})[kind]||'执行当前维护',health:'验证稳定入口',cleanup:'处理本次暂存',material:'准备对话能力材料',register:'注册到 Codex',remove:'移除 Codex 注册'};
4
+ const statusLabels={completed:'已完成',pending:'待确认',planned:'尚待处理',running:'进行中',executing:'进行中',consumed:'进行中',failed:'失败',cancelled:'已取消',expired:'已过期',skipped:'已跳过',undecided:'可选,待选择',retained:'已处理,保留部分材料','verification-required':'待核实'};
5
+ const keys=[...(includeAcquisition?['acquire']:[]),...(includeSelection?['select']:[]),...(includeSkillRemoval?['remove']:[]),'program',...(includeHealth?['health']:[]),...(includeCleanup?['cleanup']:[])];
6
+ if(kind!=='uninstall'&&(observations.material||observations.register||skillChoice==='selected'))keys.push('material','register');else if(kind!=='uninstall')keys.push('optional-skill');
7
+ const steps=keys.map(key=>{const supplied=observations[key],state=key==='optional-skill'?(skillChoice==='skipped'?'skipped':'undecided'):supplied?.state||'planned';return{id:`${id}:${key}`,key,title:labels[key]||'Codex 对话能力',state,label:statusLabels[state]||'待核实',evidence:supplied?.evidence||null,required:key!=='optional-skill'};});
8
+ const unresolved=steps.filter(s=>s.required&&!['completed','skipped','retained'].includes(s.state)),name=({install:'安装',update:'更新',uninstall:'卸载'})[kind]||'维护';
9
+ const program=observations.program?.state==='completed',trouble=unresolved.find(s=>['failed','cancelled','expired','verification-required'].includes(s.state));
10
+ const title=!unresolved.length?`${name}完成`:program?`程序已完成,${(trouble||unresolved[0]).title}${trouble?'未完成':'仍待处理'}`:trouble?`${name}未完成`:`${name}流程`;
11
+ return {schemaVersion:'1.1.0',id,kind,title,ended:unresolved.length===0,steps,remaining:unresolved.map(s=>s.title),summary:unresolved.length?`尚余:${unresolved.map(s=>s.title+'('+s.label+')').join('、')}`:kind==='uninstall'?'本次卸载步骤已完成。请查看卸载回执中的实际删除范围、保留文件与清理说明。':skillChoice==='undecided'&&!observations.register?'已确定操作已完成;对话能力可选,尚未选择。项目接入独立。':'本次已选择步骤均已处理;新任务发现及项目接入另验。',executionAuthority:false};
12
+ }
@@ -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. Even the first update uses the OLD stable launcher.
21
+ export async function followMaintenance({operation,kind,candidate,env,onChange}) {
22
+ if(!['update','uninstall'].includes(kind))throw Error('维护类型无效');
23
+ const client=installedClient(operation.installationRoot,env),{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
+ // On 0.2.10 inspect has no codexSkill field. File presence only requests an
30
+ // ownership-checked plan; the old engine validates its signed receipt/files.
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});
76
+ return {state:'completed',terminal:true,phase:'finished',skillRegistered:false,next:'更新及稳定入口健康核验完成;未新增 Skill,项目与用户数据按计划保留。'};
77
+ }
@@ -0,0 +1,62 @@
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) {
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};
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],{env:client.env,stdio:['ignore','pipe','pipe']});
20
+ let pending='',json='',finished=false;
21
+ return new Promise((resolve,reject)=>{
22
+ const stop=()=>child.kill('SIGTERM');
23
+ const finish=(error,value)=>{if(finished)return;finished=true;clearTimeout(timer);process.off('SIGINT',stop);process.off('SIGTERM',stop);
24
+ if(error||value?.responseFinished===true)child.kill('SIGTERM');
25
+ else { // Older managers report results before HTTP flush. Leave their
26
+ // existing bounded page lifetime intact rather than aborting the reply.
27
+ child.stdout.destroy();child.stderr.destroy();child.unref();
28
+ onChange({state:value.state,temporaryManager:{pid:child.pid,expiresAt:requested.expiresAt,reason:'old-manager-response-flush-unavailable'}});
29
+ }
30
+ error?reject(error):resolve(value);
31
+ };
32
+ const timer=setTimeout(()=>finish(Error('确认等待到期;保留同次计划记录')),Math.max(1,Math.min(16*60*1000,requested.expiresAt-Date.now()+1000)));
33
+ process.once('SIGINT',stop);process.once('SIGTERM',stop);
34
+ child.once('error',e=>finish(e));
35
+ child.once('close',()=>{if(!finished)finish(Error('页面进程退出,结果待核实;不自动重放'));});
36
+ child.stderr.on('data',()=>{});
37
+ child.stdout.setEncoding('utf8');
38
+ child.stdout.on('data',chunk=>{
39
+ pending+=chunk;
40
+ if(pending.length+json.length>4*1024*1024)return finish(Error('状态输出超过限制'));
41
+ let end;
42
+ while((end=pending.indexOf('\n'))>=0){
43
+ const line=pending.slice(0,end);pending=pending.slice(end+1);
44
+ if(!json&&!line.trim().startsWith('{'))continue;
45
+ json+=line+'\n';let event;try{event=JSON.parse(json);}catch{continue;}json='';
46
+ if(event.planRef!==requested.planRef)continue;
47
+ if(event.status==='AWAITING_FOUNDATION_UI_CONFIRMATION'){
48
+ const url=new URL(event.url);if(url.protocol!=='http:'||url.hostname!=='127.0.0.1'||url.username||url.password)return finish(Error('确认网址无效'));
49
+ onChange({state:'pending',confirmationUrl:url.href,sessionId:event.sessionId});
50
+ }
51
+ if(event.status==='FOUNDATION_OPERATION_STATE')onChange({state:event.state,sessionId:event.sessionId});
52
+ if(event.status==='FOUNDATION_OPERATION_RESULT'){
53
+ // The result comes from this exact installed process/session. Uninstall
54
+ // may already have removed the launcher, so its external manager receipt
55
+ // must remain the durable recovery source; do not execute deleted code.
56
+ if(!event.sessionId)return finish(Error('操作结果缺少会话身份'));
57
+ finish(null,event);
58
+ }
59
+ }
60
+ });
61
+ });
62
+ }
@@ -7,12 +7,15 @@ export function createOperation() {
7
7
  }
8
8
  // Observations from the verified runtime, never confirmation or execution authority.
9
9
  export function runtimeObservation(event) {
10
- if(event.status==='AWAITING_FOUNDATION_DIRECTORY_SELECTION')return {phase:'selecting-directory',selectionId:event.selectionId,installationWrites:'none'};
11
- if(['FOUNDATION_SELECTION_BOUND','AWAITING_FOUNDATION_UI_CONFIRMATION'].includes(event.status))return {phase:'awaiting-confirmation',sessionId:event.sessionId,planHash:event.planHash,installationWrites:'none'};
10
+ let confirmationUrl;
11
+ if(event.url){const url=new URL(event.url);if(url.protocol==='http:'&&url.hostname==='127.0.0.1'&&!url.username&&!url.password)confirmationUrl=url.href;}
12
+ if(event.status==='AWAITING_FOUNDATION_DIRECTORY_SELECTION')return {phase:'selecting-directory',selectionId:event.selectionId,journeyContext:event.journeyContext,installationWrites:'none',...(confirmationUrl?{confirmationUrl}:{})};
13
+ if(['FOUNDATION_SELECTION_BOUND','AWAITING_FOUNDATION_UI_CONFIRMATION'].includes(event.status))return {phase:'awaiting-confirmation',...(event.journeyContext?{journeyContext:event.journeyContext}:{}),sessionId:event.sessionId,planHash:event.planHash,installationWrites:'none',...(confirmationUrl?{confirmationUrl}:{})};
12
14
  if(event.status==='FOUNDATION_OPERATION_STATE'&&['executing','consumed'].includes(event.state))return {phase:'executing',sessionId:event.sessionId,installationWrites:'possible'};
13
15
  if(event.status==='BOOTSTRAP_OPERATION_ENDED'){
14
16
  const state=['completed','failed','cancelled','expired'].includes(event.state)?event.state:'verification-required';
15
- return {phase:'runtime-ended',state,terminal:state!=='verification-required',sessionId:event.sessionId,installationRoot:event.installationRoot,installationWrites:state==='completed'?'runtime-reported-installed':'unknown',runtimeHealth:event.result?.executableHealth||'unknown',next:'只读核验稳定 installed launcher 或同次 session 结果,不重放确认'};
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 结果,不重放确认'};
16
19
  }
17
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'};
18
21
  return null;
@@ -0,0 +1,57 @@
1
+ import http from 'node:http';
2
+ import crypto from 'node:crypto';
3
+ import {journeyView} from './journey-view.mjs';
4
+
5
+ export function downloadText(download) {
6
+ if(!download||!Number.isFinite(download.downloadedBytes)||download.downloadedBytes<0)return '';
7
+ const amount=n=>(n/1048576).toFixed(2)+' MiB';
8
+ const known=Number.isFinite(download.totalBytes)&&download.totalBytes>0;
9
+ return '已接收 '+amount(download.downloadedBytes)+(known?' / '+amount(download.totalBytes)+(download.downloadedBytes<=download.totalBytes?' · '+Math.floor(download.downloadedBytes/download.totalBytes*100)+'%':'(长度待核实)'):'(总量未知)');
10
+ }
11
+
12
+ export function acquisitionProgress(record) {
13
+ 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:'安装结果已返回'};
14
+ const terminal=record.terminal;
15
+ const acquired=['acquired','starting-confirmation-service','selecting-directory','directory-selection-ended','awaiting-confirmation','executing','runtime-ended','finished'].includes(record.phase);
16
+ const observations={acquire:{state:acquired?'completed':terminal?'failed':'running',evidence:record.resultFile}};
17
+ if(record.phase==='selecting-directory')observations.select={state:'pending'};
18
+ if(record.phase==='directory-selection-ended')observations.select={state:record.state==='cancelled-no-install'?'cancelled':record.state==='expired-no-install'?'expired':'verification-required',evidence:record.selectionId};
19
+ if(['awaiting-confirmation','executing','runtime-ended','finished'].includes(record.phase))observations.select={state:'completed',evidence:record.selectionId};
20
+ if(record.phase==='awaiting-confirmation')observations.program={state:'pending',evidence:record.sessionId};
21
+ if(record.phase==='executing')observations.program={state:'running',evidence:record.sessionId};
22
+ if(['runtime-ended','finished'].includes(record.phase))observations.program={state:record.programState||record.state,evidence:record.sessionId};
23
+ if(record.runtimeHealth==='passed')observations.health={state:'completed',evidence:record.sessionId};
24
+ if(record.programState==='completed'){
25
+ observations.acquire={state:'completed',evidence:record.resultFile};
26
+ observations.select={state:'completed',evidence:record.selectionId};
27
+ observations.program={state:'completed',evidence:record.sessionId};
28
+ }
29
+ for(const key of ['material','register'])if(record.skillSteps?.[key])observations[key]=record.skillSteps[key];
30
+ for(const key of ['remove','program'])if(record.maintenanceSteps?.[key])observations[key]=record.maintenanceSteps[key];
31
+ const kind=record.kind||'install';
32
+ 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'});
33
+ if(record.terminal&&record.state==='partial'){journey.ended=false;journey.title=record.programState==='completed'?'程序已完成,Codex 接入未完成':'本次操作尚未全部完成';journey.summary=record.next;}
34
+ const verb=kind==='uninstall'?'卸载':kind==='update'?'更新':'安装';
35
+ Object.assign(phases,{'awaiting-confirmation':'等待你确认'+verb,executing:'正在'+verb,'runtime-ended':'读取'+verb+'结果',finished:verb+'结果已返回'});
36
+ if(record.terminal&&record.state==='partial'&&record.programState==='completed'&&kind==='update')journey.title='程序已更新,Skill 尚未更新';
37
+ 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};
38
+ }
39
+
40
+ // A bounded, read-only loopback view over the existing in-memory operation.
41
+ // No POST, launch endpoint, confirmation authority or downloaded executable.
42
+ export async function startProgressPage(readRecord) {
43
+ const token=crypto.randomBytes(24).toString('hex'),base=`/${token}`,clients=new Set();
44
+ 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>`;
45
+ let origin;
46
+ const current=()=>JSON.stringify(acquisitionProgress(readRecord()));
47
+ const server=http.createServer((req,res)=>{
48
+ if(req.method!=='GET'||req.headers.host!==origin||req.headers.origin&&req.headers.origin!==`http://${origin}`){res.writeHead(403).end();return;}
49
+ 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'"};
50
+ if(req.url===base){res.writeHead(200,{...headers,'content-type':'text/html; charset=utf-8'}).end(html);}
51
+ 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));}
52
+ else{res.writeHead(404).end();}
53
+ });
54
+ await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(0,'127.0.0.1',resolve);});
55
+ origin=`127.0.0.1:${server.address().port}`;
56
+ return {url:`http://${origin}${base}`,publish(){for(const res of clients)res.write(`data: ${current()}\n\n`);},close(){for(const res of clients)res.end();clients.clear();return new Promise(resolve=>server.close(resolve));}};
57
+ }
@@ -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}) {
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&&current.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});
24
+ }
@@ -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}) {
10
+ if(operation.programState!=='completed'||operation.journeyContext?.skillChoice!=='selected')throw Error('Skill 接续需要已完成程序和明确所选意向');
11
+ const client=installedClient(operation.installationRoot,env),{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
+ }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@josephyulei/summon-foundation",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@josephyulei/summon-foundation",
9
- "version": "0.1.3",
9
+ "version": "0.1.5",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@sigstore/bundle": "4.0.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyulei/summon-foundation",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Prepare a verified Foundation release for an explicitly confirmed Codex installation",
5
5
  "type": "module",
6
6
  "bin": {"summon": "bin/summon.mjs"},