@harmonyos-arkts/d2h 0.1.20 → 0.1.21

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.
Files changed (151) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +40 -0
  3. package/agents/android-to-hmos-00-orchestrator.md +3 -315
  4. package/agents/d2h.md +53 -38
  5. package/contracts/workflow/artifacts.md +22 -0
  6. package/contracts/workflow/delivery.md +54 -0
  7. package/contracts/workflow/dispatch.md +26 -0
  8. package/contracts/workflow/execution.md +125 -0
  9. package/contracts/workflow/generation.md +40 -0
  10. package/contracts/workflow/orchestration.md +49 -0
  11. package/contracts/workflow/overview.md +58 -0
  12. package/contracts/workflow/plan.md +119 -0
  13. package/contracts/workflow/scope.md +151 -0
  14. package/dist/bin/install-opencode.js +34 -15
  15. package/dist/src/install-codex.js +131 -0
  16. package/dist/src/install-opencode.js +5 -5
  17. package/dist/src/workflow.js +54 -0
  18. package/docs/architecture-plan-guide.zh-CN.md +70 -0
  19. package/docs/incremental-spec-evaluation-design.md +670 -0
  20. package/docs/spec-plan-guide.md +226 -0
  21. package/docs/workflow-decoupling.md +59 -0
  22. package/opencode/interactive-tools/advance-progress-demo.mjs +21 -0
  23. package/opencode/interactive-tools/browser-host.ts +110 -0
  24. package/opencode/interactive-tools/core.ts +242 -0
  25. package/opencode/interactive-tools/demo-workflow.ts +55 -0
  26. package/opencode/interactive-tools/demo.mjs +37 -0
  27. package/opencode/interactive-tools/package-lock.json +1876 -0
  28. package/opencode/interactive-tools/package.json +23 -0
  29. package/opencode/interactive-tools/panel.ts +40 -0
  30. package/opencode/interactive-tools/prepare-demo.mjs +17 -0
  31. package/opencode/interactive-tools/prepare-progress-demo.mjs +13 -0
  32. package/opencode/interactive-tools/progress.ts +118 -0
  33. package/opencode/interactive-tools/server-legacy.mjs +2 -0
  34. package/opencode/interactive-tools/server.mjs +40 -0
  35. package/opencode/interactive-tools/tui.tsx +230 -0
  36. package/package.json +9 -1
  37. package/schemas/native-libs-analysis-report.schema.json +695 -0
  38. package/schemas/native-libs-verify-report.schema.json +566 -0
  39. package/schemas/spec-native-libs-shard.schema.json +110 -0
  40. package/skills/arkts-icon-sizing/SKILL.md +0 -10
  41. package/skills/cpp/d2h-native-library-analyse/SKILL.md +91 -0
  42. package/skills/cpp/d2h-native-library-analyse/scripts/analyse-android-so.py +630 -0
  43. package/skills/cpp/d2h-native-library-build/SKILL.md +403 -0
  44. package/skills/cpp/d2h-native-library-build/references/build-config-mapping.md +180 -0
  45. package/skills/cpp/d2h-native-library-build/references/jni-to-napi-mapping.md +74 -0
  46. package/skills/cpp/d2h-native-library-build/scripts/check-android-so.py +580 -0
  47. package/skills/cpp/d2h-native-library-verify/SKILL.md +111 -0
  48. package/skills/cpp/d2h-native-library-verify/references/demo-build-sign-run.md +67 -0
  49. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/AppScope/app.json5 +1 -0
  50. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/AppScope/resources/base/element/string.json +1 -0
  51. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/README.md +25 -0
  52. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/build-profile.json5 +24 -0
  53. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/build-profile.json5 +1 -0
  54. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/hvigorfile.ts +2 -0
  55. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/oh-package.json5 +1 -0
  56. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/ets/entryability/EntryAbility.ets +5 -0
  57. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/ets/pages/Index.ets +3 -0
  58. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/ets/pages/NativeVerifyDemo.ets +276 -0
  59. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/module.json5 +1 -0
  60. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/resources/base/element/color.json +1 -0
  61. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/resources/base/element/string.json +1 -0
  62. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/resources/base/profile/main_pages.json +1 -0
  63. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/ohosTest/ets/test/FFmpegVerify.test.ets +176 -0
  64. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/ohosTest/ets/test/List.test.ets +2 -0
  65. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/ohosTest/module.json5 +1 -0
  66. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/hvigor/hvigor-config.json5 +1 -0
  67. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/hvigorfile.ts +2 -0
  68. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/oh-package.json5 +9 -0
  69. package/skills/cpp/d2h-native-library-verify/references/demo-template.md +176 -0
  70. package/skills/cpp/d2h-native-library-verify/references/hypium-test-template.md +163 -0
  71. package/skills/cpp/d2h-native-library-verify/scripts/__pycache__/native_common.cpython-313.pyc +0 -0
  72. package/skills/cpp/d2h-native-library-verify/scripts/device-verify.py +304 -0
  73. package/skills/cpp/d2h-native-library-verify/scripts/native_common.py +341 -0
  74. package/skills/cpp/d2h-native-library-verify/scripts/verify-ohos-native.py +1099 -0
  75. package/skills/d2h-app-solution/SKILL.md +198 -0
  76. package/skills/d2h-app-solution/agents/openai.yaml +4 -0
  77. package/skills/d2h-app-solution/references/framework-evidence.md +117 -0
  78. package/skills/d2h-app-solution/references/hierarchy-rules.md +88 -0
  79. package/skills/d2h-app-solution/references/inventory-schema.md +305 -0
  80. package/skills/d2h-app-solution/references/technical-analysis.md +101 -0
  81. package/skills/d2h-app-solution/scripts/scan_repositories.py +350 -0
  82. package/skills/d2h-app-solution/scripts/validate_inventory.py +337 -0
  83. package/skills/d2h-architecture-plan/SKILL.md +35 -0
  84. package/skills/d2h-architecture-plan/references/directory-creation.md +43 -0
  85. package/skills/d2h-architecture-plan/references/hybrid-design.md +84 -0
  86. package/skills/d2h-architecture-plan/references/layout-reference.md +83 -0
  87. package/skills/d2h-architecture-plan/references/plan-contract.md +51 -0
  88. package/skills/d2h-architecture-plan/references/source-review.md +50 -0
  89. package/skills/d2h-cloud-knowledge/SKILL.md +28 -7
  90. package/skills/d2h-cloud-knowledge/kb_query.mjs +8 -0
  91. package/skills/d2h-dashboard/SKILL.md +56 -0
  92. package/skills/d2h-product-family-migration/SKILL.md +103 -0
  93. package/skills/d2h-product-family-migration/agents/openai.yaml +3 -0
  94. package/skills/d2h-product-family-migration/assets/incremental-plan.template.md +55 -0
  95. package/skills/d2h-product-family-migration/references/advanced/behavioral-verification.md +59 -0
  96. package/skills/d2h-product-family-migration/references/advanced/existing-skill-integration.md +30 -0
  97. package/skills/d2h-product-family-migration/references/advanced/packaging-and-verification.md +43 -0
  98. package/skills/d2h-product-family-migration/references/advanced/platform-probes.md +26 -0
  99. package/skills/d2h-product-family-migration/references/advanced/product-line-design.md +40 -0
  100. package/skills/d2h-product-family-migration/references/baseline-and-delta.md +50 -0
  101. package/skills/d2h-product-family-migration/tests/markdown-cases.md +76 -0
  102. package/skills/{d2h-page-develop → d2h-spec-develop}/SKILL.md +143 -126
  103. package/skills/d2h-spec-develop/agents/openai.yaml +7 -0
  104. package/skills/d2h-spec-develop/references/worker-workflow.md +26 -0
  105. package/skills/{d2h-page-spec-generate → d2h-spec-generate}/SKILL.md +24 -13
  106. package/skills/d2h-spec-generate/assets/spec-template.md +109 -0
  107. package/skills/d2h-spec-generate/references/from-plan.md +19 -0
  108. package/skills/d2h-spec-generate/references/spec-content.md +34 -0
  109. package/skills/{d2h-page-spec-generate → d2h-spec-generate}/scripts/build_batch_report.py +35 -84
  110. package/skills/d2h-spec-plan/SKILL.md +38 -0
  111. package/skills/d2h-spec-plan/agents/openai.yaml +4 -0
  112. package/skills/d2h-spec-plan/references/architecture-handoff.md +9 -0
  113. package/skills/d2h-spec-plan/references/plan-contract.md +13 -0
  114. package/skills/d2h-spec-plan/references/planning-method.md +46 -0
  115. package/skills/d2h-spec-plan/references/round-scope-assessment.md +49 -0
  116. package/skills/d2h-spec-plan/references/work-packets.md +61 -0
  117. package/skills/d2h-utg-ai-generate/references/click-paths-schema.json +2 -2
  118. package/skills/d2h-utg-ai-generate/references/page-schema.json +2 -2
  119. package/skills/d2h-utg-ai-generate/references/utg-schema.json +2 -2
  120. package/skills/d2h-utg-static-generate/SKILL.md +2 -2
  121. package/skills/managing-d2h-toolchain/environment-requirements.json +1 -1
  122. package/skills/managing-d2h-toolchain/toolchain.json +1 -1
  123. package/skills/preparing-migration-workspace/SKILL.md +32 -2
  124. package/workflow/__pycache__/artifact_bindings.cpython-313.pyc +0 -0
  125. package/workflow/__pycache__/spec_runtime.cpython-313.pyc +0 -0
  126. package/workflow/artifact_bindings.py +224 -0
  127. package/workflow/assets/dashboard.html +88 -0
  128. package/workflow/build_batch_report.py +8 -0
  129. package/workflow/collect_code_metrics.py +75 -0
  130. package/workflow/dashboard_overview.py +109 -0
  131. package/workflow/delivery_view.py +82 -0
  132. package/workflow/feature_plan.py +234 -0
  133. package/workflow/page_batches.py +223 -0
  134. package/workflow/spec_generation.py +120 -0
  135. package/workflow/spec_runtime.py +890 -0
  136. package/workflow/work_packets.py +85 -0
  137. package/skills/d2h-page-develop/agents/openai.yaml +0 -7
  138. package/skills/harmonyos-migration-playbook/SKILL.md +0 -83
  139. package/skills/harmonyos-migration-playbook/agents/openai.yaml +0 -7
  140. package/skills/harmonyos-migration-playbook/references/arkts-compile.md +0 -24
  141. package/skills/harmonyos-migration-playbook/references/harmony-runtime.md +0 -53
  142. package/skills/harmonyos-migration-playbook/references/lesson-lifecycle.md +0 -45
  143. package/skills/harmonyos-migration-playbook/references/protocol-e2e.md +0 -23
  144. package/skills/harmonyos-migration-playbook/references/ui-automation.md +0 -53
  145. package/skills/harmonyos-migration-playbook/references/windows-environment.md +0 -38
  146. package/skills/native-library-substitution/SKILL.md +0 -385
  147. package/skills/native-library-substitution/references/native-library-substitution.json +0 -56906
  148. package/skills/native-library-substitution/references/native-library-substitution.md +0 -163
  149. package/skills/porting-cmake-native-libraries/SKILL.md +0 -57
  150. package/skills/porting-cmake-native-libraries/references/platform-adaptation.md +0 -49
  151. package/skills/porting-cmake-native-libraries/references/report-and-build.md +0 -60
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@harmonyos-arkts/d2h-interactions-mvp",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "./server.mjs",
7
+ "exports": {
8
+ "./server": "./server.mjs",
9
+ "./tui": "./tui.tsx"
10
+ },
11
+ "engines": {
12
+ "opencode": ">=1.19.3 <2",
13
+ "node": ">=22.5 <25"
14
+ },
15
+ "dependencies": {
16
+ "@opencode-ai/plugin": "1.19.3",
17
+ "solid-js": "^1.9.12"
18
+ },
19
+ "devDependencies": {
20
+ "@opentui/core": "0.4.5",
21
+ "@opentui/solid": "0.4.5"
22
+ }
23
+ }
@@ -0,0 +1,40 @@
1
+ /** Shared trusted UI: VS Code Webview and the local review preview use the same renderer. */
2
+ export function interactionHtml(nonce: string): string {
3
+ return `<!doctype html>
4
+ <html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
5
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'nonce-${nonce}'; connect-src 'self'">
6
+ <title>d2h · 迁移工作台</title><style>
7
+ :root{color-scheme:dark;--bg:var(--vscode-editor-background,#14171c);--fg:var(--vscode-editor-foreground,#e9edf3);--muted:var(--vscode-descriptionForeground,#a7b0c0);--line:var(--vscode-panel-border,#313844);--accent:var(--vscode-button-background,#346dff);--card:var(--vscode-sideBar-background,#1b2028)}
8
+ *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.65 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}button,input,textarea{font:inherit}button{cursor:pointer}button:disabled{opacity:.45;cursor:default}button:focus-visible,input:focus-visible,textarea:focus-visible{outline:2px solid var(--accent);outline-offset:3px}header{padding:22px 30px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:14px;flex-wrap:wrap}.brand{font-size:23px;font-weight:650}.badge{font-size:12px;padding:3px 10px;background:#29436c;border-radius:20px}.layout{display:grid;grid-template-columns:220px minmax(0,1fr);min-height:620px}aside{padding:28px 18px;background:var(--card);border-right:1px solid var(--line)}.step{display:block;border:0;background:transparent;color:var(--muted);width:100%;text-align:left;padding:14px 12px;border-radius:8px;margin-bottom:6px}.step.active{background:var(--accent);color:white}.number{display:inline-flex;width:25px;height:25px;align-items:center;justify-content:center;margin-right:8px;border:1px solid currentColor;border-radius:50%}main{padding:28px 36px;max-width:1100px;width:100%;margin:auto}h1{font-size:27px;line-height:1.3;margin:8px 0 12px}h2{font-size:16px;margin:0 0 10px}.muted{color:var(--muted)}.eyebrow{font-size:12px;letter-spacing:1.5px;color:var(--muted)}.panel{margin:22px 0;padding:20px;background:var(--card);border:1px solid var(--line);border-radius:10px}.check{display:grid;grid-template-columns:1fr auto;gap:6px;padding:13px 0;border-bottom:1px solid var(--line)}.check:last-child{border-bottom:0}.detail{font-size:13px;color:var(--muted)}.choice{display:flex;gap:12px;padding:16px 0;border-bottom:1px solid var(--line);align-items:flex-start}.choice:last-child{border-bottom:0}.choice input{margin-top:6px;accent-color:var(--accent);width:17px;height:17px}.choice strong{display:block}.notice{padding:12px 15px;border-left:3px solid #e2ac5b;background:var(--card);margin:18px 0}.actions{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding-top:18px;border-top:1px solid var(--line)}button.primary{background:var(--accent);color:#fff;border:0;border-radius:7px;padding:11px 22px}.secondary{background:transparent;color:var(--fg);border:1px solid var(--line);border-radius:7px;padding:10px 17px}textarea{width:100%;min-height:90px;background:var(--bg);color:var(--fg);border:1px solid var(--line);border-radius:7px;padding:12px;resize:vertical}.binding{font-size:12px;overflow-wrap:anywhere}.status{margin-left:auto}.good{color:#87d9b0}pre{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit}#error{color:#ffb1a8;min-height:24px}.empty{padding:60px 0}.footer{font-size:12px;margin-top:20px;color:var(--muted)}@media(max-width:650px){.layout{grid-template-columns:1fr}aside{display:flex;flex-wrap:wrap;padding:10px;border-right:0}.step{width:auto;padding:8px}.number{display:none}main{padding:20px}header{padding:16px}.brand{font-size:20px}}
9
+ </style></head><body><header><span class="brand">d2h</span><span>迁移工作台</span><span class="badge" id="mode">交互式工具 MVP</span><select id="session" aria-label="原会话" style="max-width:320px;background:var(--card);color:var(--fg);padding:6px;border:1px solid var(--line)"></select><span class="status muted" id="state">正在连接宿主</span></header>
10
+ <div class="layout"><aside id="steps" aria-label="迁移交互步骤"></aside><main><div class="eyebrow">ANDROID → HARMONYOS</div><div id="content"></div><div id="error" role="alert"></div></main></div>
11
+ <script nonce="${nonce}">
12
+ (()=>{
13
+ const labels={environment:'确认环境',architecture:'选择架构',scope:'选择实施范围',report:'查看报告'};
14
+ const $=id=>document.getElementById(id);let snapshot=[],allItems=[],sessionId='',currentId=new URLSearchParams(location.search).get('request')||'',sending=false,lastSnapshot='',lastPending='';
15
+ const drafts={};let vscode;try{vscode=typeof acquireVsCodeApi==='function'?acquireVsCodeApi():undefined}catch{}
16
+ if(vscode){const state=vscode.getState();Object.assign(drafts,state?.drafts||{});sessionId=state?.sessionId||''}
17
+ const esc=value=>String(value??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
18
+ function send(message){if(vscode){vscode.postMessage(message)}else{fetch('action',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(message)}).then(async r=>{const data=await r.json();if(!r.ok)throw Error(data.error||'提交失败');if(data.error)throw Error(data.error);}).catch(e=>{sending=false;$('error').textContent=e.message;render()})}}
19
+ function render(){
20
+ const item=snapshot.find(x=>x.request.id===currentId)||snapshot.find(x=>x.status==='等待确认')||snapshot.at(-1);if(item)currentId=item.request.id;
21
+ $('steps').innerHTML=Object.entries(labels).map(([step,label],i)=>{const row=snapshot.filter(x=>x.request.step===step).at(-1);return '<button class="step '+(item?.request.step===step?'active':'')+'" data-step="'+step+'" '+(!row?'disabled':'')+'><span class="number">'+(i+1)+'</span>'+label+'<div class="detail">'+esc(row?.status||'尚未请求')+'</div></button>'}).join('');
22
+ if(!item){$('state').textContent='等待 Agent 请求';$('content').innerHTML='<div class="empty"><h1>由对话进入工作台</h1><p class="muted">Agent 调用 d2h_interact 后,这里展示环境、架构、范围或报告。提交结果返回原工具,无需手动输入“继续”。</p><button class="secondary" id="refresh">刷新请求</button></div>';return}
23
+ const r=item.request,done=item.status!=='等待确认';const saved=drafts[r.id]||{selected:r.step==='architecture'?r.workflow.architectures.filter(c=>c.selected).slice(0,1).map(c=>c.id):r.step==='scope'?r.workflow.scopes.filter(c=>c.selected).map(c=>c.id):[],reason:''};drafts[r.id]=saved;
24
+ $('mode').textContent=r.workflow.demo?'演示数据 · 不执行迁移':'交互式工具';$('state').textContent=item.status;
25
+ let body='';if(r.step==='environment'){body='<div class="panel">'+r.workflow.checks.map(c=>'<div class="check"><strong>'+esc(c.label)+'</strong><span class="'+(c.status==='ready'?'good':'muted')+'">'+esc({ready:'已检查',missing:'缺失',unverified:'待验证'}[c.status])+'</span><div class="detail">'+esc(c.detail)+'</div></div>').join('')+'</div>'}
26
+ if(r.step==='architecture'||r.step==='scope'){const choices=r.step==='architecture'?r.workflow.architectures:r.workflow.scopes;body='<div class="panel">'+choices.map(c=>'<label class="choice"><input name="choice" type="'+(r.step==='architecture'?'radio':'checkbox')+'" value="'+esc(c.id)+'" '+(saved.selected.includes(c.id)?'checked':'')+' '+(done?'disabled':'')+'><span><strong>'+esc(c.label)+'</strong><span class="detail">'+esc(c.detail)+'</span></span></label>').join('')+'</div>'}
27
+ if(r.step==='report'){body='<div class="panel"><pre>'+esc(r.workflow.report)+'</pre></div>'}
28
+ const blocked=r.step==='environment'&&r.workflow.checks.some(c=>c.status!=='ready');
29
+ $('content').innerHTML='<h1>'+esc(labels[r.step])+'</h1><p class="muted">'+esc(r.workflow.title)+'</p><div class="binding muted">原会话 '+esc(r.sessionId)+' · 输入版本 '+esc(r.revision.slice(0,12))+'</div>'+body+(r.step==='scope'?'<div class="notice">提交的是范围草稿。主 Agent 仍需校验依赖、修订计划并 reconcile;不会直接修改计划或启动开发。</div>':'')+(r.step==='report'?'<div class="notice">确认结束本次交互,不表示构建、设备测试或迁移验收通过。</div>':'')+'<label for="reason">'+(r.step==='scope'?'选择原因(必填)':'补充说明(可选)')+'</label><textarea id="reason" '+(done?'disabled':'')+' placeholder="保留你的选择、约束和理由">'+esc(saved.reason)+'</textarea>'+(blocked?'<p class="notice">仍有缺失或未验证项,不能确认通过。可取消后更新检查报告,再发起确认。</p>':'')+'<div class="actions"><button class="primary" id="submit" '+(done||blocked||sending?'disabled':'')+'>'+(done?esc(item.status):sending?'正在提交…':r.step==='report'?'确认并返回对话':'提交并继续原会话')+'</button><button class="secondary" id="cancel" '+(done||sending?'disabled':'')+'>取消本次交互</button></div><div class="footer">关闭窗口可稍后重开;取消才会返回取消结果。原工具取消或超时后,本请求失效。</div>';
30
+ }
31
+ function save(){const r=snapshot.find(x=>x.request.id===currentId)?.request;if(!r)return;drafts[r.id]={selected:[...document.querySelectorAll('input[name="choice"]:checked')].map(n=>n.value),reason:$('reason')?.value||''};vscode?.setState({drafts,sessionId})}
32
+ function bindSession(){const ids=[...new Set(allItems.map(x=>x.request.sessionId))];if(!ids.includes(sessionId))sessionId=allItems.find(x=>x.status==='等待确认')?.request.sessionId||ids.at(-1)||'';snapshot=allItems.filter(x=>x.request.sessionId===sessionId);$('session').innerHTML=ids.map(id=>'<option value="'+esc(id)+'" '+(id===sessionId?'selected':'')+'>'+esc(id)+'</option>').join('')}
33
+ $('session').addEventListener('change',()=>{save();sessionId=$('session').value;currentId='';lastPending='';bindSession();render();vscode?.setState({drafts,sessionId})});
34
+ document.addEventListener('input',save);document.addEventListener('change',save);
35
+ document.addEventListener('click',e=>{const b=e.target.closest('button');if(!b||b.disabled)return;if(b.dataset.step){save();currentId=snapshot.filter(x=>x.request.step===b.dataset.step).at(-1)?.request.id;render();return}if(b.id==='refresh'){send({type:'ready'});return}if(['submit','cancel'].includes(b.id)){save();const item=snapshot.find(x=>x.request.id===currentId),r=item.request,d=drafts[r.id];if(b.id==='submit'&&r.step==='scope'&&!d.reason.trim()){$('error').textContent='请填写选择原因';return}sending=true;$('error').textContent='';send({type:'submit',id:r.id,revision:r.revision,action:b.id==='cancel'?'cancel':'accept',selected:b.id==='cancel'?[]:d.selected,reason:d.reason});render()}});
36
+ function receive(data){if(data.type==='snapshot'){const signature=JSON.stringify(data.items);if(signature===lastSnapshot)return;lastSnapshot=signature;allItems=data.items;bindSession();const next=(snapshot.find(x=>x.request.id===currentId&&x.status==='等待确认')||snapshot.find(x=>x.status==='等待确认'))?.request.id;if(next&&next!==lastPending){currentId=next;lastPending=next}sending=false;render()}if(data.type==='error'){sending=false;render();$('error').textContent=data.message}}
37
+ window.addEventListener('message',e=>receive(e.data));if(!vscode){async function poll(){try{const r=await fetch('snapshot');receive(await r.json())}catch(e){$('state').textContent='连接中断'}setTimeout(poll,700)}poll()}else{send({type:'ready'})}render();
38
+ })();
39
+ </script></body></html>`;
40
+ }
@@ -0,0 +1,17 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath, pathToFileURL } from 'node:url';
4
+ import { demoWorkflow } from '../../dist/opencode/interactive-tools/demo-workflow.js';
5
+
6
+ const root = path.dirname(fileURLToPath(import.meta.url));
7
+ const project = path.resolve(process.argv[2] || path.join(root, '../../../validation/interactive-tools-mvp', `opencode-${Date.now()}`));
8
+ const config = path.join(project, '.opencode');
9
+ fs.mkdirSync(config, { recursive: true });
10
+ for (const [file, data] of [
11
+ [path.join(config, 'opencode.json'), { $schema: 'https://opencode.ai/config.json', plugin: [pathToFileURL(path.join(root, 'server.mjs')).href] }],
12
+ [path.join(config, 'tui.json'), { $schema: 'https://opencode.ai/tui.json', plugin: [path.join(root, 'tui.tsx')] }],
13
+ [path.join(project, 'workflow.json'), demoWorkflow],
14
+ ]) { fs.writeFileSync(file, JSON.stringify(data, null, 2), { flag: 'wx' }); }
15
+ console.log(project);
16
+ console.log('在这个目录运行 opencode,再输入 /d2h-demo 可体验四步交互(不调用模型)。');
17
+ console.log('真实 Agent 工具调用:d2h_interact({ workflowPath: <workflow.json 绝对路径>, step: environment|architecture|scope|report })');
@@ -0,0 +1,13 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { execFileSync } from 'node:child_process';
5
+
6
+ // Development checkout fixture, never used by the real progress reader.
7
+ const root = path.dirname(fileURLToPath(import.meta.url));
8
+ const project = path.resolve(process.argv[2] || path.join(root, '../../../validation/interactive-tools-mvp', `progress-${Date.now()}`));
9
+ execFileSync(process.execPath, [path.join(root, 'prepare-demo.mjs'), project], { stdio: 'ignore' });
10
+ const plan = project + '-migration/.migration/page-specs';
11
+ execFileSync(process.platform === 'win32' ? 'python' : 'python3', [path.join(root, '../../scripts/tests/interactive-progress-fixture.py'), plan], { stdio: 'inherit' });
12
+ fs.writeFileSync(path.join(project, '.opencode/d2h-progress.json'), JSON.stringify({ schema: 'd2h-progress-config/v1', plan: path.join(plan, 'pages.json'), demo: true }, null, 2), { flag: 'wx' });
13
+ console.log(`演示项目:${project}\n启动:opencode "${project}"\n进入后:/d2h-progress-demo\n侧栏隐藏时:/d2h-progress\n预期:Spec 实现 1/3,验收 0/3;全部是演示数据,不调用模型。`);
@@ -0,0 +1,118 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { execFile } from 'node:child_process';
5
+ import { interactionRoot } from './core.ts';
6
+
7
+ type Ratio = { total: number; accepted: number; percent: number | null; unplanned: number };
8
+ export interface DashboardSnapshot {
9
+ schema: 'd2h-dashboard/v1'; captured_at: string;
10
+ activity: { label: string };
11
+ plan: { milestone: string; specs: Array<{ spec_id: string; scope?: string[] }> };
12
+ execution: { candidate: string | null; tasks: Record<string, { implementation: string }> };
13
+ derived: {
14
+ relevant_specs: string[]; running: string[];
15
+ tasks: Record<string, { accepted: boolean; blocked_by: string[]; external_blockers: string[] }>;
16
+ validation_debt: { specs: string[] };
17
+ delivery_progress: { selected: Ratio | null; full: Ratio | null };
18
+ };
19
+ }
20
+ export interface ProgressSummary {
21
+ phase: string; milestone: string; total: number; implemented: number; accepted: number;
22
+ running: string[]; blocked: number; validationDebt: number; selected: Ratio | null; full: Ratio | null;
23
+ }
24
+ export interface ProgressState {
25
+ status: 'loading' | 'ready' | 'missing' | 'error';
26
+ snapshot?: DashboardSnapshot; summary?: ProgressSummary; demo?: boolean;
27
+ checkedAt?: string; error?: string;
28
+ }
29
+ function readJson(file: string): any {
30
+ const stat = fs.lstatSync(file);
31
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 16 * 1024 * 1024) { throw new Error('进度输入必须为小于 16 MiB 的普通 JSON 文件'); }
32
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
33
+ }
34
+ export function workflowAsset(relative: string): string {
35
+ for (const prefix of ['../../workflow/', '../../../workflow/']) {
36
+ const file = fileURLToPath(new URL(prefix + relative, import.meta.url));
37
+ if (fs.existsSync(file)) { return file; }
38
+ }
39
+ throw new Error('未找到 D2H 公共 workflow 工具,请重新安装完整 D2H 包');
40
+ }
41
+ export function summarizeProgress(data: DashboardSnapshot): ProgressSummary {
42
+ if (data.schema !== 'd2h-dashboard/v1' || !data.activity || !Array.isArray(data.derived?.relevant_specs)
43
+ || !data.execution?.tasks || !data.derived.tasks || !Array.isArray(data.derived.running)) { throw new Error('无效的 d2h 进度快照'); }
44
+ const ids = data.derived.relevant_specs;
45
+ if (new Set(ids).size !== ids.length || ids.some(id => !data.execution.tasks[id] || !data.derived.tasks[id])) { throw new Error('进度 Spec 身份不一致'); }
46
+ return {
47
+ phase: data.activity.label, milestone: data.plan.milestone, total: ids.length,
48
+ implemented: ids.filter(id => data.execution.tasks[id].implementation === 'implemented').length,
49
+ accepted: ids.filter(id => data.derived.tasks[id].accepted === true).length,
50
+ running: data.derived.running.map(id => `${id} · ${data.plan.specs.find(spec => spec.spec_id === id)?.scope?.[0] ?? id}`),
51
+ blocked: ids.filter(id => data.derived.tasks[id].blocked_by.length || data.derived.tasks[id].external_blockers.length).length,
52
+ validationDebt: data.derived.validation_debt.specs.length,
53
+ selected: data.derived.delivery_progress.selected, full: data.derived.delivery_progress.full,
54
+ };
55
+ }
56
+ export async function loadProgress(project: string, signal?: AbortSignal): Promise<ProgressState | undefined> {
57
+ const root = fs.realpathSync(project);
58
+ const configFile = path.join(root, '.opencode', 'd2h-progress.json');
59
+ const configured = fs.existsSync(configFile);
60
+ const config = configured ? readJson(configFile) : undefined;
61
+ if (configured && (!config || typeof config !== 'object' || Array.isArray(config)
62
+ || config.schema !== 'd2h-progress-config/v1' || typeof config.plan !== 'string'
63
+ || (config.demo !== undefined && typeof config.demo !== 'boolean'))) { throw new Error('无效的 .opencode/d2h-progress.json'); }
64
+ const candidates = config ? [path.resolve(root, config.plan)] : [
65
+ path.join(path.dirname(interactionRoot(root)), 'page-specs', 'pages.json'),
66
+ path.join(root, '.migration', 'page-specs', 'pages.json'),
67
+ ].filter(file => fs.existsSync(file));
68
+ if (!candidates.length) { return undefined; }
69
+ if (candidates.length > 1) { throw new Error('发现多个计划目录,请在 .opencode/d2h-progress.json 指定 plan'); }
70
+ const planFile = candidates[0]; const plan = readJson(planFile);
71
+ const source = config?.features ?? plan.feature_source?.path;
72
+ if (typeof source !== 'string' || !source) { throw new Error('计划缺少 feature_source.path'); }
73
+ const features = path.resolve(root, source);
74
+ const execution = config?.execution ? path.resolve(root, config.execution) : path.join(path.dirname(planFile), 'execution.json');
75
+ readJson(features); readJson(execution);
76
+ const stdout = await new Promise<string>((resolve, reject) => {
77
+ execFile(process.platform === 'win32' ? 'python' : 'python3', [
78
+ workflowAsset('spec_runtime.py'), 'dashboard', '--plan', planFile,
79
+ '--features', features, '--execution', execution,
80
+ ], { cwd: root, encoding: 'utf8', timeout: 10_000, maxBuffer: 16 * 1024 * 1024, signal, windowsHide: true },
81
+ (error, output, stderr) => {
82
+ if (error) { reject(new Error(`进度派生失败:${String(stderr || output || error.message).slice(0, 800)}`)); }
83
+ else { resolve(output); }
84
+ });
85
+ });
86
+ const snapshot = JSON.parse(stdout) as DashboardSnapshot;
87
+ return { status: 'ready', snapshot, summary: summarizeProgress(snapshot), demo: config?.demo === true, checkedAt: new Date().toISOString() };
88
+ }
89
+
90
+ /** Keep the last valid projection visible, but never present it as fresh after an error. */
91
+ export class ProgressReader {
92
+ current: ProgressState = { status: 'loading' };
93
+ private pending?: Promise<ProgressState>;
94
+ constructor(private readonly project: string, private readonly signal?: AbortSignal) {}
95
+ refresh(): Promise<ProgressState> {
96
+ if (this.pending) { return this.pending; }
97
+ this.pending = this.update().finally(() => { this.pending = undefined; });
98
+ return this.pending;
99
+ }
100
+ private async update(): Promise<ProgressState> {
101
+ try {
102
+ const next = await loadProgress(this.project, this.signal);
103
+ this.current = next ?? { ...this.current, status: this.current.snapshot ? 'error' : 'missing', error: '尚未发现 pages.json;请建立计划或配置 d2h-progress.json' };
104
+ } catch (error) {
105
+ this.current = { ...this.current, status: 'error', error: error instanceof Error ? error.message : String(error) };
106
+ }
107
+ return this.current;
108
+ }
109
+ }
110
+
111
+ export function progressHtml(state: ProgressState): string {
112
+ if (!state.snapshot) { throw new Error(state.error ?? '尚无可展示的计划进度'); }
113
+ let html = fs.readFileSync(workflowAsset('assets/dashboard.html'), 'utf8');
114
+ const payload = JSON.stringify(state.snapshot).replaceAll('<', '\\u003c').replaceAll('\u2028', '\\u2028').replaceAll('\u2029', '\\u2029');
115
+ html = html.replace('/*__D2H_SNAPSHOT__*/null', () => payload);
116
+ if (state.demo) { html = html.replace('<body>', '<body><div style="position:relative;z-index:5;text-align:center;background:#fff3cd">演示数据 · 不代表真实应用开发或验收结果</div>'); }
117
+ return html;
118
+ }
@@ -0,0 +1,2 @@
1
+ // Entry point for older harmony-acp/OpenCode loaders that enumerate plugin functions.
2
+ export { D2hInteractiveTools as default } from './server.mjs';
@@ -0,0 +1,40 @@
1
+ import { tool } from '@opencode-ai/plugin';
2
+ import { interactionCapability, requestInteraction, scopeChange } from './core.ts';
3
+
4
+ /** V1 server plugin. UI is supplied by a separate host adapter; this tool waits in the original turn. */
5
+ export const D2hInteractiveTools = async () => ({
6
+ tool: {
7
+ d2h_interact_capabilities: tool({
8
+ description: '只读检查当前项目、会话是否有活跃交互界面;unsupported 时跳过看板询问,改用当前对话。不是用户授权。',
9
+ args: {},
10
+ async execute(_args, context) {
11
+ return JSON.stringify(interactionCapability(context.directory, context.sessionID));
12
+ },
13
+ }),
14
+ d2h_interact: tool({
15
+ description: '在宿主工作台请求用户确认环境、选择架构/实施范围或查看报告。仅在当前会话有活跃界面时读取工作流并等待用户;界面不可用立即返回 unsupported,调用方改用当前对话。取消时停止当前流程。不会修改计划或开发代码。',
16
+ args: {
17
+ workflowPath: tool.schema.string().describe('已有 d2h-interactive-workflow/v1 JSON 的绝对路径;实际数据必须来自当前报告,不得伪造 ready 状态'),
18
+ step: tool.schema.enum(['environment', 'architecture', 'scope', 'report']),
19
+ },
20
+ async execute(args, context) {
21
+ const outcome = await requestInteraction(context.directory, args.workflowPath, context.sessionID,
22
+ context.messageID, args.step, context.abort, request => {
23
+ context.metadata({ title: `d2h · ${args.step} · 等待用户`, metadata: { d2hInteractionId: request.id } });
24
+ });
25
+ if (outcome.status === 'unsupported') {
26
+ return JSON.stringify({ ...outcome, message: '跳过看板交互,改用当前会话询问;已有授权可复用,普通报告无需确认。未获得任何新的用户授权。' });
27
+ }
28
+ const { request, decision: result } = outcome;
29
+ const choices = args.step === 'architecture' ? request.workflow.architectures : args.step === 'scope' ? request.workflow.scopes : [];
30
+ return JSON.stringify({ ...result, step: args.step,
31
+ selectedOptions: choices.filter(choice => result.selected.includes(choice.id)),
32
+ ...(args.step === 'scope' && result.action === 'accept' ? { scopeDraft: scopeChange(request, result) } : {}),
33
+ ...(args.step === 'report' && result.action === 'accept' ? { report: request.workflow.report } : {}),
34
+ });
35
+ },
36
+ }),
37
+ },
38
+ });
39
+
40
+ export default { id: 'd2h.interactions.server', server: D2hInteractiveTools };
@@ -0,0 +1,230 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { createSignal, For, Show, onCleanup } from 'solid-js';
5
+ import type { TuiPluginModule } from '@opencode-ai/plugin/tui';
6
+ import { listInteractions, interactionStatus, submitDecision, stepLabels } from './core.ts';
7
+ import type { Interaction, Decision } from './core.ts';
8
+ import { demoWorkflow, demoReport } from './demo-workflow.ts';
9
+ import { createInteraction, waitForDecision, scopeChange, registerInteractionHost } from './core.ts';
10
+ import { startInteractionBrowser, openInteractionBrowser } from './browser-host.ts';
11
+ import { ProgressReader } from './progress.ts';
12
+
13
+ const plugin: TuiPluginModule = {
14
+ id: 'd2h.interactions',
15
+ async tui(api) {
16
+ if (!/^1\.19\./.test(api.app.version)) {
17
+ api.ui.toast({ variant: 'error', message: 'd2h MVP 仅验证 OpenCode 1.19.x;未启用交互界面' });
18
+ return;
19
+ }
20
+ const project = api.state.path.directory;
21
+ const [active, setActive] = createSignal<Interaction>();
22
+ let previousRoute = api.route.current;
23
+ const [rows, setRows] = createSignal<Interaction[]>([]);
24
+ const [selected, setSelected] = createSignal<string[]>([]);
25
+ const [reason, setReason] = createSignal('');
26
+ const [cursor, setCursor] = createSignal(0);
27
+ const [editingReason, setEditingReason] = createSignal(false);
28
+ const [error, setError] = createSignal('');
29
+ const [tick, setTick] = createSignal(0);
30
+ const progressReader = new ProgressReader(project, api.lifecycle.signal);
31
+ const [progress, setProgress] = createSignal(progressReader.current);
32
+ const refreshProgress = async () => {
33
+ const next = await progressReader.refresh();
34
+ if (!api.lifecycle.signal.aborted) { setProgress({ ...next }); }
35
+ };
36
+ let browser: ReturnType<typeof startInteractionBrowser> | undefined;
37
+ const openBrowser = async () => {
38
+ const request = active();
39
+ if (!request) { setError('请先打开一个交互请求'); return; }
40
+ try {
41
+ browser ??= startInteractionBrowser().catch(cause => { browser = undefined; throw cause; });
42
+ const host = await browser;
43
+ if (api.lifecycle.signal.aborted) { return; }
44
+ await openInteractionBrowser(host.urlFor(request));
45
+ api.ui.toast({ variant: 'info', message: '已打开当前会话的网页工作台;提交后原流程继续' });
46
+ } catch (cause) { setError(`打开网页失败:${String(cause)}`); }
47
+ };
48
+ api.lifecycle.onDispose(async () => { if (browser) { await (await browser).close(); } });
49
+ const openProgress = async () => {
50
+ await refreshProgress();
51
+ if (!progressReader.current.snapshot) { api.ui.toast({ variant: 'info', message: progressReader.current.error ?? '尚无计划进度' }); return; }
52
+ try {
53
+ browser ??= startInteractionBrowser().catch(cause => { browser = undefined; throw cause; });
54
+ const host = await browser;
55
+ if (!api.lifecycle.signal.aborted) { await openInteractionBrowser(host.progressUrlFor(() => progressReader.current)); }
56
+ } catch (cause) { api.ui.toast({ variant: 'error', message: String(cause) }); }
57
+ };
58
+ const currentSession = () => api.route.current.name === 'session' ? String(api.route.current.params?.sessionID ?? '') : '';
59
+ const refresh = () => { setRows(listInteractions(project)); setTick(value => value + 1); };
60
+ const options = () => active()?.step === 'architecture' ? active()!.workflow.architectures : active()?.step === 'scope' ? active()!.workflow.scopes : [];
61
+ const toggleOption = (index: number) => {
62
+ const request = active(); const choice = options()[index];
63
+ if (!request || !choice) { return; }
64
+ if (request.step === 'architecture') { setSelected([choice.id]); }
65
+ else { setSelected(old => old.includes(choice.id) ? old.filter(id => id !== choice.id) : [...old, choice.id]); }
66
+ };
67
+ const close = () => { api.route.navigate(previousRoute.name, 'params' in previousRoute ? previousRoute.params : undefined); };
68
+ const open = (request?: Interaction) => {
69
+ if (api.route.current.name !== 'd2h.interactions') { previousRoute = api.route.current; }
70
+ const next = request ?? rows().filter(r => interactionStatus(r) === '等待确认' && (!currentSession() || r.sessionId === currentSession())).at(0);
71
+ setActive(next);
72
+ setSelected(next?.step === 'architecture' ? next.workflow.architectures.filter(c => c.selected).slice(0, 1).map(c => c.id) : next?.step === 'scope' ? next.workflow.scopes.filter(c => c.selected).map(c => c.id) : []);
73
+ setReason(''); setError(''); setCursor(0); setEditingReason(false); refresh();
74
+ api.route.navigate('d2h.interactions');
75
+ };
76
+ const submit = (action: 'accept' | 'cancel') => {
77
+ const request = active();
78
+ if (!request) { return; }
79
+ try {
80
+ submitDecision(project, request.id, request.revision, action, action === 'accept' ? selected() : [], reason());
81
+ api.ui.toast({ variant: 'success', message: action === 'accept' ? '结果已返回原工具,等待原会话继续' : '已取消本次交互' });
82
+ setActive(undefined); close();
83
+ } catch (cause) { setError(String(cause)); }
84
+ };
85
+ const pendingFor = (sessionId: string) => rows().filter(request => {
86
+ try { return request.sessionId === sessionId && interactionStatus(request) === '等待确认'; } catch { return false; }
87
+ });
88
+ api.slots.register({ order: 40, slots: { sidebar_content(_context, value) {
89
+ return <box flexDirection="column" paddingTop={1} paddingBottom={1}>
90
+ <text fg={api.theme.current.primary}>d2h 开发进度{progress().demo ? ' · 演示' : ''}</text>
91
+ <Show when={progress().status === 'error'}><text fg={api.theme.current.error}>{progress().snapshot ? '读取失败 · 上次数据已过期' : '读取失败 · 尚无有效进度'}</text></Show>
92
+ <Show when={progress().summary} fallback={<text>{progress().status === 'loading' ? '正在读取执行账本…' : '尚未接入有效计划'}</text>}>{summary => <>
93
+ <text>{summary().phase}</text>
94
+ <text>{`里程碑:${summary().milestone}`}</text>
95
+ <text>{`Spec 实现:${summary().implemented}/${summary().total}`}</text>
96
+ <text>{`Spec 验收:${summary().accepted}/${summary().total}`}</text>
97
+ <text>{summary().selected ? `选用范围验收:${summary().selected!.accepted}/${summary().selected!.total}` : '选用范围验收:—(无逐项追踪)'}</text>
98
+ <text>{`在途 ${summary().running.length} · 阻塞 ${summary().blocked} · 验证债务 ${summary().validationDebt}`}</text>
99
+ <For each={summary().running.slice(0, 2)}>{task => <text>{`正在执行:${task}`}</text>}</For>
100
+ <text>{`采集 ${progress().snapshot!.captured_at.slice(11, 19)} UTC`}</text>
101
+ </>}</Show>
102
+ <text>{`本会话待确认:${pendingFor(value.session_id).length}`}</text>
103
+ <box onMouseDown={() => { void openProgress(); }}><text fg={api.theme.current.primary}>[查看详细看板]</text></box>
104
+ <box onMouseDown={() => {
105
+ const request = pendingFor(value.session_id)[0];
106
+ if (request) { open(request); } else { api.ui.toast({ variant: 'info', message: '当前会话没有待确认事项' }); }
107
+ }}><text fg={api.theme.current.primary}>[处理待确认事项]</text></box>
108
+ <box onMouseDown={() => { void refreshProgress(); }}><text>[刷新进度]</text></box>
109
+ </box>;
110
+ } } });
111
+ void refreshProgress();
112
+ const progressTimer = setInterval(() => { void refreshProgress(); }, 3000);
113
+ api.lifecycle.onDispose(() => clearInterval(progressTimer));
114
+ api.route.register([{
115
+ name: 'd2h.interactions',
116
+ render: () => {
117
+ const popMode = api.mode.push('d2h.interactions'); onCleanup(popMode);
118
+ return <box flexDirection="column" height="100%" padding={1} gap={0}>
119
+ <text height={1} fg={api.theme.current.primary}>d2h 迁移工作台 · OpenCode V1</text>
120
+ <text height={1}>{(() => { tick(); const r = active(); return r ? `${stepLabels[r.step]} · ${interactionStatus(r)}${r.workflow.demo ? ' · 演示数据' : ''}` : '等待中的交互请求'; })()}</text>
121
+ <text height={1}>{active() ? `原会话 ${active()!.sessionId} · ${active()!.revision.slice(0, 12)}` : '选择请求后提交;不会发送新的 prompt'}</text>
122
+ <text height={2} fg={api.theme.current.primary}>{options().length ? '↑/↓ 移动高亮,Enter / Ctrl+O 勾选;Tab 切换到原因输入。' : '本页无需选择:↑/↓ 滚动内容,Enter / Ctrl+S 确认继续。'}</text>
123
+ <box flexDirection="column" flexGrow={1} minHeight={1}>
124
+ <Show when={options().length > 0} fallback={<scrollbox flexGrow={1} minHeight={1} focused={!editingReason()} onKeyDown={key => {
125
+ if (['return', 'linefeed'].includes(key.name)) { key.preventDefault(); submit('accept'); }
126
+ }}>
127
+ <box flexDirection="column" gap={0}>
128
+ <Show when={active()} keyed fallback={<For each={rows().filter(r => interactionStatus(r) === '等待确认')}>{request =>
129
+ <box onMouseDown={() => open(request)}><text>{`${stepLabels[request.step]} · ${request.sessionId} · 点击打开`}</text></box>
130
+ }</For>}>{request => <>
131
+ <text>{request.workflow.title}</text>
132
+ {request.step === 'environment' ? <For each={request.workflow.checks}>{check => <text>{`[${check.status}] ${check.label}:${check.detail}`}</text>}</For> : null}
133
+ {request.step === 'report' ? <text>{request.workflow.report}</text> : null}
134
+ </>}</Show>
135
+ </box>
136
+ </scrollbox>}>
137
+ <select flexGrow={1} minHeight={1} focused={!editingReason()} selectedIndex={cursor()}
138
+ options={options().map(choice => ({ name: `${selected().includes(choice.id) ? '[x]' : '[ ]'} ${choice.label}`, description: choice.detail, value: choice.id }))}
139
+ showDescription={false} showScrollIndicator={true} wrapSelection={true}
140
+ onChange={index => setCursor(index)} onSelect={index => toggleOption(index)} />
141
+ <text height={2}>{options()[cursor()]?.detail ?? ''}</text>
142
+ </Show>
143
+ </box>
144
+ <text height={1}>{editingReason() ? '正在输入原因 · Tab 返回内容/选项' : '原因输入未聚焦 · 按 Tab 输入(范围确认必填)'}</text>
145
+ <box height={1} flexShrink={0}><input width="100%" focused={editingReason()} onMouseDown={() => setEditingReason(true)} placeholder="填写原因后 Ctrl+S 提交" value={reason()} onInput={value => setReason(value)} onSubmit={() => submit('accept')} /></box>
146
+ <Show when={error()}><text height={2} fg={api.theme.current.error}>{error()}</text></Show>
147
+ <box height={1} flexShrink={0} onMouseDown={() => { void openBrowser(); }}><text fg={api.theme.current.primary}>[在浏览器打开工作台 Ctrl+B]</text></box>
148
+ <box height={1} flexShrink={0} flexDirection="row" gap={2}>
149
+ <box onMouseDown={() => submit('accept')}><text fg={api.theme.current.primary}>[提交并继续 Ctrl+S]</text></box>
150
+ <box onMouseDown={() => submit('cancel')}><text>[取消 Ctrl+X]</text></box>
151
+ <box onMouseDown={close}><text>[稍后处理 Esc]</text></box>
152
+ </box>
153
+ <text height={1}>范围只生成草稿;不会直接修改计划或启动开发。</text>
154
+ </box>;
155
+ },
156
+ }]);
157
+ api.keymap.registerLayer({ mode: 'd2h.interactions', bindings: [
158
+ { key: 'ctrl+s', cmd: () => submit('accept'), desc: '提交结果' },
159
+ { key: 'ctrl+x', cmd: () => submit('cancel'), desc: '取消交互' },
160
+ { key: 'escape', cmd: close, desc: '稍后处理' },
161
+ { key: 'tab', cmd: () => setEditingReason(value => !value), desc: '切换内容与原因输入' },
162
+ { key: 'ctrl+o', cmd: () => toggleOption(cursor()), desc: '勾选当前选项' },
163
+ { key: 'ctrl+b', cmd: () => { void openBrowser(); }, desc: '在浏览器打开当前工作台' },
164
+ ] });
165
+ api.keymap.registerLayer({ commands: [{ name: 'd2h.open', title: 'd2h: 打开迁移工作台', category: 'd2h', namespace: 'palette', slashName: 'd2h', run: () => open() }] });
166
+ api.keymap.registerLayer({ commands: [
167
+ { name: 'd2h.progress', title: 'd2h: 显示/隐藏进度侧栏', category: 'd2h', namespace: 'palette', slashName: 'd2h-progress', run: () => {
168
+ if (api.route.current.name === 'session') { api.keymap.dispatchCommand('session.sidebar.toggle'); }
169
+ else { api.ui.toast({ variant: 'info', message: '进入一条对话后运行 /d2h-progress,可显示或隐藏右侧进度栏' }); }
170
+ } },
171
+ { name: 'd2h.board', title: 'd2h: 在浏览器查看开发进度', category: 'd2h', namespace: 'palette', slashName: 'd2h-board', run: () => { void openProgress(); } },
172
+ { name: 'd2h.progress-demo', title: 'd2h: 体验进度侧栏(仅演示项目,无模型)', category: 'd2h', namespace: 'palette', slashName: 'd2h-progress-demo', async run() {
173
+ await refreshProgress();
174
+ if (!progress().demo) { api.ui.toast({ variant: 'info', message: '此入口仅适用于明确标记 demo 的演示项目;正式项目直接进入任务对话' }); return; }
175
+ try {
176
+ const result = await api.client.session.create({ directory: project, title: 'd2h 进度侧栏体验(无模型)' });
177
+ if (!result.data) { throw new Error('未能创建演示会话'); }
178
+ api.route.navigate('session', { sessionID: result.data.id });
179
+ api.ui.toast({ variant: 'info', message: '演示会话不调用模型;窄终端输入 /d2h-progress 显示右侧进度栏' });
180
+ } catch (cause) { api.ui.toast({ variant: 'error', message: String(cause) }); }
181
+ } },
182
+ ] });
183
+ // Explicit local fixture driver: tests the host without calling a model or changing user configuration.
184
+ api.keymap.registerLayer({ commands: [{ name: 'd2h.demo', title: 'd2h: 演示四步交互(不迁移)', category: 'd2h', namespace: 'palette', slashName: 'd2h-demo', async run() {
185
+ const file = path.join(project, `d2h-demo-${Date.now()}.json`);
186
+ fs.writeFileSync(file, JSON.stringify(demoWorkflow, null, 2), { flag: 'wx' });
187
+ const sid = currentSession() || 'demo-tui-no-model';
188
+ const results: Decision[] = [];
189
+ for (const step of ['environment', 'architecture', 'scope', 'report'] as const) {
190
+ const request = createInteraction(project, file, sid, 'demo-driver', step);
191
+ open(request);
192
+ try {
193
+ const decision = await waitForDecision(request, api.lifecycle.signal);
194
+ results.push(decision);
195
+ fs.writeFileSync(file + '.results.json', JSON.stringify(results, null, 2));
196
+ if (decision.action === 'cancel') { return; }
197
+ if (step === 'scope') {
198
+ fs.writeFileSync(file + '.scope-draft.json', JSON.stringify(scopeChange(request, decision), null, 2));
199
+ fs.writeFileSync(file, JSON.stringify({ ...demoWorkflow, report: demoReport(results) }, null, 2));
200
+ }
201
+ }
202
+ catch (cause) { api.ui.toast({ variant: 'error', message: String(cause) }); return; }
203
+ }
204
+ api.ui.toast({ variant: 'success', message: '演示完成;未调用模型、未执行迁移' });
205
+ } }] });
206
+ let host: ReturnType<typeof registerInteractionHost> | undefined;
207
+ let hostSession = '', lastHeartbeat = 0;
208
+ const publishHost = () => {
209
+ const sid = currentSession() || (api.route.current.name === 'd2h.interactions' ? active()?.sessionId ?? '' : '');
210
+ if (sid !== hostSession) { host?.dispose(); host = undefined; hostSession = sid; lastHeartbeat = 0; }
211
+ if (sid && Date.now() - lastHeartbeat >= 3000) {
212
+ if (host) { host.heartbeat(); } else { host = registerInteractionHost(project, sid); }
213
+ lastHeartbeat = Date.now();
214
+ }
215
+ };
216
+ try { publishHost(); } catch (cause) { setError(String(cause)); }
217
+ const timer = setInterval(() => {
218
+ try { publishHost(); refresh(); const sid = currentSession(); const next = rows().find(r => interactionStatus(r) === '等待确认' && r.sessionId === sid);
219
+ const current = active();
220
+ if (current && interactionStatus(current) !== '等待确认') {
221
+ setActive(undefined);
222
+ if (api.route.current.name === 'd2h.interactions') { close(); }
223
+ }
224
+ if (next && !active() && api.route.current.name === 'session') { open(next); }
225
+ } catch (cause) { setError(String(cause)); }
226
+ }, 700);
227
+ api.lifecycle.onDispose(() => { clearInterval(timer); host?.dispose(); });
228
+ },
229
+ };
230
+ export default plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harmonyos-arkts/d2h",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "description": "Install Android-to-HarmonyOS migration agents and skills into OpenCode or DevEco Code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,11 +13,19 @@
13
13
  "agents/",
14
14
  "AGENTS.md",
15
15
  "schemas/",
16
+ "workflow/",
17
+ "contracts/",
16
18
  "opencode/",
17
19
  ".claude-plugin/",
18
20
  "install-opencode.sh",
19
21
  "scripts/check-environment.mjs",
20
22
  "docs/environment-check.md",
23
+ "docs/incremental-spec-evaluation-design.md",
24
+ "docs/spec-plan-guide.md",
25
+ "docs/workflow-decoupling.md",
26
+ "docs/architecture-plan-guide.zh-CN.md",
27
+ "docs/architecture-plan-performance.md",
28
+ "docs/architecture-plan-two-project-evaluation.md",
21
29
  "README.md",
22
30
  "LICENSE"
23
31
  ],