@harmonyos-arkts/d2h 0.1.11 → 0.1.13
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/.claude-plugin/marketplace.json +15 -13
- package/.claude-plugin/plugin.json +13 -7
- package/AGENTS.md +5 -5
- package/README.md +212 -212
- package/agents/android-to-hmos-00-orchestrator.md +318 -318
- package/agents/d2h.md +157 -157
- package/dist/bin/install-opencode.js +30 -30
- package/docs/environment-check.md +86 -86
- package/install-opencode.sh +160 -160
- package/opencode/agents.json +16 -16
- package/opencode/vscode-d2h-package.json +7 -7
- package/opencode/vscode-d2h-plugin.mjs +29 -29
- package/package.json +54 -54
- package/schemas/android-source-manifest.schema.json +25 -25
- package/schemas/checkpoint-provenance.schema.json +67 -67
- package/schemas/final-acceptance.schema.json +29 -29
- package/schemas/managed-evidence-index.schema.json +15 -15
- package/schemas/managed-evidence.schema.json +114 -114
- package/schemas/migration-config.schema.json +51 -51
- package/schemas/migration-report-index.schema.json +39 -39
- package/schemas/migration-report-item.schema.json +51 -51
- package/schemas/migration-report-summary.schema.json +25 -25
- package/schemas/migration-status.schema.json +202 -202
- package/schemas/preflight.schema.json +60 -60
- package/schemas/source-order-audit.schema.json +16 -16
- package/schemas/source-provenance-event.schema.json +19 -19
- package/schemas/spec-app-shard.schema.json +45 -45
- package/schemas/spec-fact-corrections-shard.schema.json +13 -13
- package/schemas/spec-features-shard.schema.json +43 -43
- package/schemas/spec-index.schema.json +26 -26
- package/schemas/spec-interactions-shard.schema.json +40 -40
- package/schemas/spec-page.schema.json +98 -98
- package/schemas/spec-pages-index.schema.json +1 -1
- package/schemas/spec-unresolved-shard.schema.json +1 -1
- package/schemas/task-envelope.schema.json +109 -109
- package/schemas/task-plan.schema.json +341 -341
- package/scripts/check-environment.mjs +320 -320
- package/skills/android2hmos_resources_convert/SKILL.md +180 -180
- package/skills/android2hmos_resources_convert/references/image-conversion-rules.md +230 -230
- package/skills/android2hmos_resources_convert/references/svg-fix-patterns.md +175 -175
- package/skills/android2hmos_resources_convert/references/xml-drawable-to-svg-rules.md +513 -513
- package/skills/arkts-app-identity/SKILL.md +238 -238
- package/skills/arkts-i18n/SKILL.md +496 -496
- package/skills/arkts-i18n/evals/evals.json +84 -84
- package/skills/arkts-i18n/references/code-examples.md +302 -302
- package/skills/arkts-i18n/references/common-pitfalls.md +391 -391
- package/skills/arkts-i18n/references/dynamic-language-switch.md +604 -604
- package/skills/arkts-i18n/references/hardcoded-string-scanner.md +348 -348
- package/skills/arkts-i18n/references/language-codes.md +104 -104
- package/skills/arkts-i18n/references/resource-file-structure.md +775 -775
- package/skills/arkts-i18n/references/static-vs-dynamic.md +242 -242
- package/skills/arkts-i18n/references/v1-compat.md +244 -244
- package/skills/arkts-i18n/scripts/audit_i18n_completeness.sh +174 -174
- package/skills/arkts-icon-sizing/SKILL.md +211 -211
- package/skills/arkts-icon-sizing/scripts/icon_audit.py +131 -131
- package/skills/arkts-icon-sizing/scripts/icon_autofix.py +88 -88
- package/skills/arkts-icon-sizing/scripts/icon_dims.py +179 -179
- package/skills/arkts-icon-sizing/scripts/icon_fix.py +119 -119
- package/skills/arkts-mvvm-architecture/SKILL.md +613 -613
- package/skills/d2h-hypium-forge/SKILL.md +309 -309
- package/skills/d2h-hypium-forge/references/functional-spec-template.md +140 -140
- package/skills/d2h-hypium-forge/references/official-api.md +135 -135
- package/skills/d2h-hypium-forge/references/pitfalls.md +98 -98
- package/skills/d2h-hypium-forge/references/testability-template.md +71 -71
- package/skills/d2h-hypium-forge/scripts/_common_template.py +659 -659
- package/skills/d2h-hypium-forge/scripts/analyze_results.py +165 -165
- package/skills/d2h-hypium-forge/scripts/assert_audit.py +107 -107
- package/skills/d2h-hypium-forge/scripts/collect_runtime_pages.py +169 -169
- package/skills/d2h-hypium-forge/scripts/forge_report.py +325 -325
- package/skills/d2h-hypium-forge/scripts/inject_resource_ids.py +114 -114
- package/skills/d2h-hypium-forge/scripts/lint_asserts.py +80 -80
- package/skills/d2h-hypium-forge/scripts/lint_locate.py +60 -60
- package/skills/d2h-hypium-forge/scripts/merge_appgraph.py +249 -249
- package/skills/d2h-hypium-forge/scripts/precondition.py +140 -140
- package/skills/d2h-hypium-forge/scripts/preinject_ids.py +232 -232
- package/skills/d2h-hypium-forge/scripts/probe.py +197 -197
- package/skills/d2h-hypium-forge/scripts/run_direct.py +823 -823
- package/skills/d2h-hypium-forge/scripts/run_parallel.py +121 -121
- package/skills/d2h-hypium-forge/scripts/scan_features.py +255 -255
- package/skills/d2h-hypium-forge/scripts/upgrade_asserts.py +66 -66
- package/skills/d2h-hypium-forge/scripts/validate_scenarios.py +84 -84
- package/skills/d2h-page-develop/SKILL.md +112 -112
- package/skills/d2h-page-develop/agents/openai.yaml +7 -7
- package/skills/d2h-page-spec-generate/SKILL.md +206 -206
- package/skills/d2h-page-spec-generate/scripts/build_batch_report.py +227 -227
- package/skills/d2h-utg-ai-generate/SKILL.md +128 -128
- package/skills/d2h-utg-ai-generate/references/click-paths-schema.json +76 -76
- package/skills/d2h-utg-ai-generate/references/extraction-guide.md +34 -34
- package/skills/d2h-utg-ai-generate/references/facts-contract.md +96 -96
- package/skills/d2h-utg-ai-generate/references/page-schema.json +61 -61
- package/skills/d2h-utg-ai-generate/references/utg-schema.json +89 -89
- package/skills/d2h-utg-ai-generate/scripts/build_page_json.py +314 -314
- package/skills/d2h-utg-ai-generate/scripts/build_utg.py +123 -123
- package/skills/d2h-utg-ai-generate/scripts/capture_from_page_json.ts +280 -280
- package/skills/d2h-utg-ai-generate/scripts/utg_common.py +524 -524
- package/skills/d2h-utg-ai-generate/scripts/utg_html.py +489 -489
- package/skills/d2h-utg-ai-generate/scripts/validate_utg.py +69 -69
- package/skills/d2h-utg-ai-generate/scripts/vendor/D3-LICENSE +13 -13
- package/skills/d2h-utg-ai-generate/scripts/vendor/d3-7.9.0.min.js +2 -2
- package/skills/d2h-utg-ai-generate/tests/test_pipeline.py +369 -369
- package/skills/d2h-utg-static-generate/SKILL.md +73 -73
- package/skills/d2h-utg-static-generate/references/discoveries.md +38 -38
- package/skills/d2h-utg-static-generate/scripts/generate.py +499 -499
- package/skills/d2h-utg-static-generate/tests/test_static.py +335 -335
- package/skills/d2h-utg-static-generate/tests/verify-offline-html.py +60 -60
- package/skills/d2h-utg-static-generate/tests/verify-produced-fixture.py +131 -131
- package/skills/harmonyos-migration-playbook/SKILL.md +83 -83
- package/skills/harmonyos-migration-playbook/agents/openai.yaml +7 -7
- package/skills/harmonyos-migration-playbook/references/arkts-compile.md +24 -24
- package/skills/harmonyos-migration-playbook/references/harmony-runtime.md +53 -53
- package/skills/harmonyos-migration-playbook/references/lesson-lifecycle.md +45 -45
- package/skills/harmonyos-migration-playbook/references/protocol-e2e.md +23 -23
- package/skills/harmonyos-migration-playbook/references/ui-automation.md +53 -53
- package/skills/harmonyos-migration-playbook/references/windows-environment.md +38 -38
- package/skills/maintaining-migration-report/SKILL.md +144 -144
- package/skills/maintaining-migration-report/references/final-acceptance.md +48 -48
- package/skills/maintaining-migration-report/references/source-provenance.md +66 -66
- package/skills/managing-d2h-toolchain/SKILL.md +24 -24
- package/skills/managing-d2h-toolchain/environment-requirements.json +16 -16
- package/skills/managing-d2h-toolchain/scripts/manage-toolchain.mjs +98 -98
- package/skills/managing-d2h-toolchain/toolchain.json +16 -16
- package/skills/native-library-substitution/SKILL.md +384 -384
- package/skills/native-library-substitution/references/native-library-substitution.json +56905 -56905
- package/skills/native-library-substitution/references/native-library-substitution.md +163 -163
- package/skills/preparing-migration-workspace/SKILL.md +131 -131
- package/skills/reviewing-migration-process/SKILL.md +62 -62
|
@@ -1,232 +1,232 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""preinject_ids.py — ID 预置(铁律①的机械强制,先于脚本编写,不依赖 cases DSL)。
|
|
3
|
-
|
|
4
|
-
输入 features.json(scan_features 产物),对全部 has_id=False 的交互点批量注入
|
|
5
|
-
`.id('tc_<page>_<n>')`(资源文案/精确模糊文本/行号三路定位,属性链内插桩永远合法),
|
|
6
|
-
输出 ids.json 映射(脚本编写只认此表与既有 ID)。注入后须 rebuild + uninstall&&install。
|
|
7
|
-
|
|
8
|
-
用法:
|
|
9
|
-
python3 preinject_ids.py --features features.json [--out ids.json]
|
|
10
|
-
"""
|
|
11
|
-
import argparse
|
|
12
|
-
import glob
|
|
13
|
-
import json
|
|
14
|
-
import os
|
|
15
|
-
import re
|
|
16
|
-
|
|
17
|
-
TEXT_PAT = re.compile(
|
|
18
|
-
r"^(\s*)(Text|Button|Toggle|Checkbox|Radio|MenuItem|Image|Row|Column)\("
|
|
19
|
-
r"[^\n]*?['\"](.*?)['\"][^\n]*\)?\s*$")
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def read(p):
|
|
23
|
-
return open(p, encoding='utf-8').read()
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def insert_after_component(lines, i):
|
|
27
|
-
depth = 0
|
|
28
|
-
started = False
|
|
29
|
-
for k in range(i, min(len(lines), i + 40)):
|
|
30
|
-
depth += lines[k].count('(') - lines[k].count(')')
|
|
31
|
-
started = True
|
|
32
|
-
if started and depth <= 0 and k > i:
|
|
33
|
-
return k + 1
|
|
34
|
-
if started and depth < 0:
|
|
35
|
-
return k
|
|
36
|
-
return min(len(lines), i + 12)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def find_and_inject(page_file, target, seq):
|
|
40
|
-
"""三路定位注入:资源文案 → 精确/模糊文本 → 行号。返回 (id or None, modified_src)。"""
|
|
41
|
-
src = read(page_file)
|
|
42
|
-
lines = src.split('\n')
|
|
43
|
-
tid = f"tc_{os.path.splitext(os.path.basename(page_file))[0].lower()}_{seq}"
|
|
44
|
-
|
|
45
|
-
if target and re.search(rf"\.id\(\s*'{re.escape(target)}'", src):
|
|
46
|
-
return target, src
|
|
47
|
-
|
|
48
|
-
if target and 'app.string.' in src:
|
|
49
|
-
modroot = page_file
|
|
50
|
-
for _ in range(4):
|
|
51
|
-
modroot = os.path.dirname(modroot)
|
|
52
|
-
cand = os.path.join(modroot, 'resources', 'base', 'element', 'string.json')
|
|
53
|
-
if os.path.exists(cand):
|
|
54
|
-
resname = None
|
|
55
|
-
try:
|
|
56
|
-
for item in json.load(open(cand, encoding='utf-8')).get('string', []):
|
|
57
|
-
if item.get('value') == target:
|
|
58
|
-
resname = item.get('name')
|
|
59
|
-
break
|
|
60
|
-
except Exception:
|
|
61
|
-
pass
|
|
62
|
-
if resname:
|
|
63
|
-
break
|
|
64
|
-
if resname:
|
|
65
|
-
rx = re.compile(r"[\$r]\(\s*'app\.string\." + re.escape(resname) + r"'")
|
|
66
|
-
for i, ln in enumerate(lines):
|
|
67
|
-
if rx.search(ln):
|
|
68
|
-
for k in range(i, min(len(lines), i + 10)):
|
|
69
|
-
if re.match(r'^\s*\.[a-zA-Z]', lines[k]):
|
|
70
|
-
indent = re.match(r'(\s*)', lines[k]).group(1)
|
|
71
|
-
lines.insert(k, f"{indent}.id('{tid}')")
|
|
72
|
-
return tid, '\n'.join(lines)
|
|
73
|
-
indent = re.match(r'(\s*)', ln).group(1)
|
|
74
|
-
lines.insert(i + 1, f"{indent} .id('{tid}')")
|
|
75
|
-
return tid, '\n'.join(lines)
|
|
76
|
-
|
|
77
|
-
if target:
|
|
78
|
-
for i, ln in enumerate(lines):
|
|
79
|
-
m = TEXT_PAT.match(ln)
|
|
80
|
-
if not m:
|
|
81
|
-
continue
|
|
82
|
-
if m.group(3) == target or (target in m.group(3) or m.group(3) in target):
|
|
83
|
-
seg = '\n'.join(lines[i:i + 25])
|
|
84
|
-
if re.search(r'\.id\(', seg):
|
|
85
|
-
idm = re.search(r"\.id\(\s*'([^']+)'", seg)
|
|
86
|
-
if idm:
|
|
87
|
-
return idm.group(1), src
|
|
88
|
-
for k in range(i + 1, min(len(lines), i + 15)):
|
|
89
|
-
if re.match(r'^\s*\.[a-zA-Z]', lines[k]):
|
|
90
|
-
indent = re.match(r'(\s*)', lines[k]).group(1)
|
|
91
|
-
lines.insert(k, f"{indent}.id('{tid}')")
|
|
92
|
-
return tid, '\n'.join(lines)
|
|
93
|
-
j = insert_after_component(lines, i)
|
|
94
|
-
lines.insert(j, f"{m.group(1)} .id('{tid}')")
|
|
95
|
-
return tid, '\n'.join(lines)
|
|
96
|
-
|
|
97
|
-
tc = re.match(r'line(\d+):(\w+)', target or '')
|
|
98
|
-
if tc:
|
|
99
|
-
i = int(tc.group(1)) - 1
|
|
100
|
-
if 0 <= i < len(lines):
|
|
101
|
-
for k in range(i + 1, min(len(lines), i + 9)):
|
|
102
|
-
if re.match(r'^\s*\.[a-zA-Z]', lines[k]):
|
|
103
|
-
indent = re.match(r'(\s*)', lines[k]).group(1)
|
|
104
|
-
lines.insert(k, f"{indent}.id('{tid}')")
|
|
105
|
-
return tid, '\n'.join(lines)
|
|
106
|
-
for k in range(i, max(0, i - 8), -1):
|
|
107
|
-
if re.match(r'^\s*\.[a-zA-Z]', lines[k]):
|
|
108
|
-
indent = re.match(r'(\s*)', lines[k]).group(1)
|
|
109
|
-
lines.insert(k + 1, f"{indent}.id('{tid}')")
|
|
110
|
-
return tid, '\n'.join(lines)
|
|
111
|
-
return None, src
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def main():
|
|
115
|
-
ap = argparse.ArgumentParser()
|
|
116
|
-
ap.add_argument('--features', required=True)
|
|
117
|
-
ap.add_argument('--out', default=None)
|
|
118
|
-
args = ap.parse_args()
|
|
119
|
-
feats = json.load(open(args.features, encoding='utf-8'))
|
|
120
|
-
project = feats['project_dir']
|
|
121
|
-
out = args.out or os.path.join(os.path.dirname(args.features), 'ids.json')
|
|
122
|
-
mapping = {}
|
|
123
|
-
if os.path.exists(out):
|
|
124
|
-
try:
|
|
125
|
-
mapping = json.load(open(out, encoding='utf-8'))
|
|
126
|
-
except Exception:
|
|
127
|
-
mapping = {}
|
|
128
|
-
injected = existing = missed = 0
|
|
129
|
-
for pg in feats.get('pages', []):
|
|
130
|
-
fpath = pg.get('file')
|
|
131
|
-
if not fpath or not os.path.exists(fpath):
|
|
132
|
-
continue
|
|
133
|
-
seq = 0
|
|
134
|
-
for it in pg.get('interactions', []):
|
|
135
|
-
target = it.get('target')
|
|
136
|
-
desc = it.get('desc')
|
|
137
|
-
if not target:
|
|
138
|
-
continue
|
|
139
|
-
seq += 1
|
|
140
|
-
key = f"{pg['page']}:{desc or target}"
|
|
141
|
-
prev = mapping.get(key)
|
|
142
|
-
if prev and re.search(rf"\.id\(\s*'{re.escape(prev)}'", read(fpath)):
|
|
143
|
-
existing += 1
|
|
144
|
-
continue
|
|
145
|
-
if it.get('has_id'):
|
|
146
|
-
existing += 1
|
|
147
|
-
mapping[key] = target
|
|
148
|
-
continue
|
|
149
|
-
tid, new_src = find_and_inject(fpath, target, seq)
|
|
150
|
-
if not tid:
|
|
151
|
-
tid2, new_src2 = find_and_inject(fpath, f"line{it.get('line')}:{it.get('type')}", seq)
|
|
152
|
-
tid, new_src = tid2 or tid, new_src2 if tid2 else new_src
|
|
153
|
-
if tid:
|
|
154
|
-
disk = read(fpath)
|
|
155
|
-
if new_src != disk:
|
|
156
|
-
open(fpath, 'w', encoding='utf-8').write(new_src) # 逐次落盘(页尾统一写会丢同页其余注入)
|
|
157
|
-
injected += 1
|
|
158
|
-
else:
|
|
159
|
-
existing += 1
|
|
160
|
-
mapping[key] = tid
|
|
161
|
-
else:
|
|
162
|
-
missed += 1
|
|
163
|
-
for res_id in re.findall(r"\.id\('([a-z][a-z0-9_]+)'\)", '\n'.join(
|
|
164
|
-
read(p) for p in glob.glob(os.path.join(project, '**', '*.ets'), recursive=True)
|
|
165
|
-
if 'oh_modules' not in p))[:0]:
|
|
166
|
-
pass
|
|
167
|
-
# 连续 .id() 消除(TD-006):注入点组件已有语义 id 时,删除本轮新插的机械 id,
|
|
168
|
-
# mapping 改指向已有 id——运行时链式 .id() 后者覆盖前者,机械 id 失效且污染源码
|
|
169
|
-
import glob as _g
|
|
170
|
-
fixed = 0
|
|
171
|
-
for pg in feats.get('pages', []):
|
|
172
|
-
fp = pg.get('file')
|
|
173
|
-
if not fp or not os.path.exists(fp):
|
|
174
|
-
continue
|
|
175
|
-
try:
|
|
176
|
-
src = open(fp, encoding='utf-8').read()
|
|
177
|
-
except Exception:
|
|
178
|
-
continue
|
|
179
|
-
new_src = src
|
|
180
|
-
for m in re.finditer(r"\.id\('(tc_[^']+)'\)\s*\.id\('([^']+)'\)", src):
|
|
181
|
-
dead, alive = m.group(1), m.group(2)
|
|
182
|
-
new_src = new_src.replace(f".id('{dead}')\n", '', 1).replace(f".id('{dead}')", '', 1)
|
|
183
|
-
for k, v in list(mapping.items()):
|
|
184
|
-
if v == dead:
|
|
185
|
-
mapping[k] = alive
|
|
186
|
-
fixed += 1
|
|
187
|
-
if new_src != src:
|
|
188
|
-
open(fp, 'w', encoding='utf-8').write(new_src)
|
|
189
|
-
# ID 稳定性门禁(TD-013):旧 ids.json 被用例引用的 id 若在新轮消失 → BLOCKER 阻断,
|
|
190
|
-
# 禁止静默让用例回退 text 定位(run 期间 id 漂移的机制化防线)
|
|
191
|
-
old_out = out + '.prev'
|
|
192
|
-
if os.path.exists(out):
|
|
193
|
-
import shutil as _sh
|
|
194
|
-
_sh.copy(out, old_out)
|
|
195
|
-
try:
|
|
196
|
-
old_map = json.load(open(old_out, encoding='utf-8'))
|
|
197
|
-
except Exception:
|
|
198
|
-
old_map = {}
|
|
199
|
-
gone = {k: v for k, v in old_map.items() if v not in set(mapping.values())
|
|
200
|
-
and v not in {m for m in mapping.values()}}
|
|
201
|
-
if gone:
|
|
202
|
-
print(f'✗ BLOCKER:上一轮 {len(gone)} 个 id 本轮消失(用例可能引用,禁静默回退 text):')
|
|
203
|
-
for k, v in list(gone.items())[:6]:
|
|
204
|
-
print(f' {k} → {v}')
|
|
205
|
-
print(' 处置:核对注入冲突/页面改动,或显式重命名并同步全部用例后重跑。')
|
|
206
|
-
json.dump(mapping, open(out, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
|
|
207
|
-
print(f'ID 预置完成: 新注入 {injected} / 已有 {existing} / 未定位 {missed}'
|
|
208
|
-
f' / 连续id消除 {fixed} → {out}')
|
|
209
|
-
|
|
210
|
-
# 语法快速自检:检测连续 .id() 调用(最常见注入冲突)
|
|
211
|
-
import re as _re
|
|
212
|
-
_warn = 0
|
|
213
|
-
for pg in feats.get('pages', []):
|
|
214
|
-
fp = pg.get('file')
|
|
215
|
-
if not fp or not os.path.exists(fp):
|
|
216
|
-
continue
|
|
217
|
-
try:
|
|
218
|
-
src = open(fp, encoding='utf-8').read()
|
|
219
|
-
for m in _re.finditer(r"\.id\(\s*'[^']+'\s*\)\s*\.id\(", src):
|
|
220
|
-
print(f' ⚠️ 语法风险: {os.path.basename(fp)} 存在连续 .id() 调用')
|
|
221
|
-
_warn += 1
|
|
222
|
-
except Exception:
|
|
223
|
-
pass
|
|
224
|
-
if _warn:
|
|
225
|
-
print(f'⚠️ 发现 {_warn} 处语法风险——建议先跑构建验证再使用 ids.json')
|
|
226
|
-
else:
|
|
227
|
-
print('✓ 语法快速检查通过')
|
|
228
|
-
print('⚠️ 注入后必须 rebuild + uninstall&&install 再执行脚本')
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if __name__ == '__main__':
|
|
232
|
-
main()
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""preinject_ids.py — ID 预置(铁律①的机械强制,先于脚本编写,不依赖 cases DSL)。
|
|
3
|
+
|
|
4
|
+
输入 features.json(scan_features 产物),对全部 has_id=False 的交互点批量注入
|
|
5
|
+
`.id('tc_<page>_<n>')`(资源文案/精确模糊文本/行号三路定位,属性链内插桩永远合法),
|
|
6
|
+
输出 ids.json 映射(脚本编写只认此表与既有 ID)。注入后须 rebuild + uninstall&&install。
|
|
7
|
+
|
|
8
|
+
用法:
|
|
9
|
+
python3 preinject_ids.py --features features.json [--out ids.json]
|
|
10
|
+
"""
|
|
11
|
+
import argparse
|
|
12
|
+
import glob
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
|
|
17
|
+
TEXT_PAT = re.compile(
|
|
18
|
+
r"^(\s*)(Text|Button|Toggle|Checkbox|Radio|MenuItem|Image|Row|Column)\("
|
|
19
|
+
r"[^\n]*?['\"](.*?)['\"][^\n]*\)?\s*$")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def read(p):
|
|
23
|
+
return open(p, encoding='utf-8').read()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def insert_after_component(lines, i):
|
|
27
|
+
depth = 0
|
|
28
|
+
started = False
|
|
29
|
+
for k in range(i, min(len(lines), i + 40)):
|
|
30
|
+
depth += lines[k].count('(') - lines[k].count(')')
|
|
31
|
+
started = True
|
|
32
|
+
if started and depth <= 0 and k > i:
|
|
33
|
+
return k + 1
|
|
34
|
+
if started and depth < 0:
|
|
35
|
+
return k
|
|
36
|
+
return min(len(lines), i + 12)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def find_and_inject(page_file, target, seq):
|
|
40
|
+
"""三路定位注入:资源文案 → 精确/模糊文本 → 行号。返回 (id or None, modified_src)。"""
|
|
41
|
+
src = read(page_file)
|
|
42
|
+
lines = src.split('\n')
|
|
43
|
+
tid = f"tc_{os.path.splitext(os.path.basename(page_file))[0].lower()}_{seq}"
|
|
44
|
+
|
|
45
|
+
if target and re.search(rf"\.id\(\s*'{re.escape(target)}'", src):
|
|
46
|
+
return target, src
|
|
47
|
+
|
|
48
|
+
if target and 'app.string.' in src:
|
|
49
|
+
modroot = page_file
|
|
50
|
+
for _ in range(4):
|
|
51
|
+
modroot = os.path.dirname(modroot)
|
|
52
|
+
cand = os.path.join(modroot, 'resources', 'base', 'element', 'string.json')
|
|
53
|
+
if os.path.exists(cand):
|
|
54
|
+
resname = None
|
|
55
|
+
try:
|
|
56
|
+
for item in json.load(open(cand, encoding='utf-8')).get('string', []):
|
|
57
|
+
if item.get('value') == target:
|
|
58
|
+
resname = item.get('name')
|
|
59
|
+
break
|
|
60
|
+
except Exception:
|
|
61
|
+
pass
|
|
62
|
+
if resname:
|
|
63
|
+
break
|
|
64
|
+
if resname:
|
|
65
|
+
rx = re.compile(r"[\$r]\(\s*'app\.string\." + re.escape(resname) + r"'")
|
|
66
|
+
for i, ln in enumerate(lines):
|
|
67
|
+
if rx.search(ln):
|
|
68
|
+
for k in range(i, min(len(lines), i + 10)):
|
|
69
|
+
if re.match(r'^\s*\.[a-zA-Z]', lines[k]):
|
|
70
|
+
indent = re.match(r'(\s*)', lines[k]).group(1)
|
|
71
|
+
lines.insert(k, f"{indent}.id('{tid}')")
|
|
72
|
+
return tid, '\n'.join(lines)
|
|
73
|
+
indent = re.match(r'(\s*)', ln).group(1)
|
|
74
|
+
lines.insert(i + 1, f"{indent} .id('{tid}')")
|
|
75
|
+
return tid, '\n'.join(lines)
|
|
76
|
+
|
|
77
|
+
if target:
|
|
78
|
+
for i, ln in enumerate(lines):
|
|
79
|
+
m = TEXT_PAT.match(ln)
|
|
80
|
+
if not m:
|
|
81
|
+
continue
|
|
82
|
+
if m.group(3) == target or (target in m.group(3) or m.group(3) in target):
|
|
83
|
+
seg = '\n'.join(lines[i:i + 25])
|
|
84
|
+
if re.search(r'\.id\(', seg):
|
|
85
|
+
idm = re.search(r"\.id\(\s*'([^']+)'", seg)
|
|
86
|
+
if idm:
|
|
87
|
+
return idm.group(1), src
|
|
88
|
+
for k in range(i + 1, min(len(lines), i + 15)):
|
|
89
|
+
if re.match(r'^\s*\.[a-zA-Z]', lines[k]):
|
|
90
|
+
indent = re.match(r'(\s*)', lines[k]).group(1)
|
|
91
|
+
lines.insert(k, f"{indent}.id('{tid}')")
|
|
92
|
+
return tid, '\n'.join(lines)
|
|
93
|
+
j = insert_after_component(lines, i)
|
|
94
|
+
lines.insert(j, f"{m.group(1)} .id('{tid}')")
|
|
95
|
+
return tid, '\n'.join(lines)
|
|
96
|
+
|
|
97
|
+
tc = re.match(r'line(\d+):(\w+)', target or '')
|
|
98
|
+
if tc:
|
|
99
|
+
i = int(tc.group(1)) - 1
|
|
100
|
+
if 0 <= i < len(lines):
|
|
101
|
+
for k in range(i + 1, min(len(lines), i + 9)):
|
|
102
|
+
if re.match(r'^\s*\.[a-zA-Z]', lines[k]):
|
|
103
|
+
indent = re.match(r'(\s*)', lines[k]).group(1)
|
|
104
|
+
lines.insert(k, f"{indent}.id('{tid}')")
|
|
105
|
+
return tid, '\n'.join(lines)
|
|
106
|
+
for k in range(i, max(0, i - 8), -1):
|
|
107
|
+
if re.match(r'^\s*\.[a-zA-Z]', lines[k]):
|
|
108
|
+
indent = re.match(r'(\s*)', lines[k]).group(1)
|
|
109
|
+
lines.insert(k + 1, f"{indent}.id('{tid}')")
|
|
110
|
+
return tid, '\n'.join(lines)
|
|
111
|
+
return None, src
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def main():
|
|
115
|
+
ap = argparse.ArgumentParser()
|
|
116
|
+
ap.add_argument('--features', required=True)
|
|
117
|
+
ap.add_argument('--out', default=None)
|
|
118
|
+
args = ap.parse_args()
|
|
119
|
+
feats = json.load(open(args.features, encoding='utf-8'))
|
|
120
|
+
project = feats['project_dir']
|
|
121
|
+
out = args.out or os.path.join(os.path.dirname(args.features), 'ids.json')
|
|
122
|
+
mapping = {}
|
|
123
|
+
if os.path.exists(out):
|
|
124
|
+
try:
|
|
125
|
+
mapping = json.load(open(out, encoding='utf-8'))
|
|
126
|
+
except Exception:
|
|
127
|
+
mapping = {}
|
|
128
|
+
injected = existing = missed = 0
|
|
129
|
+
for pg in feats.get('pages', []):
|
|
130
|
+
fpath = pg.get('file')
|
|
131
|
+
if not fpath or not os.path.exists(fpath):
|
|
132
|
+
continue
|
|
133
|
+
seq = 0
|
|
134
|
+
for it in pg.get('interactions', []):
|
|
135
|
+
target = it.get('target')
|
|
136
|
+
desc = it.get('desc')
|
|
137
|
+
if not target:
|
|
138
|
+
continue
|
|
139
|
+
seq += 1
|
|
140
|
+
key = f"{pg['page']}:{desc or target}"
|
|
141
|
+
prev = mapping.get(key)
|
|
142
|
+
if prev and re.search(rf"\.id\(\s*'{re.escape(prev)}'", read(fpath)):
|
|
143
|
+
existing += 1
|
|
144
|
+
continue
|
|
145
|
+
if it.get('has_id'):
|
|
146
|
+
existing += 1
|
|
147
|
+
mapping[key] = target
|
|
148
|
+
continue
|
|
149
|
+
tid, new_src = find_and_inject(fpath, target, seq)
|
|
150
|
+
if not tid:
|
|
151
|
+
tid2, new_src2 = find_and_inject(fpath, f"line{it.get('line')}:{it.get('type')}", seq)
|
|
152
|
+
tid, new_src = tid2 or tid, new_src2 if tid2 else new_src
|
|
153
|
+
if tid:
|
|
154
|
+
disk = read(fpath)
|
|
155
|
+
if new_src != disk:
|
|
156
|
+
open(fpath, 'w', encoding='utf-8').write(new_src) # 逐次落盘(页尾统一写会丢同页其余注入)
|
|
157
|
+
injected += 1
|
|
158
|
+
else:
|
|
159
|
+
existing += 1
|
|
160
|
+
mapping[key] = tid
|
|
161
|
+
else:
|
|
162
|
+
missed += 1
|
|
163
|
+
for res_id in re.findall(r"\.id\('([a-z][a-z0-9_]+)'\)", '\n'.join(
|
|
164
|
+
read(p) for p in glob.glob(os.path.join(project, '**', '*.ets'), recursive=True)
|
|
165
|
+
if 'oh_modules' not in p))[:0]:
|
|
166
|
+
pass
|
|
167
|
+
# 连续 .id() 消除(TD-006):注入点组件已有语义 id 时,删除本轮新插的机械 id,
|
|
168
|
+
# mapping 改指向已有 id——运行时链式 .id() 后者覆盖前者,机械 id 失效且污染源码
|
|
169
|
+
import glob as _g
|
|
170
|
+
fixed = 0
|
|
171
|
+
for pg in feats.get('pages', []):
|
|
172
|
+
fp = pg.get('file')
|
|
173
|
+
if not fp or not os.path.exists(fp):
|
|
174
|
+
continue
|
|
175
|
+
try:
|
|
176
|
+
src = open(fp, encoding='utf-8').read()
|
|
177
|
+
except Exception:
|
|
178
|
+
continue
|
|
179
|
+
new_src = src
|
|
180
|
+
for m in re.finditer(r"\.id\('(tc_[^']+)'\)\s*\.id\('([^']+)'\)", src):
|
|
181
|
+
dead, alive = m.group(1), m.group(2)
|
|
182
|
+
new_src = new_src.replace(f".id('{dead}')\n", '', 1).replace(f".id('{dead}')", '', 1)
|
|
183
|
+
for k, v in list(mapping.items()):
|
|
184
|
+
if v == dead:
|
|
185
|
+
mapping[k] = alive
|
|
186
|
+
fixed += 1
|
|
187
|
+
if new_src != src:
|
|
188
|
+
open(fp, 'w', encoding='utf-8').write(new_src)
|
|
189
|
+
# ID 稳定性门禁(TD-013):旧 ids.json 被用例引用的 id 若在新轮消失 → BLOCKER 阻断,
|
|
190
|
+
# 禁止静默让用例回退 text 定位(run 期间 id 漂移的机制化防线)
|
|
191
|
+
old_out = out + '.prev'
|
|
192
|
+
if os.path.exists(out):
|
|
193
|
+
import shutil as _sh
|
|
194
|
+
_sh.copy(out, old_out)
|
|
195
|
+
try:
|
|
196
|
+
old_map = json.load(open(old_out, encoding='utf-8'))
|
|
197
|
+
except Exception:
|
|
198
|
+
old_map = {}
|
|
199
|
+
gone = {k: v for k, v in old_map.items() if v not in set(mapping.values())
|
|
200
|
+
and v not in {m for m in mapping.values()}}
|
|
201
|
+
if gone:
|
|
202
|
+
print(f'✗ BLOCKER:上一轮 {len(gone)} 个 id 本轮消失(用例可能引用,禁静默回退 text):')
|
|
203
|
+
for k, v in list(gone.items())[:6]:
|
|
204
|
+
print(f' {k} → {v}')
|
|
205
|
+
print(' 处置:核对注入冲突/页面改动,或显式重命名并同步全部用例后重跑。')
|
|
206
|
+
json.dump(mapping, open(out, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
|
|
207
|
+
print(f'ID 预置完成: 新注入 {injected} / 已有 {existing} / 未定位 {missed}'
|
|
208
|
+
f' / 连续id消除 {fixed} → {out}')
|
|
209
|
+
|
|
210
|
+
# 语法快速自检:检测连续 .id() 调用(最常见注入冲突)
|
|
211
|
+
import re as _re
|
|
212
|
+
_warn = 0
|
|
213
|
+
for pg in feats.get('pages', []):
|
|
214
|
+
fp = pg.get('file')
|
|
215
|
+
if not fp or not os.path.exists(fp):
|
|
216
|
+
continue
|
|
217
|
+
try:
|
|
218
|
+
src = open(fp, encoding='utf-8').read()
|
|
219
|
+
for m in _re.finditer(r"\.id\(\s*'[^']+'\s*\)\s*\.id\(", src):
|
|
220
|
+
print(f' ⚠️ 语法风险: {os.path.basename(fp)} 存在连续 .id() 调用')
|
|
221
|
+
_warn += 1
|
|
222
|
+
except Exception:
|
|
223
|
+
pass
|
|
224
|
+
if _warn:
|
|
225
|
+
print(f'⚠️ 发现 {_warn} 处语法风险——建议先跑构建验证再使用 ids.json')
|
|
226
|
+
else:
|
|
227
|
+
print('✓ 语法快速检查通过')
|
|
228
|
+
print('⚠️ 注入后必须 rebuild + uninstall&&install 再执行脚本')
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
if __name__ == '__main__':
|
|
232
|
+
main()
|