@harmonyos-arkts/d2h 0.1.19 → 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 -132
  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,242 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+
5
+ export type Step = 'environment' | 'architecture' | 'scope' | 'report';
6
+ export interface Choice { id: string; label: string; detail: string; selected?: boolean; featureId?: string }
7
+ export interface Check { label: string; status: 'ready' | 'missing' | 'unverified'; detail: string }
8
+ export interface Workflow {
9
+ schema: 'd2h-interactive-workflow/v1';
10
+ title: string;
11
+ demo: boolean;
12
+ checks: Check[];
13
+ architectures: Choice[];
14
+ scopes: Choice[];
15
+ report: string;
16
+ baseline: { featureHash: string; planHash: string; candidate: string; eventSequence: number };
17
+ target?: 'mvp' | 'core' | 'full' | null;
18
+ userPriorities?: Record<string, 'must' | 'normal' | 'later'>;
19
+ }
20
+ export interface Interaction {
21
+ schema: 'd2h-interaction/v1'; id: string; projectRoot: string; sessionId: string; messageId: string;
22
+ workflowPath: string; revision: string; step: Step; createdAt: string; expiresAt: string; workflow: Workflow;
23
+ }
24
+ export interface Decision {
25
+ schema: 'd2h-interaction-result/v1'; requestId: string; sessionId: string; revision: string;
26
+ action: 'accept' | 'cancel'; selected: string[]; reason: string; submittedAt: string;
27
+ }
28
+ const steps: Step[] = ['environment', 'architecture', 'scope', 'report'];
29
+ export const stepLabels: Record<Step, string> = {
30
+ environment: '确认环境', architecture: '选择架构', scope: '选择实施范围', report: '查看报告',
31
+ };
32
+ export function digest(value: string): string { return createHash('sha256').update(value).digest('hex'); }
33
+ function assert(condition: unknown, message: string): asserts condition { if (!condition) { throw new Error(message); } }
34
+ function plainFile(file: string): string {
35
+ const stat = fs.lstatSync(file);
36
+ assert(stat.isFile() && !stat.isSymbolicLink() && stat.size <= 1024 * 1024, '文件必须是小于 1 MiB 的普通文件');
37
+ return fs.readFileSync(file, 'utf8');
38
+ }
39
+ export function readWorkflow(file: string): Workflow {
40
+ const data = JSON.parse(plainFile(file)) as Workflow;
41
+ assert(data.schema === 'd2h-interactive-workflow/v1' && typeof data.title === 'string' && typeof data.demo === 'boolean', '无效的工作流');
42
+ assert(Array.isArray(data.checks) && data.checks.every(c => typeof c.label === 'string' && typeof c.detail === 'string' && ['ready', 'missing', 'unverified'].includes(c.status)), '无效的环境检查');
43
+ for (const choices of [data.architectures, data.scopes]) {
44
+ assert(Array.isArray(choices) && choices.length > 0 && choices.length <= 200, '选项必须为 1–200 项');
45
+ assert(choices.every(c => typeof c.id === 'string' && /^[\w.-]+$/.test(c.id) && typeof c.label === 'string' && typeof c.detail === 'string'), '无效的选项');
46
+ assert(new Set(choices.map(c => c.id)).size === choices.length, '选项 ID 重复');
47
+ }
48
+ assert(typeof data.report === 'string' && data.baseline && typeof data.baseline.featureHash === 'string' && typeof data.baseline.planHash === 'string' && typeof data.baseline.candidate === 'string' && Number.isSafeInteger(data.baseline.eventSequence), '缺少报告或基线身份');
49
+ assert(data.scopes.every(c => typeof c.featureId === 'string' && /^[\w.-]+$/.test(c.featureId)), '范围选项缺少 featureId');
50
+ return data;
51
+ }
52
+ export function interactionRoot(project: string): string {
53
+ const root = fs.realpathSync(project);
54
+ return path.join(path.dirname(root), `${path.basename(root)}-migration`, '.migration', 'interactions');
55
+ }
56
+ function requestDirectory(project: string, id: string): string {
57
+ assert(/^[a-f0-9-]{36}$/.test(id), '无效的请求 ID');
58
+ const root = interactionRoot(project);
59
+ const migration = path.dirname(path.dirname(root));
60
+ for (let ancestor = root; ancestor !== path.dirname(migration); ancestor = path.dirname(ancestor)) {
61
+ if (fs.existsSync(ancestor)) { assert(fs.realpathSync(ancestor) === ancestor, '交互目录不能通过符号链接重定向'); }
62
+ }
63
+ const dir = path.join(root, id);
64
+ if (fs.existsSync(dir)) {
65
+ assert(!fs.lstatSync(dir).isSymbolicLink() && fs.realpathSync(dir) === dir, '交互目录不能通过符号链接重定向');
66
+ }
67
+ return dir;
68
+ }
69
+ function writeExclusive(file: string, value: unknown): void {
70
+ const temp = `${file}.${randomUUID()}.tmp`;
71
+ fs.writeFileSync(temp, JSON.stringify(value, null, 2), { mode: 0o600, flag: 'wx' });
72
+ try { fs.linkSync(temp, file); } finally { fs.unlinkSync(temp); }
73
+ }
74
+ export interface InteractionCapability {
75
+ supported: boolean; fallback: 'conversation'; reason: 'host_ready' | 'host_unavailable';
76
+ }
77
+ const hostTtlMs = 10_000;
78
+ function hostDirectory(project: string): string {
79
+ requestDirectory(project, '00000000-0000-0000-0000-000000000000');
80
+ const dir = path.join(interactionRoot(project), '.hosts');
81
+ if (fs.existsSync(dir)) { assert(fs.realpathSync(dir) === dir, '界面能力目录不能通过符号链接重定向'); }
82
+ return dir;
83
+ }
84
+ /** A UI adapter advertises only sessions it can currently present. Never a user decision. */
85
+ export function registerInteractionHost(project: string, sessionId: string) {
86
+ assert(sessionId.length > 0, '缺少界面会话');
87
+ const dir = hostDirectory(project);
88
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
89
+ const file = path.join(dir, randomUUID() + '.json');
90
+ const heartbeat = () => {
91
+ assert(hostDirectory(project) === dir, '界面能力目录发生变化');
92
+ const temp = file + '.tmp';
93
+ fs.writeFileSync(temp, JSON.stringify({ schema: 'd2h-interaction-host/v1',
94
+ projectRoot: fs.realpathSync(project), sessionId, steps, updatedAt: Date.now() }), { mode: 0o600, flag: 'wx' });
95
+ fs.renameSync(temp, file);
96
+ };
97
+ heartbeat();
98
+ return { heartbeat, dispose: () => {
99
+ try { hostDirectory(project); fs.rmSync(file, { force: true }); }
100
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { throw error; } }
101
+ } };
102
+ }
103
+ export function interactionCapability(project: string, sessionId: string, step?: Step): InteractionCapability {
104
+ try {
105
+ const dir = hostDirectory(project), now = Date.now();
106
+ const supported = fs.readdirSync(dir).some(name => {
107
+ if (!/^[a-f0-9-]{36}\.json$/.test(name)) { return false; }
108
+ try {
109
+ const host = JSON.parse(plainFile(path.join(dir, name)));
110
+ return host.schema === 'd2h-interaction-host/v1' && host.projectRoot === fs.realpathSync(project)
111
+ && host.sessionId === sessionId && Number.isFinite(host.updatedAt)
112
+ && now >= host.updatedAt && now - host.updatedAt < hostTtlMs
113
+ && Array.isArray(host.steps) && (step ? host.steps.includes(step) : steps.every(s => host.steps.includes(s)));
114
+ } catch { return false; }
115
+ });
116
+ return { supported, fallback: 'conversation', reason: supported ? 'host_ready' : 'host_unavailable' };
117
+ } catch { return { supported: false, fallback: 'conversation', reason: 'host_unavailable' }; }
118
+ }
119
+ export class InteractionHostUnavailable extends Error {}
120
+ /** Guard the tool itself as well as offering a separate capability query. */
121
+ export async function requestInteraction(project: string, workflowPath: string, sessionId: string,
122
+ messageId: string, step: Step, signal?: AbortSignal,
123
+ onRequest?: (request: Interaction) => void) {
124
+ const capability = interactionCapability(project, sessionId, step);
125
+ if (!capability.supported) { return { status: 'unsupported' as const, ...capability }; }
126
+ const request = createInteraction(project, workflowPath, sessionId, messageId, step);
127
+ onRequest?.(request);
128
+ try { return { status: 'completed' as const, request, decision: await waitForDecision(request, signal, true) }; }
129
+ catch (error) {
130
+ if (!(error instanceof InteractionHostUnavailable)) { throw error; }
131
+ return { status: 'unsupported' as const, supported: false, fallback: 'conversation' as const, reason: 'host_unavailable' as const, requestId: request.id };
132
+ }
133
+ }
134
+ export function createInteraction(project: string, workflowPath: string, sessionId: string, messageId: string, step: Step, ttlMs = 30 * 60_000): Interaction {
135
+ assert(steps.includes(step) && sessionId.length > 0 && ttlMs > 0, '无效的会话或步骤');
136
+ assert(path.isAbsolute(workflowPath), '工作流路径必须为绝对路径');
137
+ const projectRoot = fs.realpathSync(project);
138
+ const workflow = readWorkflow(workflowPath);
139
+ const id = randomUUID();
140
+ const dir = requestDirectory(projectRoot, id);
141
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
142
+ requestDirectory(projectRoot, id);
143
+ const request: Interaction = {
144
+ schema: 'd2h-interaction/v1', id, projectRoot, sessionId, messageId, step,
145
+ workflowPath: path.resolve(workflowPath), revision: digest(plainFile(workflowPath)), workflow,
146
+ createdAt: new Date().toISOString(), expiresAt: new Date(Date.now() + ttlMs).toISOString(),
147
+ };
148
+ writeExclusive(path.join(dir, 'request.json'), request);
149
+ return request;
150
+ }
151
+ export function readInteraction(project: string, id: string): Interaction {
152
+ const data = JSON.parse(plainFile(path.join(requestDirectory(project, id), 'request.json'))) as Interaction;
153
+ assert(data.schema === 'd2h-interaction/v1' && data.id === id && data.projectRoot === fs.realpathSync(project) && steps.includes(data.step), '请求身份不匹配');
154
+ return data;
155
+ }
156
+ export function listInteractions(project: string): Interaction[] {
157
+ const root = interactionRoot(project);
158
+ if (!fs.existsSync(root)) { return []; }
159
+ return fs.readdirSync(root).filter(id => /^[a-f0-9-]{36}$/.test(id)).flatMap(id => {
160
+ try { return [readInteraction(project, id)]; } catch { return []; }
161
+ }).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
162
+ }
163
+ export function readDecision(request: Interaction): Decision | undefined {
164
+ const file = path.join(requestDirectory(request.projectRoot, request.id), 'result.json');
165
+ if (!fs.existsSync(file)) { return undefined; }
166
+ const result = JSON.parse(plainFile(file)) as Decision;
167
+ assert(result.schema === 'd2h-interaction-result/v1' && result.requestId === request.id && result.sessionId === request.sessionId && result.revision === request.revision, '结果不属于当前请求');
168
+ validateDecision(request, result.action, result.selected, result.reason);
169
+ return result;
170
+ }
171
+ function validateDecision(request: Interaction, action: string, selected: string[], reason: string): void {
172
+ assert(['accept', 'cancel'].includes(action) && typeof reason === 'string' && reason.length <= 4000, '无效的提交');
173
+ assert(Array.isArray(selected) && selected.every(id => typeof id === 'string') && new Set(selected).size === selected.length, '无效的选择');
174
+ if (action === 'cancel') { assert(selected.length === 0, '取消不能携带选择'); return; }
175
+ if (request.step === 'environment') {
176
+ assert(selected.length === 0 && request.workflow.checks.every(c => c.status === 'ready'), '环境仍有缺失或未验证项,不能确认通过');
177
+ } else if (request.step === 'architecture' || request.step === 'scope') {
178
+ const choices = request.step === 'architecture' ? request.workflow.architectures : request.workflow.scopes;
179
+ assert(selected.length > 0 && selected.every(id => choices.some(c => c.id === id)), '选择为空或包含未知项');
180
+ assert(request.step !== 'architecture' || selected.length === 1, '架构必须单选');
181
+ assert(request.step !== 'scope' || reason.trim().length > 0, '范围确认需要填写原因');
182
+ } else { assert(selected.length === 0, '报告确认不接受范围项'); }
183
+ }
184
+ export function submitDecision(project: string, id: string, revision: string, action: 'accept' | 'cancel', selected: string[], reason: string): Decision {
185
+ const request = readInteraction(project, id);
186
+ assert(request.revision === revision, '请求版本不匹配');
187
+ if (action === 'accept') { assert(digest(plainFile(request.workflowPath)) === revision, '输入版本已变化,请取消本次请求并重新发起'); }
188
+ assert(Date.now() < Date.parse(request.expiresAt), '请求已过期,请重新发起');
189
+ assert(!fs.existsSync(path.join(requestDirectory(project, id), 'aborted.json')), '原工具已经取消');
190
+ validateDecision(request, action, selected, reason);
191
+ const result: Decision = { schema: 'd2h-interaction-result/v1', requestId: id, sessionId: request.sessionId, revision, action, selected, reason, submittedAt: new Date().toISOString() };
192
+ writeExclusive(path.join(requestDirectory(project, id), 'result.json'), result);
193
+ return result;
194
+ }
195
+ export function interactionStatus(request: Interaction): string {
196
+ const dir = requestDirectory(request.projectRoot, request.id);
197
+ if (fs.existsSync(path.join(dir, 'aborted.json'))) {
198
+ return JSON.parse(plainFile(path.join(dir, 'aborted.json'))).reason === 'expired' ? '已过期' : '已取消';
199
+ }
200
+ const result = readDecision(request);
201
+ if (result) { return result.action === 'accept' ? '已提交' : '已取消'; }
202
+ if (Date.now() >= Date.parse(request.expiresAt)) { return '已过期'; }
203
+ return '等待确认';
204
+ }
205
+ export function isWaitingForInteraction(project: string, sessionId: string): boolean {
206
+ try { return listInteractions(project).some(request => request.sessionId === sessionId && interactionStatus(request) === '等待确认'); }
207
+ catch { return false; }
208
+ }
209
+ export async function waitForDecision(request: Interaction, signal?: AbortSignal, requireHost = false): Promise<Decision> {
210
+ for (;;) {
211
+ if (signal?.aborted || Date.now() >= Date.parse(request.expiresAt)) {
212
+ const file = path.join(requestDirectory(request.projectRoot, request.id), 'aborted.json');
213
+ if (!fs.existsSync(file)) { writeExclusive(file, { reason: signal?.aborted ? 'tool_cancelled' : 'expired' }); }
214
+ throw new Error(signal?.aborted ? '交互已取消' : '交互已过期');
215
+ }
216
+ const result = readDecision(request);
217
+ if (result) {
218
+ if (result.action === 'accept') { assert(digest(plainFile(request.workflowPath)) === request.revision, '工具恢复前输入版本发生变化'); }
219
+ return result;
220
+ }
221
+ if (requireHost && !interactionCapability(request.projectRoot, request.sessionId, request.step).supported) {
222
+ const file = path.join(requestDirectory(request.projectRoot, request.id), 'aborted.json');
223
+ if (!fs.existsSync(file)) { writeExclusive(file, { reason: 'host_unavailable' }); }
224
+ throw new InteractionHostUnavailable('界面不可用,请回到原会话询问;未记录用户同意');
225
+ }
226
+ await new Promise(resolve => setTimeout(resolve, 250));
227
+ }
228
+ }
229
+ export function scopeChange(request: Interaction, decision: Decision): object {
230
+ assert(request.step === 'scope' && decision.action === 'accept', '不是已提交的范围请求');
231
+ return {
232
+ schema: 'd2h-scope-change-request/v1',
233
+ feature_hash: request.workflow.baseline.featureHash,
234
+ plan_hash: request.workflow.baseline.planHash,
235
+ candidate: request.workflow.baseline.candidate,
236
+ event_sequence: request.workflow.baseline.eventSequence,
237
+ target: request.workflow.target ?? null,
238
+ scope_refs: decision.selected.map(id => ({ feature_id: request.workflow.scopes.find(c => c.id === id)!.featureId, scope_id: id })),
239
+ user_priorities: request.workflow.userPriorities ?? {},
240
+ reason: decision.reason,
241
+ };
242
+ }
@@ -0,0 +1,55 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createInteraction, waitForDecision, scopeChange } from './core.ts';
4
+ import type { Decision, Workflow } from './core.ts';
5
+
6
+ export const demoWorkflow: Workflow = {
7
+ schema: 'd2h-interactive-workflow/v1', demo: true, title: '迁移规划演示 · 启动 → 基本登录 → 主页面雏形',
8
+ checks: [
9
+ { label: '开发运行时', status: 'ready', detail: '演示 fixture:Node / Python / Git 已准备。实际工具须读取真实检查报告。' },
10
+ { label: 'HarmonyOS 工具链', status: 'ready', detail: '演示 fixture:不代表 SDK、签名、编译或设备已验证。' },
11
+ { label: '本轮执行边界', status: 'ready', detail: '仅收集用户决策、形成范围草稿;不进入目标应用开发。' },
12
+ ],
13
+ architectures: [
14
+ { id: 'native-arkts', label: '原生 ArkTS + 平台适配', detail: '界面按鸿蒙范式实现,Native 库与绑定层单独评估。', selected: true },
15
+ { id: 'retain-native', label: '保留可复用 Native 核心', detail: 'UI 使用 ArkTS;复用核心前必须验证构建、平台依赖及 N-API 边界。' },
16
+ ],
17
+ scopes: [
18
+ { id: 'startup', featureId: 'feature-startup', label: '启动与路由', detail: '启动到可交互首屏。', selected: true },
19
+ { id: 'login-basic', featureId: 'feature-login', label: '基本登录', detail: '完成进入主页面所需的最小登录路径。', selected: true },
20
+ { id: 'home-shell', featureId: 'feature-home', label: '主页面雏形', detail: '尽早形成导航和核心内容骨架,不等待完整登录体系。', selected: true },
21
+ { id: 'login-complete', featureId: 'feature-login', label: '完整账号体系', detail: '多种登录、找回密码等,默认延期。' },
22
+ ],
23
+ baseline: { featureHash: 'demo-features-v1', planHash: 'demo-plan-v1', candidate: 'demo-only', eventSequence: 0 },
24
+ target: 'mvp', userPriorities: { 'feature-startup': 'normal', 'feature-login': 'normal', 'feature-home': 'must' },
25
+ report: '本次仅验证交互流程。\n\n用户决策将返回原工具调用;没有生成应用源码,没有执行构建或设备验收。\n\n范围草稿需经主 Agent 校验、修订上游和 reconcile 后才算应用成功。',
26
+ };
27
+
28
+ export function demoReport(results: Decision[]): string {
29
+ const architecture = results.find(result => result.selected.some(id => demoWorkflow.architectures.some(c => c.id === id)));
30
+ const scope = results.find(result => result.selected.some(id => demoWorkflow.scopes.some(c => c.id === id)));
31
+ return `交互决策摘要(演示)\n\n架构:${demoWorkflow.architectures.find(c => architecture?.selected.includes(c.id))?.label ?? '未确认'}\n实施范围:${demoWorkflow.scopes.filter(c => scope?.selected.includes(c.id)).map(c => c.label).join(' → ')}\n选择原因:${scope?.reason ?? ''}\n\n${demoWorkflow.report}`;
32
+ }
33
+
34
+ export async function runDemo(project: string, sessionId: string, signal?: AbortSignal, onStep?: (message: string) => void): Promise<Decision[]> {
35
+ const file = path.join(project, 'd2h-interaction-demo.json');
36
+ if (fs.existsSync(file)) { throw new Error('演示输入已存在,请使用新的演示目录,避免覆盖'); }
37
+ fs.mkdirSync(project, { recursive: true });
38
+ fs.writeFileSync(file, JSON.stringify(demoWorkflow, null, 2), { flag: 'wx' });
39
+ const results: Decision[] = [];
40
+ for (const step of ['environment', 'architecture', 'scope', 'report'] as const) {
41
+ onStep?.(`等待用户:${step}`);
42
+ const request = createInteraction(project, file, sessionId, 'demo-driver', step);
43
+ const result = await waitForDecision(request, signal);
44
+ results.push(result);
45
+ if (step === 'scope' && result.action === 'accept') {
46
+ fs.writeFileSync(path.join(project, 'scope-draft.json'), JSON.stringify(scopeChange(request, result), null, 2));
47
+ fs.writeFileSync(file, JSON.stringify({ ...demoWorkflow, report: demoReport(results) }, null, 2));
48
+ }
49
+ if (result.action === 'cancel') { onStep?.('用户取消,流程停止'); return results; }
50
+ onStep?.(`已收到用户结果,原流程继续:${step}`);
51
+ }
52
+ fs.writeFileSync(path.join(project, 'interaction-results.json'), JSON.stringify(results, null, 2));
53
+ onStep?.('四步交互完成;未执行迁移或验收');
54
+ return results;
55
+ }
@@ -0,0 +1,37 @@
1
+ import http from 'node:http';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { randomBytes } from 'node:crypto';
6
+ import { listInteractions, interactionStatus, submitDecision } from '../../dist/opencode/interactive-tools/core.js';
7
+ import { interactionHtml } from '../../dist/opencode/interactive-tools/panel.js';
8
+ import { runDemo } from '../../dist/opencode/interactive-tools/demo-workflow.js';
9
+
10
+ const root = path.dirname(fileURLToPath(import.meta.url));
11
+ const project = path.resolve(process.argv[2] || path.join(root, '../../../validation/interactive-tools-mvp', `browser-${Date.now()}`));
12
+ fs.mkdirSync(project, { recursive: true });
13
+ const port = Number(process.env.D2H_PREVIEW_PORT || 4318);
14
+ const origin = `http://127.0.0.1:${port}`;
15
+ const controller = new AbortController();
16
+ const server = http.createServer(async (req, res) => {
17
+ res.setHeader('Cache-Control', 'no-store');
18
+ res.setHeader('X-Content-Type-Options', 'nosniff');
19
+ if (req.headers.host !== `127.0.0.1:${port}` || (req.headers.origin && req.headers.origin !== origin)) { res.writeHead(403).end(); return; }
20
+ try {
21
+ if (req.method === 'GET' && req.url === '/') { res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.end(interactionHtml(randomBytes(18).toString('base64'))); }
22
+ else if (req.method === 'GET' && req.url === '/snapshot') { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ type: 'snapshot', items: listInteractions(project).map(request => ({ request, status: interactionStatus(request) })) })); }
23
+ else if (req.method === 'POST' && req.url === '/action') {
24
+ if (req.headers.origin !== origin || req.headers['content-type'] !== 'application/json') { res.writeHead(403).end(); return; }
25
+ let body = ''; for await (const chunk of req) { body += chunk; if (body.length > 64 * 1024) { throw Error('请求过大'); } }
26
+ const message = JSON.parse(body);
27
+ if (message.type === 'submit') { submitDecision(project, message.id, message.revision, message.action, message.selected, message.reason); }
28
+ else if (message.type !== 'ready') { throw Error('未知动作'); }
29
+ res.setHeader('Content-Type', 'application/json'); res.end('{}');
30
+ } else { res.writeHead(404).end(); }
31
+ } catch (error) { res.writeHead(400, { 'Content-Type': 'application/json' }).end(JSON.stringify({ error: String(error.message) })); }
32
+ });
33
+ server.listen(port, '127.0.0.1', () => {
34
+ console.log(`d2h 交互 MVP:${origin}\n演示目录:${project}\n这是工具等待/返回的本地驱动,没有调用模型。`);
35
+ void runDemo(project, 'demo-browser-no-model', controller.signal, console.log).catch(error => console.error(error.message));
36
+ });
37
+ for (const signal of ['SIGINT', 'SIGTERM']) { process.on(signal, () => { controller.abort(); server.close(); }); }