@josephyulei/summon-foundation 0.1.12 → 0.1.14
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/bin/summon.mjs +9 -2
- package/lib/acquire.mjs +2 -10
- package/lib/cli-options.mjs +1 -1
- package/lib/environment-check.mjs +1 -1
- package/lib/journey-control.mjs +24 -5
- package/lib/local-path.mjs +13 -0
- package/lib/maintenance-handoff.mjs +1 -1
- package/lib/manager-step.mjs +16 -1
- package/lib/operation-result.mjs +44 -0
- package/lib/progress-page.mjs +9 -2
- package/lib/resume-handoff.mjs +4 -3
- package/lib/single-page-style.mjs +1 -1
- package/lib/single-page-view.mjs +10 -7
- package/lib/skill-handoff.mjs +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +24 -6
package/bin/summon.mjs
CHANGED
|
@@ -20,7 +20,7 @@ summon foundation --inspect [--version x.y.z]
|
|
|
20
20
|
summon foundation --prepare --version x.y.z --destination /absolute/folder
|
|
21
21
|
summon foundation --acquire --version x.y.z
|
|
22
22
|
summon foundation --status /absolute/acquisition/operation-result.json
|
|
23
|
-
summon foundation --update --root /absolute/installed-folder --version x.y.z
|
|
23
|
+
summon foundation --update --root /absolute/installed-folder [--version x.y.z]
|
|
24
24
|
summon foundation --uninstall --root /absolute/installed-folder
|
|
25
25
|
summon foundation --resume /absolute/acquisition/operation-result.json
|
|
26
26
|
默认命令下载并验证正式发行,进入安装准备;未给目录时在安装页选择。不会静默安装或注册 Skill。
|
|
@@ -89,8 +89,15 @@ try{
|
|
|
89
89
|
if(environment.state!=='ready')fail('环境复检未通过:'+environment.state+';缺少 '+environment.missing.join('、')+';保留当前安装,不修改系统工具');
|
|
90
90
|
let candidate;
|
|
91
91
|
if(args.update){
|
|
92
|
-
assertUpdateEngineSupport(inspection,{root:args.root,stage});
|
|
92
|
+
if(args.version&&args.version!==current.version)assertUpdateEngineSupport(inspection,{root:args.root,stage});
|
|
93
93
|
updateOperation({phase:'discovering'});
|
|
94
|
+
const target=inspectRelease(args.version);args.version=target.version;
|
|
95
|
+
updateOperation({version:target.version,sourceCommit:target.sourceCommit});
|
|
96
|
+
if(target.version===current.version){
|
|
97
|
+
updateOperation({state:'completed',terminal:true,phase:'finished',programState:'unchanged',installationWrites:'none',next:'当前已是所选正式版本;未下载、更新或重新注册 Skill。'});
|
|
98
|
+
await progressPage.close();progressPage=null;process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
assertUpdateEngineSupport(inspection,{root:args.root,stage});
|
|
94
101
|
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,...(phase==='fetching-and-verifying-runtime'?{download:null}:{})})});
|
|
95
102
|
const directory=plainPath(path.dirname(receipt.launcher)),manifest=JSON.parse(fs.readFileSync(plainPath(path.join(directory,'manifest.json'))));
|
|
96
103
|
if(manifest.productVersion!==context.version)fail('获取版本与候选不一致');
|
package/lib/acquire.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {plainPath} from './local-path.mjs';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import crypto from 'node:crypto';
|
|
@@ -55,16 +56,7 @@ async function downloadLarge(url,total,onProgress) {
|
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
function regular(file) { const s=fs.lstatSync(file); if(!s.isFile()||s.isSymbolicLink())fail('不是普通文件');return s; }
|
|
58
|
-
export
|
|
59
|
-
if(!path.isAbsolute(file)||path.normalize(file)!==file)fail('路径必须是规范化绝对路径');
|
|
60
|
-
for(let cursor=file;cursor!==path.dirname(cursor);cursor=path.dirname(cursor)){
|
|
61
|
-
if(fs.existsSync(cursor)||fs.lstatSync(cursor,{throwIfNoEntry:false})){
|
|
62
|
-
const s=fs.lstatSync(cursor);if(s.isSymbolicLink())fail('路径包含符号链接');
|
|
63
|
-
if(fs.realpathSync(cursor)!==cursor)fail('路径真实位置不一致');
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return file;
|
|
67
|
-
}
|
|
59
|
+
export {plainPath} from './local-path.mjs';
|
|
68
60
|
export function inspectRelease(version) {
|
|
69
61
|
if(!Number.isSafeInteger(policy.repositoryId)||policy.repositoryId<=0)fail('公共仓库身份尚未绑定;此源码不是可用发行入口');
|
|
70
62
|
if(version!==undefined&&!semver.test(version))fail('版本必须是明确的 x.y.z');
|
package/lib/cli-options.mjs
CHANGED
|
@@ -16,7 +16,7 @@ export function parseSummonArgs(args){
|
|
|
16
16
|
if(seen.has('--status')){if(!result.status||seen.size!==1)fail('结果查询需要非空路径且不能与其他操作组合');return result;}
|
|
17
17
|
if(seen.has('--resume')){if(!result.resume||seen.size!==1)fail('恢复需要单一结果路径,不接受新目标或额外操作');return result;}
|
|
18
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
|
|
19
|
+
if(result.update&&result.uninstall||!result.root||result.inspect||result.acquire||result.prepare||result.destination||result.uninstall&&result.version)fail('维护需要单一动作、明确 --root;更新可用 --version 指定目标,否则查询并固定最新正式版本');
|
|
20
20
|
return result;
|
|
21
21
|
}
|
|
22
22
|
if(result.root)fail('--root 仅用于明确的更新或卸载');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
|
-
import {plainPath} from './
|
|
4
|
+
import {plainPath} from './local-path.mjs';
|
|
5
5
|
|
|
6
6
|
export function classifyEntryEnvironment({platform,arch,nodeVersion,tools}){
|
|
7
7
|
const [major,minor]=String(nodeVersion||'').split('.').map(Number);
|
package/lib/journey-control.mjs
CHANGED
|
@@ -11,12 +11,17 @@ function scopeFields(value){
|
|
|
11
11
|
// Only the exact child created by this invocation may supply an active view.
|
|
12
12
|
// Browser input cannot name a URL, route, transport token or future plan.
|
|
13
13
|
export function createJourneyControl(readRecord, changed) {
|
|
14
|
-
const children=new Map();let active=null,origin=null,choice=null,resolveChoice,rejectChoice,choiceTimer,viewRevision=0;
|
|
14
|
+
const children=new Map();let active=null,origin=null,choice=null,resolveChoice,rejectChoice,choiceTimer,viewRevision=0,submitting=0;
|
|
15
15
|
const headers=owner=>({origin,'content-type':'application/json','x-foundation-journey':owner.token,'x-foundation-operation':readRecord().operationId});
|
|
16
|
-
const request=async(owner,route,body)=>{
|
|
16
|
+
const request=async(owner,route,body,timeoutMs=body?120000:5000)=>{
|
|
17
17
|
if(owner.child.exitCode!==null||owner.child.signalCode)throw Error('确认服务已退出,结果待核实');
|
|
18
|
-
const
|
|
19
|
-
|
|
18
|
+
const ended=new AbortController();
|
|
19
|
+
const onClose=()=>ended.abort(Error('确认服务已退出,结果待核实'));
|
|
20
|
+
owner.child.once('close',onClose);
|
|
21
|
+
try {
|
|
22
|
+
const response=await fetch(owner.url+route,{method:body?'POST':'GET',headers:headers(owner),...(body?{body:JSON.stringify(body)}:{}),redirect:'error',signal:AbortSignal.any([ended.signal,AbortSignal.timeout(timeoutMs)])});
|
|
23
|
+
return {status:response.status,body:await response.json()};
|
|
24
|
+
} finally { owner.child.removeListener('close',onClose); }
|
|
20
25
|
};
|
|
21
26
|
return {
|
|
22
27
|
setOrigin(value){if(origin)throw Error('单页来源不能重绑定');origin=value;},
|
|
@@ -43,6 +48,14 @@ export function createJourneyControl(readRecord, changed) {
|
|
|
43
48
|
if(selected.status!==200){active.error=selected.body.message;choice=null;changed();}
|
|
44
49
|
}
|
|
45
50
|
},
|
|
51
|
+
pending(){return submitting>0;},
|
|
52
|
+
async reconcile(){
|
|
53
|
+
const a=active;if(!a)return Boolean(readRecord().terminal||readRecord().phase==='choosing-intent');
|
|
54
|
+
const result=await request(a.owner,a.type==='selection'?'__foundation/install/view':'__foundation/manager/view');
|
|
55
|
+
if(active!==a)return false;
|
|
56
|
+
if(result.status!==200||result.body.operationId!==readRecord().operationId||a.type==='manager'&&result.body.session?.sessionId!==a.view.session?.sessionId)throw Error('状态核对身份不符');
|
|
57
|
+
a.view=result.body;return true;
|
|
58
|
+
},
|
|
46
59
|
view(){return active?{type:active.type,...active.view,error:active.error||null}:null;},
|
|
47
60
|
waitChoice({timeoutMs=600000}={}){return new Promise((resolve,reject)=>{
|
|
48
61
|
if(resolveChoice)throw Error('目录选择已在等待');
|
|
@@ -51,6 +64,7 @@ export function createJourneyControl(readRecord, changed) {
|
|
|
51
64
|
changed();
|
|
52
65
|
});},
|
|
53
66
|
async submit(body){
|
|
67
|
+
submitting++;try{
|
|
54
68
|
if(body.operationId!==readRecord().operationId)throw Error('流程身份不匹配');
|
|
55
69
|
if(body.action==='cancel-intent'&&rejectChoice){
|
|
56
70
|
clearTimeout(choiceTimer);const reject=rejectChoice;resolveChoice=null;rejectChoice=null;
|
|
@@ -78,8 +92,13 @@ export function createJourneyControl(readRecord, changed) {
|
|
|
78
92
|
Object.assign(readRecord(),{environment});changed({environment});
|
|
79
93
|
if(environment.state!=='ready')throw Error('执行前环境复检未通过;没有提交确认,请核实当前工具环境');
|
|
80
94
|
}
|
|
81
|
-
|
|
95
|
+
// The installed manager may still verify and clean inputs after committing
|
|
96
|
+
// an update. Match observePlan's bounded lifetime; an unrelated 120s
|
|
97
|
+
// deadline can abort its final response after the mutation has succeeded.
|
|
98
|
+
const responseTimeout=Number.isSafeInteger(s.expiresAt)?Math.max(1,Math.min(16*60*1000,s.expiresAt-Date.now()+1000)):120000;
|
|
99
|
+
const result=await request(a.owner,'__foundation/manager/confirm',{operationId:body.operationId,sessionId:s.sessionId,planHash:s.planHash,managerNonce:body.managerNonce,action:body.action},responseTimeout);
|
|
82
100
|
if(active===a&&result.body.state)a.view.session={...s,...result.body};changed();return result;
|
|
101
|
+
}finally{submitting--;}
|
|
83
102
|
}
|
|
84
103
|
};
|
|
85
104
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const fail=message=>{throw Object.assign(Error('错误:'+message),{code:'ACQUISITION_VALIDATION_FAILED'})};
|
|
4
|
+
export function plainPath(file) {
|
|
5
|
+
if(!path.isAbsolute(file)||path.normalize(file)!==file)fail('路径必须是规范化绝对路径');
|
|
6
|
+
for(let cursor=file;cursor!==path.dirname(cursor);cursor=path.dirname(cursor)){
|
|
7
|
+
if(fs.existsSync(cursor)||fs.lstatSync(cursor,{throwIfNoEntry:false})){
|
|
8
|
+
const s=fs.lstatSync(cursor);if(s.isSymbolicLink())fail('路径包含符号链接');
|
|
9
|
+
if(fs.realpathSync(cursor)!==cursor)fail('路径真实位置不一致');
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return file;
|
|
13
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import {isDeepStrictEqual} from 'node:util';
|
|
4
|
-
import {plainPath} from './
|
|
4
|
+
import {plainPath} from './local-path.mjs';
|
|
5
5
|
import {installedClient,observePlan} from './manager-step.mjs';
|
|
6
6
|
import {followSelectedSkill} from './skill-handoff.mjs';
|
|
7
7
|
|
package/lib/manager-step.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {spawn,spawnSync} from 'node:child_process';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import {plainPath} from './local-path.mjs';
|
|
4
5
|
|
|
5
6
|
import {operationFailure,installedCommandFailure} from './operation-failure.mjs';
|
|
6
7
|
export {operationFailure,installedCommandFailure} from './operation-failure.mjs';
|
|
@@ -72,3 +73,17 @@ export async function observePlan(client,requested,onChange) {
|
|
|
72
73
|
});
|
|
73
74
|
});
|
|
74
75
|
}
|
|
76
|
+
|
|
77
|
+
// Resolve only the explicitly registered hint; never search for an executable.
|
|
78
|
+
export function discoverRegisteredInstallation(bindingFile,env){
|
|
79
|
+
const file=plainPath(bindingFile),stat=fs.lstatSync(file);
|
|
80
|
+
if(!stat.isFile()||stat.uid!==process.getuid()||stat.nlink!==1||stat.size>65536)throw Error('安装定位记录类型或归属无效');
|
|
81
|
+
const binding=JSON.parse(fs.readFileSync(file,'utf8'));
|
|
82
|
+
if(binding.schemaVersion!=='1.0.0'||binding.resolver!=='installed-current'||binding.authority!=='discovery-hint-only'||typeof binding.installId!=='string')throw Error('安装定位记录无效,不授予维护权限');
|
|
83
|
+
const client=installedClient(binding.installationRoot,env);
|
|
84
|
+
const inspection=client.call(['manager','inspect','--root',client.root]);
|
|
85
|
+
const current=inspection.installation?.current;
|
|
86
|
+
if(current?.identity?.installId!==binding.installId||inspection.bridge?.currentVersion!==current.version)throw Error('注册位置与当前安装身份或健康不一致');
|
|
87
|
+
if(inspection.supportedLifecycleOptions?.maintenance?.protocol!=='installed-maintenance/1')throw Error('当前安装缺少随包维护能力;请按该版本正式恢复说明处理,不替换旧引擎');
|
|
88
|
+
return {installationRoot:client.root,installId:binding.installId,launcher:client.launcher,currentVersion:current.version,candidateHash:current.candidateHash,inspection,executionAuthority:false};
|
|
89
|
+
}
|
package/lib/operation-result.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
|
+
import {plainPath} from './local-path.mjs';
|
|
4
5
|
|
|
5
6
|
export function createOperation() {
|
|
6
7
|
return {schemaVersion:'1.0.0',operationId:crypto.randomUUID(),pid:process.pid,startedAt:new Date().toISOString(),phase:'discovering',state:'running',terminal:false,installationWrites:'none',skillRegistered:false};
|
|
@@ -47,3 +48,46 @@ export function readOperation(file) {
|
|
|
47
48
|
const r=JSON.parse(fs.readFileSync(file));if(r.schemaVersion!=='1.0.0'||typeof r.operationId!=='string'||typeof r.terminal!=='boolean')throw Error('结果记录无效');
|
|
48
49
|
return {...r,...(!r.terminal?{state:'verification-required',note:'这是最后一次记录,不证明原进程仍活着。先查原工具句柄和同次记录;不自动重试。'}:{}),readOnly:true,executionAuthority:false};
|
|
49
50
|
}
|
|
51
|
+
|
|
52
|
+
// Bounded discovery only: these are the two product-owned account locations.
|
|
53
|
+
// Records are observations, never installation or execution authority.
|
|
54
|
+
export function discoverOperations({home,installationRoot,installId}) {
|
|
55
|
+
plainPath(home);plainPath(installationRoot);
|
|
56
|
+
if(!/^[a-zA-Z0-9-]+$/.test(installId||''))throw Error('安装身份格式无效');
|
|
57
|
+
const locations=[
|
|
58
|
+
[path.join(home,'Library','Application Support','Foundation Maintenance',installId),/^operation-[a-f0-9-]+$/],
|
|
59
|
+
[path.join(home,'Library','Caches','ai-product-foundation-kit-acquisition'),/^acquisition\.[a-zA-Z0-9]+$/]
|
|
60
|
+
];
|
|
61
|
+
const operations=[],unavailable=[];
|
|
62
|
+
for(const [directory,pattern] of locations){
|
|
63
|
+
plainPath(directory);if(!fs.existsSync(directory))continue;
|
|
64
|
+
const stat=fs.lstatSync(directory);
|
|
65
|
+
if(!stat.isDirectory()||stat.uid!==process.getuid()||(stat.mode&511)!==448)throw Error('操作记录目录归属或权限无效');
|
|
66
|
+
for(const name of fs.readdirSync(directory).filter(n=>pattern.test(n)).sort()){
|
|
67
|
+
const file=path.join(directory,name,'operation-result.json');
|
|
68
|
+
try{
|
|
69
|
+
plainPath(file);
|
|
70
|
+
const folder=fs.lstatSync(path.dirname(file));
|
|
71
|
+
if(!folder.isDirectory()||folder.uid!==process.getuid()||(folder.mode&511)!==448)throw Error('操作目录权限无效');
|
|
72
|
+
if(!fs.existsSync(file))continue;
|
|
73
|
+
const record=readOperation(file);
|
|
74
|
+
if(record.installationRoot!==installationRoot||record.installId!==installId)continue;
|
|
75
|
+
const kind=record.kind||(record.programState==='completed'?'install':null);
|
|
76
|
+
const completed=record.state==='completed'||kind==='uninstall'&&record.programState==='completed';
|
|
77
|
+
const remaining=completed?[]:kind==='uninstall'?[
|
|
78
|
+
...(record.maintenanceSteps?.remove?.state==='completed'?[]:['核对当前 Skill 归属;仅存在时另行确认移除']),
|
|
79
|
+
'重新确认程序卸载'
|
|
80
|
+
]:record.programState==='completed'&&record.journeyContext?.skillChoice==='selected'&&!record.skillRegistered&&record.skillSteps?.register?.state!=='completed'?['核对当前 Skill 材料和注册,仅为缺项请求新确认']:[];
|
|
81
|
+
operations.push({...record,kind,resultFile:file,completedActions:[...(record.programState==='completed'?['程序'+(kind==='uninstall'?'卸载':'安装或更新')]:[]),...(record.maintenanceSteps?.remove?.state==='completed'?['移除 Skill']:[])],remainingActions:remaining,resumable:remaining.length>0});
|
|
82
|
+
}catch(error){unavailable.push({file,reason:error.message});}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const superseded=new Set(operations.map(o=>o.resumes).filter(Boolean));
|
|
86
|
+
return {operations:operations.map(o=>({...o,resumable:o.resumable&&!superseded.has(o.operationId),superseded:superseded.has(o.operationId)})),unavailable,mutationPerformed:false,executionAuthority:false};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function selectResumableOperation(discovery,operationId=null){
|
|
90
|
+
const choices=discovery.operations.filter(o=>o.resumable&&(!operationId||o.operationId===operationId));
|
|
91
|
+
if(choices.length!==1)throw Error(choices.length?'有多个未完成操作,请从只读查询结果选择 --operation-id;不要猜测或重放批准':'未找到可续接操作;先只读核对当前状态');
|
|
92
|
+
return choices[0];
|
|
93
|
+
}
|
package/lib/progress-page.mjs
CHANGED
|
@@ -66,7 +66,7 @@ export async function startProgressPage(readRecord, {control = null} = {}) {
|
|
|
66
66
|
const current=()=>JSON.stringify(acquisitionProgress(readRecord()));
|
|
67
67
|
const server=http.createServer(async(req,res)=>{
|
|
68
68
|
if(req.headers.host!==origin||req.headers.origin&&req.headers.origin!==`http://${origin}`){res.writeHead(403).end();return;}
|
|
69
|
-
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;}
|
|
69
|
+
if(control&&req.method==='GET'&&req.url===base+'/state'){let reconciled=false;try{reconciled=await control.reconcile?.()===true;}catch{/* The child may exit between steps; retain the operation snapshot without claiming reconciliation. */}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(),reconciled,submissionPending:control.pending?.()===true}));return;}
|
|
70
70
|
if(control&&req.method==='POST'&&req.url===base+'/action'){
|
|
71
71
|
if(req.headers.origin!==`http://${origin}`||req.headers['content-type']!=='application/json'){res.writeHead(403).end();return;}
|
|
72
72
|
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;
|
|
@@ -84,6 +84,13 @@ export async function startProgressPage(readRecord, {control = null} = {}) {
|
|
|
84
84
|
// Allow the same page to receive the terminal snapshot before this bounded
|
|
85
85
|
// service exits. No polling or page acknowledgement authorizes a mutation.
|
|
86
86
|
if(control&&readRecord().terminal&&!terminalObserved)await new Promise(resolve=>{const timer=setTimeout(resolve,15000);terminalRead=()=>{clearTimeout(timer);resolve();};});
|
|
87
|
-
for(const res of clients)res.end();clients.clear();return new Promise(resolve=>
|
|
87
|
+
for(const res of clients)res.end();clients.clear();return new Promise(resolve=>{
|
|
88
|
+
// A browser may retain an unread/aborted response after the terminal
|
|
89
|
+
// snapshot. Stop accepting work, then bound connection draining so the
|
|
90
|
+
// completed CLI does not remain a live owner that blocks later recovery.
|
|
91
|
+
const drain=setTimeout(()=>server.closeAllConnections(),2000);
|
|
92
|
+
server.close(()=>{clearTimeout(drain);resolve();});
|
|
93
|
+
server.closeIdleConnections();
|
|
94
|
+
});
|
|
88
95
|
}};
|
|
89
96
|
}
|
package/lib/resume-handoff.mjs
CHANGED
|
@@ -3,22 +3,23 @@ import {installedClient} from './manager-step.mjs';
|
|
|
3
3
|
import {followSelectedSkill} from './skill-handoff.mjs';
|
|
4
4
|
|
|
5
5
|
// Explicit user-requested recovery, never an automatic replay of approval.
|
|
6
|
-
export async function resumeSkillHandoff({file,env,onChange,journeyControl=null}) {
|
|
6
|
+
export async function resumeSkillHandoff({file,env,onChange,journeyControl=null,newOperation=null}) {
|
|
7
7
|
const previous=readOperation(file);
|
|
8
8
|
if(previous.pid&&previous.pid!==process.pid){
|
|
9
9
|
try{process.kill(previous.pid,0);throw Error('原进程仍存在,先在原任务查询;不并发恢复');}
|
|
10
10
|
catch(e){if(e.code!=='ESRCH')throw e;}
|
|
11
11
|
}
|
|
12
|
+
if(previous.skillRegistered||previous.skillSteps?.register?.state==='completed')throw Error('旧记录已完成 Skill 注册;不通过恢复重新安装后来删除的 Skill');
|
|
12
13
|
if(previous.programState!=='completed'||previous.journeyContext?.skillChoice!=='selected'||!previous.installationRoot)throw Error('该记录不是程序完成后的 Skill 待续;先只读核实原操作,不重跑程序');
|
|
13
14
|
const client=installedClient(previous.installationRoot,env),current=client.call(['manager','inspect','--root',previous.installationRoot]).installation?.current;
|
|
14
15
|
if(!previous.installId||current?.identity?.installId!==previous.installId||previous.installedCandidateHash&¤t.candidateHash!==previous.installedCandidateHash)throw Error('恢复记录与当前安装不一致;需重新核实');
|
|
15
16
|
if(previous.currentPlanRef){
|
|
16
17
|
const status=client.call(['manager','status','--plan-ref',previous.currentPlanRef]);
|
|
17
|
-
if(['executing','consumed'].includes(status.state))throw Error('旧计划仅有执行中记录;先核实或按原恢复机制处理,不能自动重试');
|
|
18
|
+
if(['pending','executing','consumed'].includes(status.state))throw Error('旧计划仅有执行中记录;先核实或按原恢复机制处理,不能自动重试');
|
|
18
19
|
// Completed material is reused by capability status. Cancelled/expired
|
|
19
20
|
// confirmation is not reused: followSelectedSkill requests a fresh plan.
|
|
20
21
|
}
|
|
21
|
-
const operation={...previous,pid:process.pid,state:'awaiting-skill',terminal:false};
|
|
22
|
+
const operation={...previous,...(newOperation||{}),pid:process.pid,state:'awaiting-skill',terminal:false};
|
|
22
23
|
onChange(operation);
|
|
23
24
|
return followSelectedSkill({operation,env,onChange,journeyControl});
|
|
24
25
|
}
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
// Source SHA-256: e9949cc0d67e979b0297ca654eda1f163f2178b3338d4c4cf3ea9363e3cd8644
|
|
3
3
|
// Demo-only selectors remain inert; there are no demo controls in production.
|
|
4
4
|
// C5: download metadata wraps within the existing right-hand status slot.
|
|
5
|
-
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;min-width:0;overflow-wrap:anywhere}#foundation-flow-demo .step-state{font-size:12px;color:var(--fp-muted);max-width:48%;text-align:right;overflow-wrap:anywhere}#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}}";
|
|
5
|
+
export const singlePageStyle = "#foundation-flow-demo{max-width:960px;margin:24px auto;padding:0 16px;overflow-wrap:anywhere}#foundation-flow-demo button:focus-visible,#foundation-flow-demo input:focus-visible,#foundation-flow-demo summary:focus-visible{outline:3px solid var(--fp-accent);outline-offset:3px}#foundation-flow-demo h2{font-size:18px;font-weight:500}#foundation-flow-demo pre{white-space:pre-wrap;overflow-wrap:anywhere}#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;min-width:0;overflow-wrap:anywhere}#foundation-flow-demo .step-state{font-size:12px;color:var(--fp-muted);max-width:48%;text-align:right;overflow-wrap:anywhere}#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}}";
|
package/lib/single-page-view.mjs
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import {singlePageStyle} from './single-page-style.mjs';
|
|
2
2
|
|
|
3
3
|
// The approved product DOM/CSS; the client consumes facts, not demo timers.
|
|
4
|
-
export function singlePageDocument(csrf, {client = singlePageClient, projectPreparation = false} = {}) {
|
|
5
|
-
return `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Foundation ${projectPreparation ? "项目准备" : "安装与维护"}</title><style>body{margin:0}${singlePageStyle}</style><body><div id="foundation-flow-demo" aria-label="Foundation ${projectPreparation ? "项目准备" : "安装与维护"}"><main class="product"><header class="brand"><span class="brand-mark">F</span><span>Foundation</span><span class="private-label">${projectPreparation ? "项目准备" : "安装与维护"}</span></header
|
|
4
|
+
export function singlePageDocument(csrf, {client = singlePageClient, projectPreparation = false, content = null, script = null} = {}) {
|
|
5
|
+
return `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Foundation ${projectPreparation ? "项目准备" : "安装与维护"}</title><style>body{margin:0}${singlePageStyle}</style><body><div id="foundation-flow-demo" aria-label="Foundation ${projectPreparation ? "项目准备" : "安装与维护"}"><main class="product"><header class="brand"><span class="brand-mark">F</span><span>Foundation</span><span class="private-label">${projectPreparation ? "项目准备" : "安装与维护"}</span></header>${content ?? `<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>${script ?? `(${client.toString()})(${JSON.stringify(csrf).replaceAll("<", "\\u003c")});`}</script></body></html>`;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
function singlePageClient(csrf) {
|
|
9
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,formIdentity=null;
|
|
10
|
+
let last=null,busy=false,revision=null,stopped=false,formIdentity=null,submitted=null,posting=false;
|
|
11
11
|
const verbs={install:'安装',update:'更新',uninstall:'卸载'};
|
|
12
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
13
|
function show(p){
|
|
14
|
+
if(last&&p.record.operationId!==last.record.operationId)throw Error('流程身份变化');
|
|
15
|
+
if(submitted&&!posting&&p.record.operationId===submitted.operationId&&(p.record.terminal||p.reconciled===true&&!p.submissionPending&&p.view?.session?.sessionId===submitted.sessionId||p.view?.session?.sessionId&&p.view.session.sessionId!==submitted.sessionId||p.view?.session?.sessionId===submitted.sessionId&&p.view.session.state!=='pending'||submitted.choice&&(p.record.phase!=='choosing-intent'||p.reconciled===true&&!p.submissionPending))){busy=false;submitted=null;revision=null;}
|
|
14
16
|
last=p;const r=p.record,v=p.view,c=p.progress.content,kind=r.kind||'install',verb=verbs[kind],choice=r.phase==='choosing-intent'&&!r.terminal;
|
|
15
17
|
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
18
|
const stateLabels={failed:'本步失败',cancelled:'已取消',expired:'已过期','verification-required':'结果待核实',running:'正在执行',executing:'正在执行',consumed:'正在执行',undecided:'可选,待选择'};
|
|
@@ -31,7 +33,7 @@ function singlePageClient(csrf) {
|
|
|
31
33
|
q('#flow-eyebrow').textContent=r.terminal?'本次结果':executing?'正在执行':busy?'正在提交请求':v||choice?'等待你的操作':'正在进行';
|
|
32
34
|
q('#flow-description').textContent=c.description;
|
|
33
35
|
q('#flow-version').textContent='版本:'+(p.progress.version||'待核实');q('#flow-destination').textContent='程序位置:'+(r.installationRoot||r.destinationIntent||'等待选择');
|
|
34
|
-
q('#flow-count').textContent='已处理 '+count+' / '+steps.length+' 步';q('#flow-status').textContent=
|
|
36
|
+
q('#flow-count').textContent='已处理 '+count+' / '+steps.length+' 步';q('#flow-status').textContent=r.terminal?(r.state==='completed'?'本次流程已完成':'查看未完成项'):executing?'正在执行当前步骤':busy?'正在提交请求':('当前:'+(titles[active]||p.progress.phase));
|
|
35
37
|
const percent=steps.length?count/steps.length*100:0;q('#flow-bar').style.width=percent+'%';q('.track').setAttribute('aria-valuenow',String(Math.round(percent)));
|
|
36
38
|
const key=JSON.stringify([active,v?.session?.sessionId,v?.session?.state,v?.selectionId,r.terminal,steps.map(s=>s.state),v?.error]);
|
|
37
39
|
if(key!==revision&&!busy){
|
|
@@ -62,9 +64,10 @@ function singlePageClient(csrf) {
|
|
|
62
64
|
if(r.terminal)q('#flow-result').innerHTML='<h2>'+esc(c.title)+'</h2><p>'+esc(c.program)+'</p><p>Codex 对话功能:'+esc(c.skill)+'</p><p>项目:'+esc(c.project)+'</p><p class="hint">下一步:'+esc(c.next||p.progress.next)+'</p>';
|
|
63
65
|
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);
|
|
64
66
|
}
|
|
65
|
-
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})};
|
|
67
|
+
document.addEventListener('submit',async e=>{if(e.target.id!=='current-form')return;e.preventDefault();if(busy||!last)return;busy=true;posting=true;submitted={operationId:last.record.operationId,sessionId:last.view?.session?.sessionId,choice:last.record.phase==='choosing-intent'};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})};
|
|
66
68
|
|
|
67
|
-
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='
|
|
68
|
-
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?'结果服务已结束,上方保留最后核实结果。可在对话中按本次操作记录再次查询。':'连接已中断,保留最后核实结果;不自动重试操作。';const note=q('.current [data-download-connection]');if(note){note.textContent='连接中断,进度待核实;保留上次收到的数值。';q('.current .step-state').textContent=(last.progress.downloadStatus||last.progress.phase)+' · 待核实';}}}
|
|
69
|
+
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;submitted=null;form.querySelectorAll('button').forEach(b=>b.disabled=false);return;}busy=false;submitted=null;revision=null;await refresh();}catch{q('#connection').textContent='请求结果待核实,不自动重试。已收到的结果保留;使用“重新核对状态”查询同次流程。';}finally{posting=false;}});
|
|
70
|
+
async function refresh(){try{const response=await fetch(location.pathname+'/state');if(!response.ok)throw Error();show(await response.json());q('#connection').textContent=busy?'提交结果仍待核实;不会重复提交原确认。':'';}catch{q('#connection').textContent=last?.record?.terminal?'结果服务已结束,上方保留最后核实结果。可在对话中按本次操作记录再次查询。':'连接已中断,保留最后核实结果;不自动重试操作。';const note=q('.current [data-download-connection]');if(note){note.textContent='连接中断,进度待核实;保留上次收到的数值。';q('.current .step-state').textContent=(last.progress.downloadStatus||last.progress.phase)+' · 待核实';}}}
|
|
71
|
+
const read=document.createElement('button');read.id='read-status';read.type='button';read.textContent='重新核对状态';read.addEventListener('click',refresh);q('footer').prepend(read);
|
|
69
72
|
async function poll(){if(stopped)return;await refresh();if(!stopped)setTimeout(poll,750);}poll();window.addEventListener('pagehide',()=>{stopped=true},{once:true});
|
|
70
73
|
}
|
package/lib/skill-handoff.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import {installedClient,observePlan} from './manager-step.mjs';
|
|
4
|
-
import {plainPath} from './
|
|
4
|
+
import {plainPath} from './local-path.mjs';
|
|
5
5
|
|
|
6
6
|
// Coordinates existing exact-plan APIs. It never sends a confirmation request.
|
|
7
7
|
// The installed launcher resolves current on every call; downloaded code is not
|
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.14",
|
|
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.14",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@sigstore/bundle": "4.0.0",
|
package/package.json
CHANGED
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@josephyulei/summon-foundation",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "Prepare a verified Foundation release for an explicitly confirmed Codex installation",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
|
|
6
|
+
"bin": {
|
|
7
|
+
"summon": "bin/summon.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"lib/",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE",
|
|
14
|
+
"THIRD_PARTY_NOTICES.txt",
|
|
15
|
+
"npm-shrinkwrap.json"
|
|
16
|
+
],
|
|
8
17
|
"license": "MIT",
|
|
9
|
-
"engines": {
|
|
10
|
-
|
|
11
|
-
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=22.9.0"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/y15801172513-lang/foundation.git",
|
|
24
|
+
"directory": "distribution/summon-foundation"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public",
|
|
28
|
+
"registry": "https://registry.npmjs.org/"
|
|
29
|
+
},
|
|
12
30
|
"dependencies": {
|
|
13
31
|
"@sigstore/verify": "3.1.1",
|
|
14
32
|
"@sigstore/bundle": "4.0.0",
|