@hupan56/wlkj 3.1.6 → 3.1.8

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/package.json CHANGED
@@ -1,29 +1,29 @@
1
- {
2
- "name": "@hupan56/wlkj",
3
- "version": "3.1.6",
4
- "description": "AI Product R&D Workflow - PRD/Prototype/Search/Task/Report",
5
- "bin": {
6
- "wlkj": "bin/cli.js"
7
- },
8
- "files": [
9
- "bin/",
10
- "templates/",
11
- "MAC-VERIFY.md"
12
- ],
13
- "keywords": [
14
- "workflow",
15
- "ai",
16
- "prd",
17
- "pipeline",
18
- "qoder",
19
- "product",
20
- "team"
21
- ],
22
- "license": "MIT",
23
- "publishConfig": {
24
- "access": "public"
25
- },
26
- "engines": {
27
- "node": ">=16"
28
- }
1
+ {
2
+ "name": "@hupan56/wlkj",
3
+ "version": "3.1.8",
4
+ "description": "AI Product R&D Workflow - PRD/Prototype/Search/Task/Report",
5
+ "bin": {
6
+ "wlkj": "bin/cli.js"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "templates/",
11
+ "MAC-VERIFY.md"
12
+ ],
13
+ "keywords": [
14
+ "workflow",
15
+ "ai",
16
+ "prd",
17
+ "pipeline",
18
+ "qoder",
19
+ "product",
20
+ "team"
21
+ ],
22
+ "license": "MIT",
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "engines": {
27
+ "node": ">=16"
28
+ }
29
29
  }
@@ -1,4 +1,5 @@
1
1
  ---
2
+ name: wl-req
2
3
  description: "需求管理: 看需求/看产出/发布"
3
4
  argument-hint: "[REQ-ID] [发布]"
4
5
  ---
@@ -10,7 +10,7 @@ for _ in range(10):
10
10
  break
11
11
  if _cp not in _s.path: _s.path.insert(0, _cp)
12
12
  from bootstrap import setup; setup()
13
- from foundation.core.paths import get_repo_root
13
+ from foundation.core.paths import get_repo_root
14
14
 
15
15
  """
16
16
  gen_design_doc.py - 自动生成 DESIGN.md (给 AI 的设计系统说明书)
@@ -43,7 +43,7 @@ except (AttributeError, TypeError, OSError, IOError):
43
43
  except Exception:
44
44
  pass
45
45
 
46
- BASE = get_repo_root()
46
+ BASE = get_repo_root()
47
47
  INDEX_DIR = os.path.join(BASE, 'data', 'index')
48
48
  OUTPUT_DIR = os.path.join(BASE, 'data', 'design')
49
49
 
@@ -269,8 +269,14 @@ def gen_design_doc():
269
269
  lines.append(f'- `{field_name}` -> {title} (used {count}x)')
270
270
  lines.append('')
271
271
 
272
- # ========== 4.5 Feature Modules (from prd-features.json) ==========
273
- pf = load_json('prd-features.json')
272
+ # ========== 4.5 Feature Modules (from prd-features.json, DuckDB-backed) ==========
273
+ # prd-features.json 文件不存在, 真实数据在 DuckDB entities(FEATURE 行)
274
+ # 用 search_index.load_index 走 DuckDB (load_json 只读文件, 永远返回 {})。
275
+ try:
276
+ from domain.kg.search.search_index import load_index as _load_idx
277
+ pf = _load_idx('prd-features.json') or {}
278
+ except Exception:
279
+ pf = load_json('prd-features.json')
274
280
  features = pf.get('features', {}) if isinstance(pf, dict) else {}
275
281
  if features:
276
282
  lines.append('## 4.5 Feature Modules (系统有哪些功能模块)')
@@ -288,8 +294,14 @@ def gen_design_doc():
288
294
  lines.append(f' - 标杆页面: {sample_pages}')
289
295
  lines.append('')
290
296
 
291
- # ========== 4.6 Real Buttons (from entity-registry.json) ==========
292
- er = load_json('entity-registry.json')
297
+ # ========== 4.6 Real Buttons (from entity-registry.json, DuckDB-backed) ==========
298
+ # entity-registry.json 文件不存在, 真实数据在 DuckDB entities(BUTTON 行, 2675 个)
299
+ # 用 search_index.load_index 走 DuckDB (load_json 只读文件, 永远返回 {} → 按钮段永远空)。
300
+ try:
301
+ from domain.kg.search.search_index import load_index as _load_idx
302
+ er = _load_idx('entity-registry.json') or {}
303
+ except Exception:
304
+ er = load_json('entity-registry.json')
293
305
  entities = er.get('entities', {}) if isinstance(er, dict) else {}
294
306
  button_samples = []
295
307
  for eid, info in list(entities.items())[:500]:
@@ -1,17 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """learn_aggregate.py - 团队学习聚合 (同构 kg_build.py)
3
3
 
4
- # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
5
- import os as _o, sys as _s
6
- _f = _o.path.abspath(__file__)
7
- for _ in range(10):
8
- _f = _o.path.dirname(_f)
9
- _cp = _o.path.join(_f, 'foundation')
10
- if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
11
- break
12
- if _cp not in _s.path: _s.path.insert(0, _cp)
13
- from bootstrap import setup; setup()
14
-
15
4
  扫所有人的 workspace/members/{dev}/journal/feedback.jsonl
16
5
  → 增量写入 kg.duckdb 的 learning_events 表 (团队单一真源)
17
6
 
@@ -26,6 +15,19 @@ from bootstrap import setup; setup()
26
15
  python learn_aggregate.py # 增量
27
16
  python learn_aggregate.py --rebuild # 全量重建
28
17
  """
18
+ # v3.0 路径自举: 引导到 foundation/bootstrap, 统一 sys.path 逻辑
19
+ # ⚠ 必须在 module docstring 之外! 旧版把这块包进 docstring → bootstrap 永不执行
20
+ # → from foundation... ModuleNotFoundError → 学习聚合全链路死。
21
+ import os as _o, sys as _s
22
+ _f = _o.path.abspath(__file__)
23
+ for _ in range(10):
24
+ _f = _o.path.dirname(_f)
25
+ _cp = _o.path.join(_f, 'foundation')
26
+ if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
27
+ break
28
+ if _cp not in _s.path: _s.path.insert(0, _cp)
29
+ from bootstrap import setup; setup()
30
+
29
31
  import os
30
32
  import sys
31
33
  import json
@@ -33,9 +35,6 @@ import hashlib
33
35
  import time
34
36
  from datetime import datetime
35
37
 
36
- SCRIPTS = os.path.dirname(os.path.abspath(__file__))
37
- sys.path.insert(0, SCRIPTS)
38
-
39
38
  from foundation.core.paths import MEMBERS_DIR
40
39
  from domain.kg.storage.kg_duckdb import get_db, init_schema # v3.0: kg_duckdb 已迁入 kg/ 子包
41
40
 
@@ -258,6 +258,42 @@ def _duckdb_load(filename):
258
258
  result[proj] = {'files': vf or 0, 'classes': 0, 'apis': 0}
259
259
  return result if result else None
260
260
 
261
+ elif filename == 'entity-registry.json':
262
+ # 实体注册表 (fill_prototype 取真实按钮文案 / gen_design_doc 取按钮样本)。
263
+ # 真实数据在 DuckDB entities 表 (6131 行: BUTTON/ENDPOINT/FEATURE/...),
264
+ # 不再有 entity-registry.json 文件 (kg.py 注释: 通常不存在)。
265
+ # 重建为 {'entities': {id: {'type':..., 'canonical':..., 'cn':..., 'properties': props}}}
266
+ # 消费方 extract_real_buttons 读 entities[eid]['properties']['button_text']。
267
+ def _entity_loader(con):
268
+ rows = con.execute("SELECT id, type, canonical, cn, props FROM entities").fetchall()
269
+ result = {'entities': {}}
270
+ for eid, etype, canon, cn, props in rows:
271
+ try:
272
+ props_d = json.loads(props) if props else {}
273
+ except Exception:
274
+ props_d = {}
275
+ result['entities'][eid] = {
276
+ 'type': etype, 'canonical': canon, 'cn': cn or '',
277
+ 'properties': props_d,
278
+ }
279
+ return result if result['entities'] else None
280
+ return _load_with_pickle_cache(con, 'entity-registry', _entity_loader)
281
+
282
+ elif filename == 'prd-features.json':
283
+ # 从 entities 表 FEATURE 行重建 (prd-features.json 文件不存在)。
284
+ # gen_design_doc 取 feature 模块样本。结构: {'features': {canonical: {'cn':..., 'properties':...}}}
285
+ def _feature_loader(con):
286
+ rows = con.execute("SELECT id, canonical, cn, props FROM entities WHERE type='FEATURE'").fetchall()
287
+ result = {'features': {}}
288
+ for eid, canon, cn, props in rows:
289
+ try:
290
+ props_d = json.loads(props) if props else {}
291
+ except Exception:
292
+ props_d = {}
293
+ result['features'][canon or eid] = {'cn': cn or '', 'id': eid, 'properties': props_d}
294
+ return result if result['features'] else None
295
+ return _load_with_pickle_cache(con, 'prd-features', _feature_loader)
296
+
261
297
  except Exception:
262
298
  return None
263
299
  return None
@@ -141,16 +141,25 @@ def snapshot(repo=None):
141
141
  data["index_age_days"] = _index_freshness(meta)
142
142
 
143
143
  # 3. EVA 评估历史 (健康度趋势)
144
- data["eval_history"] = _read_jsonl_tail(
145
- os.path.join(qd, "learning", "eval-history.jsonl"), n=10)
144
+ # ⚠ 真实路径是 data/learning/eval-history.jsonl (旧版写 .qoder/learning/ 是历史 bug,
145
+ # eval_prd.py/learn.py 早改了, 这里要对齐 EVAL_HISTORY_PATH, 否则 /wl-status 永远 null)
146
+ try:
147
+ from foundation.core.paths import EVAL_HISTORY_PATH
148
+ eval_path = str(EVAL_HISTORY_PATH)
149
+ except Exception:
150
+ eval_path = os.path.join(repo, "data", "learning", "eval-history.jsonl")
151
+ data["eval_history"] = _read_jsonl_tail(eval_path, n=10)
146
152
 
147
- # 4. 学习反馈
148
- data["feedback_count"] = None
149
- fb = os.path.join(qd, "learning", "feedback.jsonl")
150
- if os.path.exists(fb):
153
+ # 4. 学习反馈 (扫所有成员的 journal/feedback.jsonl, learn.py 写这里)
154
+ data["feedback_count"] = 0
155
+ members_dir = os.path.join(repo, "workspace", "members")
156
+ if os.path.isdir(members_dir):
151
157
  try:
152
- with open(fb, encoding="utf-8") as f:
153
- data["feedback_count"] = sum(1 for _ in f)
158
+ for m in os.listdir(members_dir):
159
+ fb = os.path.join(members_dir, m, "journal", "feedback.jsonl")
160
+ if os.path.isfile(fb):
161
+ with open(fb, encoding="utf-8") as f:
162
+ data["feedback_count"] += sum(1 for _ in f)
154
163
  except Exception:
155
164
  pass
156
165
 
@@ -172,8 +181,8 @@ def snapshot(repo=None):
172
181
  # 8. 任务目录结构
173
182
  data["tasks_dir"] = _list_dir(os.path.join(qd, "workspace", "tasks"))
174
183
 
175
- # 9. 学习模式
176
- data["patterns"] = _list_dir(os.path.join(qd, "learning", "patterns"))
184
+ # 9. 学习模式 (data/learning/patterns/, 非 .qoder/learning/)
185
+ data["patterns"] = _list_dir(os.path.join(repo, "data", "learning", "patterns"))
177
186
 
178
187
  # 10. 成员目录 (团队规模)
179
188
  data["members"] = _list_dir(os.path.join(qd, "workspace", "members"))
@@ -10,7 +10,7 @@ for _ in range(10):
10
10
  break
11
11
  if _cp not in _s.path: _s.path.insert(0, _cp)
12
12
  from bootstrap import setup; setup()
13
- from foundation.core.paths import get_repo_root
13
+ from foundation.core.paths import get_repo_root
14
14
 
15
15
  """
16
16
  archive_prd.py - PRD 按时间归档到个人工作区
@@ -45,44 +45,10 @@ except (AttributeError, TypeError, OSError, IOError):
45
45
  except Exception:
46
46
  pass
47
47
 
48
- # common/ 可被 import(直接当脚本跑时)
49
- _THIS_DIR = os.path.dirname(os.path.abspath(__file__))
50
- _COMMON = os.path.join(_THIS_DIR, "foundation")
51
- if _COMMON not in sys.path:
52
- sys.path.insert(0, _COMMON)
53
-
54
- try:
55
- from paths import get_repo_root, get_developer, MEMBERS_DIR
56
- except ImportError:
57
- # 极端兜底:直接算路径
58
- def get_repo_root():
59
- current = Path(_THIS_DIR).resolve()
60
- for _ in range(10):
61
- if (current / ".qoder").is_dir():
62
- return current
63
- parent = current.parent
64
- if parent == current:
65
- break
66
- current = parent
67
- return Path(_THIS_DIR).parent.parent
68
-
69
- def get_developer(repo_root=None):
70
- dev = (repo_root or get_repo_root()) / ".qoder" / ".developer"
71
- if not dev.is_file():
72
- return None
73
- for enc in ("utf-8", "gbk"):
74
- try:
75
- for line in dev.read_text(encoding=enc).splitlines():
76
- line = line.strip()
77
- if "=" in line and line.split("=")[0].strip() == "name":
78
- return line.split("=", 1)[1].strip() or None
79
- if ":" in line and line.split(":")[0].strip() == "name":
80
- return line.split(":", 1)[1].strip() or None
81
- except (OSError, IOError, UnicodeDecodeError):
82
- continue
83
- return None
84
-
85
- MEMBERS_DIR = None # 下面用 repo_root 拼
48
+ # v3.x: 顶部 bootstrap 已 setup, 直接 import foundation.* (旧版重复尝试 from paths
49
+ # + 手写兜底, 兜底会与 foundation.core.paths 静默漂移, 这里统一走真源)
50
+ from foundation.core.paths import get_repo_root as _grr, get_developer, MEMBERS_DIR
51
+ get_repo_root = _grr # 已在顶部 import, 这里显式重绑确保本模块可见
86
52
 
87
53
 
88
54
  # ============================================================
@@ -24,10 +24,10 @@ import os
24
24
  import sys
25
25
 
26
26
  # ── 路径自举: 让脚本能独立运行 (复用 bootstrap 模式) ──
27
- # task_utils/identity 用相对导入 (.paths), 必须从 common 包上下文导入,
28
- # 因此把 scripts/ 加入 path, common.xxx / task.xxx 访问。
27
+ # 本脚本在 scripts/domain/task/ (scripts 下 2 级), dirname 两次才到 scripts/。
28
+ # 旧版只 dirname 一次 → _SCRIPTS=scripts/domain/ 找不到 foundation 整个 zentao CLI 同步死。
29
29
  _HERE = os.path.dirname(os.path.abspath(__file__))
30
- _SCRIPTS = os.path.dirname(_HERE) # .qoder/scripts
30
+ _SCRIPTS = os.path.dirname(os.path.dirname(_HERE)) # .qoder/scripts (上两级)
31
31
  _COMMON = os.path.join(_SCRIPTS, 'foundation')
32
32
  for _p in (_SCRIPTS, _COMMON):
33
33
  if _p not in sys.path:
@@ -60,15 +60,38 @@ class ZentaoClient:
60
60
 
61
61
  @classmethod
62
62
  def from_config(cls) -> Optional['ZentaoClient']:
63
- """从 .qoder/config.yaml 的 zentao 段构造(CLI 用)。凭据不全返回 None。"""
63
+ """从 .qoder/config.yaml 的 zentao 段构造(CLI 用)。凭据不全返回 None。
64
+
65
+ v3.1.4 设计: url 团队共享(放 config.yaml), user/password 个人凭据(放
66
+ workspace/members/{dev}/.private/secrets/zentao.env, gitignored)。
67
+ 所以这里 url 从 config 读, user/password 从 zentao.env 读 ——
68
+ 与 mcp_launcher.launch_zentao() 完全对齐 (旧版从 config 读 user/password
69
+ → 永远 None, 因为 config 故意不放个人凭据)。
70
+ """
71
+ # ① url 从 config.yaml
72
+ url = ''
64
73
  try:
65
74
  from foundation.core.config import load_config_section, SECTIONS
66
75
  cfg = load_config_section(SECTIONS.ZENTAO)
76
+ url = (cfg.get('url') or '').strip()
67
77
  except Exception:
68
- return None
69
- url = (cfg.get('url') or '').strip()
70
- user = (cfg.get('user') or '').strip()
71
- pwd = (cfg.get('password') or '')
78
+ url = ''
79
+ # user/password 从当前开发者的 .private/secrets/zentao.env
80
+ user = pwd = ''
81
+ try:
82
+ from foundation.core.paths import get_secrets_dir, get_developer
83
+ dev = get_developer()
84
+ if dev:
85
+ env_file = os.path.join(get_secrets_dir(dev), 'zentao.env')
86
+ if os.path.isfile(env_file):
87
+ for line in open(env_file, encoding='utf-8'):
88
+ line = line.strip()
89
+ if line.startswith('ZENTAO_USER='):
90
+ user = line.split('=', 1)[1].strip().strip('"').strip("'")
91
+ elif line.startswith('ZENTAO_PASSWORD='):
92
+ pwd = line.split('=', 1)[1].strip().strip('"').strip("'")
93
+ except Exception:
94
+ pass
72
95
  if not url or not user or not pwd:
73
96
  return None
74
97
  return cls(url, user, pwd)
@@ -1,81 +1,81 @@
1
- # Figma 协作指南(设计师专用)
2
-
3
- > 工作流出的 HTML 原型 → Figma 精修 → 录回 spec.json 闭环
4
-
5
- ## 为什么这条路可行
6
-
7
- 工作流用 `fill_prototype.py` 出的 HTML 原型有以下特征,**非常适合 html.to.design 插件转换**:
8
-
9
- - CSS 用标准 CSS 变量(`:root { --primary: ... }`),插件能正确解析成 Figma Variables
10
- - 布局用 Flexbox/Grid,转换后图层结构清晰
11
- - 颜色是 Vben 真实 HSL token(不是 #1890ff),导入后直接是正确配色
12
- - 图标是内联 SVG(Ant Design),导入后是可编辑矢量
13
-
14
- ## 完整流程(4 步)
15
-
16
- ### Step 1: PM 出原型(工作流侧)
17
-
18
- PM 在 QoderWork 说"出个 XX 的原型",工作流输出:
19
-
20
- ```
21
- workspace/members/{pm}/drafts/prototype-{需求名}-web.html
22
- ```
23
-
24
- 设计师从 PM 那拿到这个 HTML 文件。
25
-
26
- ### Step 2: 导入 Figma(设计师侧)
27
-
28
- 1. 打开 Figma,新建一个 Frame
29
- 2. 菜单:**Plugins → html.to.design**(by ‹div›RIOTS)
30
- - 没装的话:Figma Community 搜 "html.to.design" 安装(免费)
31
- 3. 插件面板里选 **File** 标签(不是 URL 标签)
32
- 4. 把 HTML 文件**拖进拖放区**(或点选文件)
33
- 5. 点 **Import**,等几秒
34
-
35
- **结果**:HTML 变成完全可编辑的 Figma 图层——每个元素是独立的 Frame/Text/Rectangle,颜色是正确的 Vben 配色。
36
-
37
- ### Step 3: 设计师精修(Figma 侧)
38
-
39
- 设计师在 Figma 里做只有人能做的精修:
40
-
41
- - 调间距/对齐(AI 的间距可能不精确)
42
- - 优化交互细节(hover 态、动画、过渡)
43
- - 换更合适的图标/插图
44
- - 调整信息层级和视觉重点
45
- - 加设计师特有的"手感"
46
-
47
- **注意**:颜色 token(`--primary` 等)尽量别改——它们是系统统一的,改了前端实现时会对不上。
48
-
49
- ### Step 4: 录回工作流(闭环)
50
-
51
- 设计师精修完,在 QoderWork 里说:
52
-
53
- > "录入设计稿"
54
-
55
- 工作流把 Figma 最终稿的关键决策(布局/配色/组件)录成 `data/style/{需求名}-design-spec.json`。
56
-
57
- 从此:
58
- - 下次同需求出原型,工作流**优先用这份 spec**
59
- - 前端拿到的 HTML 原型**匹配设计师的 Figma 终稿**
60
- - 设计师的劳动成果**沉淀进知识图谱**,不浪费
61
-
62
- ## 常见问题
63
-
64
- **Q: 导入后图层太碎怎么办?**
65
- A: html.to.design 会按 HTML DOM 结构生成图层。fill_prototype 出的 HTML 结构已经很干净(侧边栏/搜索栏/表格各自独立),不会太碎。如果某个区域图层太多,选中后用 Figma 的 "Flatten" 合并。
66
-
67
- **Q: 表格数据是假的怎么办?**
68
- A: 对的,原型里的表格行是示例数据("XX示例1")。这是故意的——原型验证的是布局和交互,不是数据。真实数据前端会从 API 取。
69
-
70
- **Q: 图标导入后变位图了?**
71
- A: 不会。工作流用的图标是内联 SVG(不是图片标签),html.to.design 会保留为矢量。如果个别图标显示异常,从 `data/index/icon-reference.json` 找原始 SVG 手动替换。
72
-
73
- **Q: APP 端原型也能导入吗?**
74
- A: 能。`prototype-{需求名}-app.html` 同样是标准 HTML,导入方式一样。APP 原型用的是 Vant 风格(移动端组件库)。
75
-
76
- ## html.to.design 插件安装
77
-
78
- - Figma Community 搜索:`html.to.design`
79
- - 或直接访问:https://www.figma.com/community/plugin/1159123024924461424
80
- - 免费版每次导入有限制,团队版无限
81
- - 也有 Chrome 扩展版(适合截取在线页面)
1
+ # Figma 协作指南(设计师专用)
2
+
3
+ > 工作流出的 HTML 原型 → Figma 精修 → 录回 spec.json 闭环
4
+
5
+ ## 为什么这条路可行
6
+
7
+ 工作流用 `fill_prototype.py` 出的 HTML 原型有以下特征,**非常适合 html.to.design 插件转换**:
8
+
9
+ - CSS 用标准 CSS 变量(`:root { --primary: ... }`),插件能正确解析成 Figma Variables
10
+ - 布局用 Flexbox/Grid,转换后图层结构清晰
11
+ - 颜色是 Vben 真实 HSL token(不是 #1890ff),导入后直接是正确配色
12
+ - 图标是内联 SVG(Ant Design),导入后是可编辑矢量
13
+
14
+ ## 完整流程(4 步)
15
+
16
+ ### Step 1: PM 出原型(工作流侧)
17
+
18
+ PM 在 QoderWork 说"出个 XX 的原型",工作流输出:
19
+
20
+ ```
21
+ workspace/members/{pm}/drafts/prototype-{需求名}-web.html
22
+ ```
23
+
24
+ 设计师从 PM 那拿到这个 HTML 文件。
25
+
26
+ ### Step 2: 导入 Figma(设计师侧)
27
+
28
+ 1. 打开 Figma,新建一个 Frame
29
+ 2. 菜单:**Plugins → html.to.design**(by ‹div›RIOTS)
30
+ - 没装的话:Figma Community 搜 "html.to.design" 安装(免费)
31
+ 3. 插件面板里选 **File** 标签(不是 URL 标签)
32
+ 4. 把 HTML 文件**拖进拖放区**(或点选文件)
33
+ 5. 点 **Import**,等几秒
34
+
35
+ **结果**:HTML 变成完全可编辑的 Figma 图层——每个元素是独立的 Frame/Text/Rectangle,颜色是正确的 Vben 配色。
36
+
37
+ ### Step 3: 设计师精修(Figma 侧)
38
+
39
+ 设计师在 Figma 里做只有人能做的精修:
40
+
41
+ - 调间距/对齐(AI 的间距可能不精确)
42
+ - 优化交互细节(hover 态、动画、过渡)
43
+ - 换更合适的图标/插图
44
+ - 调整信息层级和视觉重点
45
+ - 加设计师特有的"手感"
46
+
47
+ **注意**:颜色 token(`--primary` 等)尽量别改——它们是系统统一的,改了前端实现时会对不上。
48
+
49
+ ### Step 4: 录回工作流(闭环)
50
+
51
+ 设计师精修完,在 QoderWork 里说:
52
+
53
+ > "录入设计稿"
54
+
55
+ 工作流把 Figma 最终稿的关键决策(布局/配色/组件)录成 `data/style/{需求名}-design-spec.json`。
56
+
57
+ 从此:
58
+ - 下次同需求出原型,工作流**优先用这份 spec**
59
+ - 前端拿到的 HTML 原型**匹配设计师的 Figma 终稿**
60
+ - 设计师的劳动成果**沉淀进知识图谱**,不浪费
61
+
62
+ ## 常见问题
63
+
64
+ **Q: 导入后图层太碎怎么办?**
65
+ A: html.to.design 会按 HTML DOM 结构生成图层。fill_prototype 出的 HTML 结构已经很干净(侧边栏/搜索栏/表格各自独立),不会太碎。如果某个区域图层太多,选中后用 Figma 的 "Flatten" 合并。
66
+
67
+ **Q: 表格数据是假的怎么办?**
68
+ A: 对的,原型里的表格行是示例数据("XX示例1")。这是故意的——原型验证的是布局和交互,不是数据。真实数据前端会从 API 取。
69
+
70
+ **Q: 图标导入后变位图了?**
71
+ A: 不会。工作流用的图标是内联 SVG(不是图片标签),html.to.design 会保留为矢量。如果个别图标显示异常,从 `data/index/ref-icon.json` 找原始 SVG 手动替换。
72
+
73
+ **Q: APP 端原型也能导入吗?**
74
+ A: 能。`prototype-{需求名}-app.html` 同样是标准 HTML,导入方式一样。APP 原型用的是 Vant 风格(移动端组件库)。
75
+
76
+ ## html.to.design 插件安装
77
+
78
+ - Figma Community 搜索:`html.to.design`
79
+ - 或直接访问:https://www.figma.com/community/plugin/1159123024924461424
80
+ - 免费版每次导入有限制,团队版无限
81
+ - 也有 Chrome 扩展版(适合截取在线页面)
@@ -32,21 +32,21 @@ Generate production-ready HTML prototypes that match the project's actual UI sty
32
32
  - 若 `.qoder/.current-task` 存在,原型命名带上任务关键词
33
33
  - 平台(Web/APP/Both)必须明确;如未指定,先问用户(见 prd-generator Step 0)
34
34
 
35
- ## ⚠️ 前置门: 用户是否选了"画原型"?(v3.1 新增)
36
-
37
- 本 skill **只在用户明确选择"画原型"后才进入**。
38
-
39
- - 如果上游 `/wl-prd-full` 或 `/wl-prd` 在「批量确认」环节已决定**不画**(PRD 第 1 行
40
- `@contract` 头标记 `prototype: none`),**立即退出本 skill**,不生成任何 HTML。
41
- - 如果上游没有 contract 头(独立调用本 skill),**先问用户**:
42
- `这个需求要画原型吗?(画 / 不画)`
43
- 用户答"不画"→ 退出;用户答"画"→ 继续。
44
- - **绝不擅自决定画/不画**。决策权在用户,本 skill 只执行。
45
-
46
- > 这条门治"AI 默默画了个塑料骨架"的反模式——比如"出交车点检固定为总部全局规则"
47
- > 这种纯规则下发型需求,AI 不该自己往里塞一个配置页原型。
48
-
49
- ---
35
+ ## ⚠️ 前置门: 用户是否选了"画原型"?(v3.1 新增)
36
+
37
+ 本 skill **只在用户明确选择"画原型"后才进入**。
38
+
39
+ - 如果上游 `/wl-prd-full` 或 `/wl-prd` 在「批量确认」环节已决定**不画**(PRD 第 1 行
40
+ `@contract` 头标记 `prototype: none`),**立即退出本 skill**,不生成任何 HTML。
41
+ - 如果上游没有 contract 头(独立调用本 skill),**先问用户**:
42
+ `这个需求要画原型吗?(画 / 不画)`
43
+ 用户答"不画"→ 退出;用户答"画"→ 继续。
44
+ - **绝不擅自决定画/不画**。决策权在用户,本 skill 只执行。
45
+
46
+ > 这条门治"AI 默默画了个塑料骨架"的反模式——比如"出交车点检固定为总部全局规则"
47
+ > 这种纯规则下发型需求,AI 不该自己往里塞一个配置页原型。
48
+
49
+ ---
50
50
  ## Step 0: Know the Platform
51
51
 
52
52
  The platform MUST already be decided (asked by /wl-prd-full Step 0).
@@ -229,12 +229,12 @@ Key design tokens already in the templates (do not override):
229
229
 
230
230
  ## ⚠️ Icons: NEVER emoji, ALWAYS the project's real icon system
231
231
 
232
- Source of truth: **data/index/icon-reference.json** (real usage counts +
232
+ Source of truth: **data/index/ref-icon.json** (real usage counts +
233
233
  ready-to-paste SVGs extracted from the actual projects).
234
234
 
235
- - **Web (fywl-ui)**: Ant Design icons. Inline the SVG from icon-reference.json:
235
+ - **Web (fywl-ui)**: Ant Design icons. Inline the SVG from ref-icon.json:
236
236
  ```html
237
- <span class="anticon">{svg from icon-reference.json web.svg.outlined[...]}</span>
237
+ <span class="anticon">{svg from ref-icon.json web.svg.outlined[...]}</span>
238
238
  <style>.anticon svg{width:1em;height:1em;fill:currentColor}</style>
239
239
  ```
240
240
  Most-used in the real app: Download(303x), Plus(171x), Delete, Search, Edit...
@@ -260,31 +260,31 @@ ready-to-paste SVGs extracted from the actual projects).
260
260
  4. ICS 的菜单来自后端接口(`access_mode: backend`),静态代码里抓不到完整菜单树,
261
261
  所以菜单层级**必须由需求/PRD 提供**,不要去代码里找。
262
262
 
263
- ## 🔒 Step 3.5: 来源锚定 + UTF-8(硬门 #3,eval A2 检查)
264
-
265
- ### 来源锚定(必做)
266
-
267
- 生成的 HTML 必须在 `<head>` 里写一行注释,标注样式来源的真实文件:
268
-
269
- ```html
270
- <!-- @prototype-source: fywl-ui/apps/ics/src/views/xxx/index.vue, fywl-ui/apps/ics/src/api/xxx.ts -->
271
- <meta name="x-source-pages" content="fywl-ui/.../index.vue">
272
- ```
273
-
274
- - 来源文件**必须来自 Step 2 读过的真实 Vue/TS 文件**(prefetch 返回的 page_examples)
275
- - 写不出具体文件名 = 你没做 Step 2 = 违规
276
- - eval A2 会检查:HTML 含 `@prototype-source:` 或 `x-source-pages` → 通过;否则 A2 扣 5 分
277
-
278
- ### UTF-8 强制(治编码乱码)
279
-
280
- 生成的 HTML **必须用 UTF-8 写入**,否则中文 `<title>`/注释会变成 `鍑轰氦杞?` 乱码。
281
-
282
- - Write 工具调用时,内容里的中文必须是原始 UTF-8(不要从 GBK 源复制)
283
- - 如果用 Python 写文件:`open(path, 'w', encoding='utf-8')`
284
- - **绝不要从 bash 输出里复制中文再粘贴**(bash 在 Windows 上可能转 GBK)→ 优先让
285
- `fill_prototype` 脚本直接出文件,AI 只 Edit 占位符
286
-
287
- ---
263
+ ## 🔒 Step 3.5: 来源锚定 + UTF-8(硬门 #3,eval A2 检查)
264
+
265
+ ### 来源锚定(必做)
266
+
267
+ 生成的 HTML 必须在 `<head>` 里写一行注释,标注样式来源的真实文件:
268
+
269
+ ```html
270
+ <!-- @prototype-source: fywl-ui/apps/ics/src/views/xxx/index.vue, fywl-ui/apps/ics/src/api/xxx.ts -->
271
+ <meta name="x-source-pages" content="fywl-ui/.../index.vue">
272
+ ```
273
+
274
+ - 来源文件**必须来自 Step 2 读过的真实 Vue/TS 文件**(prefetch 返回的 page_examples)
275
+ - 写不出具体文件名 = 你没做 Step 2 = 违规
276
+ - eval A2 会检查:HTML 含 `@prototype-source:` 或 `x-source-pages` → 通过;否则 A2 扣 5 分
277
+
278
+ ### UTF-8 强制(治编码乱码)
279
+
280
+ 生成的 HTML **必须用 UTF-8 写入**,否则中文 `<title>`/注释会变成 `鍑轰氦杞?` 乱码。
281
+
282
+ - Write 工具调用时,内容里的中文必须是原始 UTF-8(不要从 GBK 源复制)
283
+ - 如果用 Python 写文件:`open(path, 'w', encoding='utf-8')`
284
+ - **绝不要从 bash 输出里复制中文再粘贴**(bash 在 Windows 上可能转 GBK)→ 优先让
285
+ `fill_prototype` 脚本直接出文件,AI 只 Edit 占位符
286
+
287
+ ---
288
288
  ## Step 4: Highlight Changes + Interactivity
289
289
 
290
290
  - Mark NEW/CHANGED elements with the template's `highlight-new` class
@@ -37,7 +37,19 @@ git status && git diff HEAD --stat
37
37
 
38
38
  ### Step 2: 质量门禁(提交前自检)
39
39
 
40
- 逐项检查,任何一项不过都暂停问用户:
40
+ **2a. 程序级强制门禁(syncgate)—— 先 git add 再跑**
41
+ ```bash
42
+ # 1. 精确 add (不用 git add .)
43
+ git add <相关文件>
44
+ # 2. 对暂存文件跑零信任门禁 (身份/作者/秘密扫描/REQ-ID)
45
+ python -c "import sys; sys.path.insert(0, r'$R/.qoder/scripts'); from domain.task.syncgate import run_gates; from foundation.core.paths import get_developer, get_repo_root; import subprocess; staged=subprocess.check_output(['git','diff','--cached','--name-only'],cwd=r'$R',text=True).split(); ok,reason=run_gates(staged, get_developer(), str(get_repo_root())); print('GATE_PASS' if ok else ('GATE_FAIL: '+reason)); sys.exit(0 if ok else 2)"
46
+ ```
47
+ - `GATE_PASS` → 继续 2b
48
+ - `GATE_FAIL: <原因>` → 停下,把原因告诉用户(如"检出疑似密钥 .env"),问修复还是强制。默认修复。
49
+ (这与 team_sync.py push 走的是同一道 syncgate,源码提交也不绕过零信任门禁。)
50
+
51
+ **2b. 人工自检(grep 类)**
52
+ 逐项检查:
41
53
  - [ ] 无 TODO / FIXME 残留(grep 一下)
42
54
  - [ ] 金额字段用了 BigDecimal(不出现 double/float 存钱)
43
55
  - [ ] 命名/风格跟周围代码一致
@@ -60,8 +72,8 @@ type 选 feat / fix / refactor / docs / test / chore。
60
72
  **用户没明确说"提交/确认"之前,不要真的 commit。**
61
73
 
62
74
  ### Step 5: 提交 + 推送
75
+ (文件已在 Step 2a git add 过,门禁已过)
63
76
  ```bash
64
- git add <相关文件> # 精确 add,不用 git add .
65
77
  git commit -m "<确认后的信息>"
66
78
  git pull --rebase # 先拉远端
67
79
  git push