@hupan56/wlkj 3.1.32 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +117 -0
- package/package.json +1 -1
- package/templates/qoder/agents/insight-planning.md +67 -67
- package/templates/qoder/agents/prd-reference.md +47 -47
- package/templates/qoder/commands/optional/wl-insight.md +4 -4
- package/templates/qoder/commands/optional/wl-report.md +1 -1
- package/templates/qoder/commands/optional/wl-spec.md +23 -3
- package/templates/qoder/commands/optional/wl-status.md +12 -1
- package/templates/qoder/commands/wl-code.md +138 -7
- package/templates/qoder/commands/wl-commit.md +12 -1
- package/templates/qoder/commands/wl-design.md +70 -6
- package/templates/qoder/commands/wl-init.md +27 -0
- package/templates/qoder/commands/wl-prd.md +230 -15
- package/templates/qoder/commands/wl-req.md +10 -3
- package/templates/qoder/commands/wl-search.md +74 -20
- package/templates/qoder/commands/wl-task.md +3 -3
- package/templates/qoder/commands/wl-test.md +17 -2
- package/templates/qoder/contracts/CHANGELOG.md +418 -0
- package/templates/qoder/contracts/README.md +180 -0
- package/templates/qoder/contracts/code.md +82 -0
- package/templates/qoder/contracts/commit.md +86 -0
- package/templates/qoder/contracts/contract-header.md +76 -0
- package/templates/qoder/contracts/design.md +106 -0
- package/templates/qoder/contracts/fallback.md +126 -0
- package/templates/qoder/contracts/isolation.md +119 -0
- package/templates/qoder/contracts/prd.md +118 -0
- package/templates/qoder/contracts/schemas/design-spec.schema.json +46 -0
- package/templates/qoder/contracts/schemas/prd.schema.json +36 -0
- package/templates/qoder/contracts/schemas/test-cases.schema.json +40 -0
- package/templates/qoder/contracts/spec.md +116 -0
- package/templates/qoder/contracts/task.md +125 -0
- package/templates/qoder/contracts/test.md +112 -0
- package/templates/qoder/hooks/post-tool-use.py +61 -0
- package/templates/qoder/hooks/session-start.py +34 -66
- package/templates/qoder/hooks/stop-eval.py +47 -0
- package/templates/qoder/rules/wl-pipeline.md +37 -0
- package/templates/qoder/scripts/capability/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/__pycache__/registry.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/__pycache__/registry_mcp.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/__init__.py +1 -1
- package/templates/qoder/scripts/capability/adapters/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/__pycache__/cli.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/__pycache__/mcp.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/__pycache__/qw.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/adapters/mcp.py +76 -100
- package/templates/qoder/scripts/capability/adapters/qw.py +295 -295
- package/templates/qoder/scripts/capability/caps/__init__.py +1 -1
- package/templates/qoder/scripts/capability/caps/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/context.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/cron.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/identity.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/memory.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/notify.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/present.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/repo.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/__pycache__/sandbox.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/capability/caps/memory.py +1 -1
- package/templates/qoder/scripts/capability/registry.py +21 -23
- package/templates/qoder/scripts/capability/registry_mcp.py +69 -5
- package/templates/qoder/scripts/capability/smoke_test_report.json +34 -20
- package/templates/qoder/scripts/deployment/setup/carriers.py +3 -1
- package/templates/qoder/scripts/deployment/setup/init_doctor.py +10 -3
- package/templates/qoder/scripts/deployment/setup/install_qoderwork.py +11 -0
- package/templates/qoder/scripts/deployment/setup/wlkj_shim.py +104 -0
- package/templates/qoder/scripts/domain/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/deployment/deploy_to_test.py +298 -0
- package/templates/qoder/scripts/domain/integration/__init__.py +0 -0
- package/templates/qoder/scripts/domain/integration/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/integration/__pycache__/return_to_platform.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/integration/return_to_platform.py +392 -0
- package/templates/qoder/scripts/domain/integration/spec_upload.py +209 -0
- package/templates/qoder/scripts/domain/kg/build/kg_build.py +268 -25
- package/templates/qoder/scripts/domain/kg/build/kg_incremental.py +108 -3
- package/templates/qoder/scripts/domain/kg/build/kg_signatures.py +169 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/__init__.py +10 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/asset_tree.py +57 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/discussion_importer.py +62 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/prd_importer.py +146 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/prototype_importer.py +64 -0
- package/templates/qoder/scripts/domain/kg/extract/asset/returns_importer.py +52 -0
- package/templates/qoder/scripts/domain/kg/extract/build_goal3.py +104 -0
- package/templates/qoder/scripts/domain/kg/extract/build_goal4.py +55 -0
- package/templates/qoder/scripts/domain/kg/extract/build_goal5.py +95 -0
- package/templates/qoder/scripts/domain/kg/extract/db/__init__.py +8 -0
- package/templates/qoder/scripts/domain/kg/extract/db/data_profile.py +22 -0
- package/templates/qoder/scripts/domain/kg/extract/db/fk_extractor.py +55 -0
- package/templates/qoder/scripts/domain/kg/extract/db/schema_extractor.py +90 -0
- package/templates/qoder/scripts/domain/kg/extract/extract.py +84 -0
- package/templates/qoder/scripts/domain/kg/extract/extract.py.bak +430 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/__init__.py +9 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/community_summarizer.py +206 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/embed_builder.py +132 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/naming_matcher.py +80 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/promote.py +59 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/recompute.py +93 -0
- package/templates/qoder/scripts/domain/kg/extract/inference/weak_link.py +421 -0
- package/templates/qoder/scripts/domain/kg/extract/java/__init__.py +15 -0
- package/templates/qoder/scripts/domain/kg/extract/java/_parser.py +271 -0
- package/templates/qoder/scripts/domain/kg/extract/java/all.py +145 -0
- package/templates/qoder/scripts/domain/kg/extract/java/build_java_to_pg.py +102 -0
- package/templates/qoder/scripts/domain/kg/extract/java/call_chain.py +49 -0
- package/templates/qoder/scripts/domain/kg/extract/java/class_extractor.py +141 -0
- package/templates/qoder/scripts/domain/kg/extract/java/domain_extractor.py +148 -0
- package/templates/qoder/scripts/domain/kg/extract/java/dubbo_extractor.py +33 -0
- package/templates/qoder/scripts/domain/kg/extract/java/endpoint_extractor.py +36 -0
- package/templates/qoder/scripts/domain/kg/extract/java/javadoc_extractor.py +110 -0
- package/templates/qoder/scripts/domain/kg/extract/java/llm_cn_filler.py +150 -0
- package/templates/qoder/scripts/domain/kg/extract/java/member_extractor.py +157 -0
- package/templates/qoder/scripts/domain/kg/extract/java/mybatisplus_extractor.py +34 -0
- package/templates/qoder/scripts/domain/kg/extract/java/pg_upsert.py +165 -0
- package/templates/qoder/scripts/domain/kg/extract/java/satoken_extractor.py +30 -0
- package/templates/qoder/scripts/domain/kg/extract/java/spring_extractor.py +39 -0
- package/templates/qoder/scripts/domain/kg/extract/java/validation_extractor.py +33 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/__init__.py +9 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/all.py +79 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/mapper_parser.py +99 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/relation_builder.py +69 -0
- package/templates/qoder/scripts/domain/kg/extract/mybatis/sql_extractor.py +78 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/__init__.py +8 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/prd_chunk_embed.py +105 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/prd_llm_extract.py +153 -0
- package/templates/qoder/scripts/domain/kg/extract/prd/req_anchor.py +120 -0
- package/templates/qoder/scripts/domain/kg/extract/ts_extract.py +111 -0
- package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +4 -2
- package/templates/qoder/scripts/domain/kg/kg.py +42 -5
- package/templates/qoder/scripts/domain/kg/search/_remote.py +187 -0
- package/templates/qoder/scripts/domain/kg/search/context_pack.py +32 -2
- package/templates/qoder/scripts/domain/kg/search/search_index.py +74 -20
- package/templates/qoder/scripts/domain/kg/storage/kg_duckdb.py +43 -0
- package/templates/qoder/scripts/domain/kg/switch_project.py +159 -0
- package/templates/qoder/scripts/domain/kg/sync_repowiki.py +109 -0
- package/templates/qoder/scripts/domain/requirement/req.py +134 -28
- package/templates/qoder/scripts/domain/task/__pycache__/wlkj_panel.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/domain/task/wlkj_panel.py +1348 -0
- package/templates/qoder/scripts/engine/poller.py +219 -0
- package/templates/qoder/scripts/foundation/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/foundation/core/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/foundation/core/__pycache__/paths.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/foundation/core/paths.py +102 -0
- package/templates/qoder/scripts/foundation/integrations/active_task.py +2 -1
- package/templates/qoder/scripts/orchestration/wlkj.py +4 -0
- package/templates/qoder/scripts/protocol/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +24 -11
- package/templates/qoder/scripts/protocol/transports/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/__pycache__/base.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/__pycache__/cli.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/__pycache__/http.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/__pycache__/stdio.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/protocol/transports/http.py +7 -1
- package/templates/qoder/scripts/validation/eval/qwork_harness.py +1 -1
- package/templates/qoder/scripts/validation/eval/report-commands.md +2 -2
- package/templates/qoder/settings.json +27 -9
- package/templates/qoder/skills/design-import/SKILL.md +3 -3
- package/templates/qoder/skills/design-review/SKILL.md +1 -1
- package/templates/qoder/skills/prd-generator/SKILL.md +4 -4
- package/templates/qoder/skills/prd-review/SKILL.md +1 -1
- package/templates/qoder/skills/prototype-generator/SKILL.md +3 -3
- package/templates/qoder/skills/spec-coder/SKILL.md +1 -1
- package/templates/qoder/skills/spec-generator/SKILL.md +80 -23
- package/templates/qoder/skills/test-generator/SKILL.md +1 -1
- package/templates/qoder/skills/wl-code/SKILL.md +13 -1
- package/templates/qoder/skills/wl-commit/SKILL.md +1 -1
- package/templates/qoder/skills/wl-design/SKILL.md +6 -6
- package/templates/qoder/skills/wl-init/SKILL.md +2 -2
- package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
- package/templates/qoder/skills/wl-prd/SKILL.md +60 -0
- package/templates/qoder/skills/wl-report/SKILL.md +2 -2
- package/templates/qoder/skills/wl-search/SKILL.md +1 -1
- package/templates/qoder/skills/wl-spec/SKILL.md +2 -2
- package/templates/qoder/skills/wl-status/SKILL.md +2 -2
- package/templates/qoder/skills/wl-task/SKILL.md +3 -3
- package/templates/qoder/skills/wl-test/SKILL.md +2 -2
- package/templates/qoder/templates/spec-template.md +124 -0
- package/templates/root/AGENTS.md +41 -14
- package/templates/qoder/scripts/domain/task/zentao_panel.py +0 -451
- package/templates/qoder/skills/wl-prd-full/SKILL.md +0 -121
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +0 -50
- package/templates/qoder/skills/wl-prd-review/SKILL.md +0 -47
|
@@ -1,451 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
"""
|
|
4
|
-
zentao_panel.py - 禅道任务面板数据接口 (供 VS Code 扩展 zentao-panel 调用)
|
|
5
|
-
|
|
6
|
-
目的: 给侧边栏面板一个稳定的 JSON 数据源, 一次返回"指派给我的【任务+需求+Bug】",
|
|
7
|
-
跨所有执行/产品全量聚合。逻辑与 my_workbench 工具一致 (只读), 但输出 JSON 而非文本。
|
|
8
|
-
|
|
9
|
-
为什么不直接调 MCP server:
|
|
10
|
-
MCP server 的数据收集依赖 import-time 全局变量 (CLIENT / MY_ACCOUNT), 由 mcp_launcher
|
|
11
|
-
注入环境变量才生效。本脚本走独立进程 (child_process.spawn 启动), 没有 launcher 注入,
|
|
12
|
-
所以照搬 zentao_sync.py 的稳健模式: 自己 ZentaoClient.from_config() + 自己解析归属。
|
|
13
|
-
收集循环则与 zentao_mcp_server.py:_collect_my_* 逐字一致 (DRY 同款), 保证数据口径统一。
|
|
14
|
-
|
|
15
|
-
用法:
|
|
16
|
-
python .qoder/scripts/domain/task/zentao_panel.py --json # 三域活动项 JSON
|
|
17
|
-
python .qoder/scripts/domain/task/zentao_panel.py --json --include-closed # 含已关闭历史项
|
|
18
|
-
python .qoder/scripts/domain/task/zentao_panel.py --json --kind tasks # 只看任务
|
|
19
|
-
|
|
20
|
-
退出码 (对齐项目规范):
|
|
21
|
-
0 成功 (含空结果); 3 配置/归属错误; 5 网络不通。
|
|
22
|
-
--json 模式下错误也输出 JSON ({"ok": false, "error": "..."}), 扩展端统一解析。
|
|
23
|
-
"""
|
|
24
|
-
import os
|
|
25
|
-
import sys
|
|
26
|
-
|
|
27
|
-
# ── 路径自举 (复用 bootstrap 模式, 与 zentao_sync.py 完全一致) ──
|
|
28
|
-
# 本脚本在 scripts/domain/task/ 下 (scripts 下 2 级), dirname 两次才到 scripts/。
|
|
29
|
-
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
30
|
-
_SCRIPTS = os.path.dirname(os.path.dirname(_HERE)) # .qoder/scripts (上两级)
|
|
31
|
-
_COMMON = os.path.join(_SCRIPTS, 'foundation')
|
|
32
|
-
for _p in (_SCRIPTS, _COMMON):
|
|
33
|
-
if _p not in sys.path:
|
|
34
|
-
sys.path.insert(0, _p)
|
|
35
|
-
try:
|
|
36
|
-
from bootstrap import setup; setup()
|
|
37
|
-
except Exception:
|
|
38
|
-
pass
|
|
39
|
-
|
|
40
|
-
import json
|
|
41
|
-
import argparse
|
|
42
|
-
from datetime import datetime
|
|
43
|
-
|
|
44
|
-
from foundation.core.paths import get_developer
|
|
45
|
-
from foundation.identity.identity import get_member
|
|
46
|
-
from foundation.integrations.zentao_client import ZentaoClient
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
# ============================================================
|
|
50
|
-
# 配置与归属解析 (照搬 zentao_sync.py, 唯一真相源)
|
|
51
|
-
# ============================================================
|
|
52
|
-
|
|
53
|
-
def _zentao_creds():
|
|
54
|
-
"""构建禅道客户端。凭据不全返回 None。
|
|
55
|
-
url 来自 .qoder/config.yaml (团队共享); user/password 来自 secrets/zentao.env (个人)。"""
|
|
56
|
-
return ZentaoClient.from_config()
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _resolve_my_account(developer):
|
|
60
|
-
"""解析当前 developer → 禅道 account。返回 (account, 错误原因)。"""
|
|
61
|
-
mem = get_member(developer)
|
|
62
|
-
if not mem:
|
|
63
|
-
return None, '当前开发者 %s 未注册 (无 member.json)。请先跑 /wl-init。' % developer
|
|
64
|
-
acc = (mem.get('zentao_account') or '').strip()
|
|
65
|
-
if not acc:
|
|
66
|
-
return None, ('%s 的 member.json 未配 zentao_account。\n'
|
|
67
|
-
' 编辑 workspace/members/%s/member.json 加 "zentao_account": "你的禅道账号"。'
|
|
68
|
-
% (developer, developer))
|
|
69
|
-
return acc, None
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
# ============================================================
|
|
73
|
-
# 收集循环 (与 zentao_mcp_server.py:_collect_my_* 逐字一致)
|
|
74
|
-
# 单独写成函数, 不依赖 self / 全局 CLIENT, 接收 client 做参数 → 可独立调用。
|
|
75
|
-
# ============================================================
|
|
76
|
-
|
|
77
|
-
def _paged_list(client, path, params, list_key, page_size=100, max_pages=20):
|
|
78
|
-
"""分页拉全量 (循环 page+limit, 按 id 去重)。照搬 mcp server 同名方法。
|
|
79
|
-
|
|
80
|
-
Returns: (合并去重列表, 是否截断[读满 max_pages 仍未到尾])。
|
|
81
|
-
"""
|
|
82
|
-
seen, all_items = set(), []
|
|
83
|
-
truncated = False
|
|
84
|
-
for _page in range(1, max_pages + 1):
|
|
85
|
-
p = dict(params or {})
|
|
86
|
-
p.update({'limit': page_size, 'page': _page})
|
|
87
|
-
r = client.api('GET', path, p)
|
|
88
|
-
if r.status_code not in (200, 201):
|
|
89
|
-
break
|
|
90
|
-
data = r.json()
|
|
91
|
-
items = data.get(list_key) if isinstance(data, dict) else None
|
|
92
|
-
if not isinstance(items, list) or not items:
|
|
93
|
-
break
|
|
94
|
-
new = 0
|
|
95
|
-
for it in items:
|
|
96
|
-
if not isinstance(it, dict):
|
|
97
|
-
continue
|
|
98
|
-
key = it.get('id', id(it))
|
|
99
|
-
if key in seen:
|
|
100
|
-
continue
|
|
101
|
-
seen.add(key)
|
|
102
|
-
all_items.append(it)
|
|
103
|
-
new += 1
|
|
104
|
-
if new == 0:
|
|
105
|
-
break
|
|
106
|
-
return all_items, truncated
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def _collect_my_tasks(client, my_account, status_filter=None):
|
|
110
|
-
"""扫所有执行, 收集指派给我的任务。返回 (任务列表, None) 或 (None, 错误)。"""
|
|
111
|
-
execs, _ = _paged_list(client, '/executions', None, 'executions')
|
|
112
|
-
if execs is None:
|
|
113
|
-
return None, '查询执行列表失败, 稍后重试。'
|
|
114
|
-
all_t = []
|
|
115
|
-
for e in execs:
|
|
116
|
-
eid = e.get('id')
|
|
117
|
-
if eid is None:
|
|
118
|
-
continue
|
|
119
|
-
ename = str(e.get('name', ''))[:20]
|
|
120
|
-
tasks, _ = _paged_list(client, '/executions/%s/tasks' % eid, None, 'tasks')
|
|
121
|
-
for t in tasks:
|
|
122
|
-
if not isinstance(t, dict):
|
|
123
|
-
continue
|
|
124
|
-
if ZentaoClient.assignee_account(t) != my_account:
|
|
125
|
-
continue
|
|
126
|
-
if status_filter and t.get('status') != status_filter:
|
|
127
|
-
continue
|
|
128
|
-
t2 = dict(t)
|
|
129
|
-
t2['execution'] = eid
|
|
130
|
-
t2['execution_name'] = ename
|
|
131
|
-
all_t.append(t2)
|
|
132
|
-
return all_t, None
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def _collect_my_bugs(client, my_account):
|
|
136
|
-
"""扫活跃产品, 收集我的 Bug (指派给我 OR 我提的)。"""
|
|
137
|
-
prods, _ = _paged_list(client, '/products', None, 'products')
|
|
138
|
-
mine = []
|
|
139
|
-
for p in prods:
|
|
140
|
-
pid = p.get('id')
|
|
141
|
-
bugs, _ = _paged_list(client, '/products/%s/bugs' % pid, None, 'bugs')
|
|
142
|
-
for b in bugs:
|
|
143
|
-
if not isinstance(b, dict):
|
|
144
|
-
continue
|
|
145
|
-
asg = ZentaoClient.assignee_account(b)
|
|
146
|
-
opn = ZentaoClient.openedby_account(b)
|
|
147
|
-
why = '指派给我' if asg == my_account else ('我提的' if opn == my_account else None)
|
|
148
|
-
if why:
|
|
149
|
-
b2 = dict(b)
|
|
150
|
-
b2['product_id'] = pid
|
|
151
|
-
b2['_why'] = why
|
|
152
|
-
mine.append(b2)
|
|
153
|
-
return mine, None
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def _collect_my_stories(client, my_account):
|
|
157
|
-
"""扫活跃产品, 收集我的需求 (指派给我 OR 我提的)。"""
|
|
158
|
-
prods, _ = _paged_list(client, '/products', None, 'products')
|
|
159
|
-
mine = []
|
|
160
|
-
for p in prods:
|
|
161
|
-
pid = p.get('id')
|
|
162
|
-
sts, _ = _paged_list(client, '/products/%s/stories' % pid, None, 'stories')
|
|
163
|
-
for s in sts:
|
|
164
|
-
if not isinstance(s, dict):
|
|
165
|
-
continue
|
|
166
|
-
asg = ZentaoClient.assignee_account(s)
|
|
167
|
-
opn = ZentaoClient.openedby_account(s)
|
|
168
|
-
why = '指派给我' if asg == my_account else ('我提的' if opn == my_account else None)
|
|
169
|
-
if why:
|
|
170
|
-
s2 = dict(s)
|
|
171
|
-
s2['product_id'] = pid
|
|
172
|
-
s2['_why'] = why
|
|
173
|
-
mine.append(s2)
|
|
174
|
-
return mine, None
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
# ============================================================
|
|
178
|
-
# 归一化: 把禅道原始 dict 裁成面板需要的精简字段 (减体积 + 隐藏内部字段)
|
|
179
|
-
# ============================================================
|
|
180
|
-
|
|
181
|
-
def _norm_task(t):
|
|
182
|
-
return {
|
|
183
|
-
'id': t.get('id'),
|
|
184
|
-
'name': str(t.get('name', '')),
|
|
185
|
-
'status': t.get('status', ''),
|
|
186
|
-
'pri': t.get('pri'),
|
|
187
|
-
'left': t.get('left'), # 剩余工时
|
|
188
|
-
'estimate': t.get('estimate'),
|
|
189
|
-
'consumed': t.get('consumed'),
|
|
190
|
-
'deadline': t.get('deadline'),
|
|
191
|
-
'execution': t.get('execution'),
|
|
192
|
-
'execution_name': t.get('execution_name', ''),
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def _norm_bug(b):
|
|
197
|
-
return {
|
|
198
|
-
'id': b.get('id'),
|
|
199
|
-
'title': str(b.get('title', '')),
|
|
200
|
-
'status': b.get('status', ''),
|
|
201
|
-
'severity': b.get('severity'),
|
|
202
|
-
'pri': b.get('pri'),
|
|
203
|
-
'why': b.get('_why', ''), # 指派给我 / 我提的
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
def _norm_story(s):
|
|
208
|
-
return {
|
|
209
|
-
'id': s.get('id'),
|
|
210
|
-
'title': str(s.get('title', '')),
|
|
211
|
-
'status': s.get('status', ''),
|
|
212
|
-
'pri': s.get('pri'),
|
|
213
|
-
'why': s.get('_why', ''),
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
# 活动态白名单 (与 my_workbench 一致 + 修正: changed 也是活动态)。
|
|
218
|
-
# 说明: 禅道 task 有 changed 状态 (需求变更/任务被改动), 旧版 LIVE_TASK 漏掉它
|
|
219
|
-
# → 用户实测"明明有个进行中的任务却看不到"。changed 不是终态, 应算活动项。
|
|
220
|
-
LIVE_TASK = {'wait', 'doing', 'pause', 'changed'}
|
|
221
|
-
LIVE_BUG = {'active'}
|
|
222
|
-
LIVE_STORY = {'active', 'reviewing', 'changing'}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def _filter_live(tasks, bugs, stories, include_closed):
|
|
226
|
-
"""过滤终态 (除非 include_closed)。"""
|
|
227
|
-
if include_closed:
|
|
228
|
-
return tasks, bugs, stories
|
|
229
|
-
return ([t for t in tasks if t.get('status') in LIVE_TASK],
|
|
230
|
-
[b for b in bugs if b.get('status') in LIVE_BUG],
|
|
231
|
-
[s for s in stories if s.get('status') in LIVE_STORY])
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
# ============================================================
|
|
235
|
-
# 主流程
|
|
236
|
-
# ============================================================
|
|
237
|
-
|
|
238
|
-
def fetch_workbench(include_closed=False, kind=None):
|
|
239
|
-
"""收集三域并归一化。返回 (result_dict, exit_code)。
|
|
240
|
-
|
|
241
|
-
Args:
|
|
242
|
-
include_closed: True=含已关闭历史项
|
|
243
|
-
kind: 'tasks'/'bugs'/'stories' 只取一域; None=全取
|
|
244
|
-
Returns:
|
|
245
|
-
({"ok": True, "data": {...}, "meta": {...}}, 0) 或
|
|
246
|
-
({"ok": False, "error": "..."}, 3 或 5)
|
|
247
|
-
"""
|
|
248
|
-
developer = get_developer()
|
|
249
|
-
|
|
250
|
-
# ① 凭据
|
|
251
|
-
client = _zentao_creds()
|
|
252
|
-
if client is None:
|
|
253
|
-
return {'ok': False,
|
|
254
|
-
'error': '禅道凭据未配置。请检查 .qoder/config.yaml 的 zentao.url, '
|
|
255
|
-
'及 workspace/members/<我>/.private/secrets/zentao.env 的账号密码。'}, 3
|
|
256
|
-
|
|
257
|
-
ok, msg = client.credentials_ok()
|
|
258
|
-
if not ok:
|
|
259
|
-
return {'ok': False, 'error': '禅道凭据不完整: %s' % msg}, 3
|
|
260
|
-
|
|
261
|
-
# ② 内网连通
|
|
262
|
-
ok, msg = client.check_intranet()
|
|
263
|
-
if not ok:
|
|
264
|
-
return {'ok': False, 'error': '连不上禅道内网: %s' % msg}, 5
|
|
265
|
-
|
|
266
|
-
# ③ 归属
|
|
267
|
-
my_account, err = _resolve_my_account(developer) if developer else (None, '未识别当前开发者身份。')
|
|
268
|
-
if not my_account:
|
|
269
|
-
return {'ok': False, 'error': err}, 3
|
|
270
|
-
|
|
271
|
-
# ④ 收集 (按 kind 决定取哪几域)
|
|
272
|
-
tasks, bugs, stories = [], [], []
|
|
273
|
-
if kind in (None, 'tasks'):
|
|
274
|
-
tasks, e = _collect_my_tasks(client, my_account)
|
|
275
|
-
if e:
|
|
276
|
-
return {'ok': False, 'error': e}, 5
|
|
277
|
-
if kind in (None, 'bugs'):
|
|
278
|
-
bugs, _ = _collect_my_bugs(client, my_account)
|
|
279
|
-
if kind in (None, 'stories'):
|
|
280
|
-
stories, _ = _collect_my_stories(client, my_account)
|
|
281
|
-
|
|
282
|
-
# ⑤ 过滤 + 归一
|
|
283
|
-
tasks, bugs, stories = _filter_live(tasks, bugs, stories, include_closed)
|
|
284
|
-
data = {
|
|
285
|
-
'account': my_account,
|
|
286
|
-
'developer': developer,
|
|
287
|
-
'tasks': [_norm_task(t) for t in tasks],
|
|
288
|
-
'bugs': [_norm_bug(b) for b in bugs],
|
|
289
|
-
'stories': [_norm_story(s) for s in stories],
|
|
290
|
-
}
|
|
291
|
-
meta = {
|
|
292
|
-
'fetched_at': datetime.now().isoformat(timespec='seconds'),
|
|
293
|
-
'counts': {
|
|
294
|
-
'tasks': len(data['tasks']),
|
|
295
|
-
'bugs': len(data['bugs']),
|
|
296
|
-
'stories': len(data['stories']),
|
|
297
|
-
},
|
|
298
|
-
'include_closed': include_closed,
|
|
299
|
-
}
|
|
300
|
-
return {'ok': True, 'data': data, 'meta': meta}, 0
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
# ============================================================
|
|
304
|
-
# 写操作 (v0.3): 面板按钮触发的禅道状态流转
|
|
305
|
-
# 设计对齐 MCP server 的 _assert_owner: 写前校验归属, 防误改队友。
|
|
306
|
-
# 任务: start/pause/finish/log_effort; Bug: confirm/resolve
|
|
307
|
-
# ============================================================
|
|
308
|
-
|
|
309
|
-
def _check_owner(client, my_account, kind, item_id):
|
|
310
|
-
"""写前归属校验 (对齐 mcp server _assert_owner)。
|
|
311
|
-
task 只认 assignedTo==我; bug 认 assignedTo==我 OR openedBy==我。
|
|
312
|
-
返回 None=放行, 字符串=拒绝原因。"""
|
|
313
|
-
r = client.api('GET', '/%ss/%s' % (kind, item_id))
|
|
314
|
-
if r.status_code not in (200, 201):
|
|
315
|
-
return None # 查询失败不阻塞 (禅道权限兜底)
|
|
316
|
-
data = r.json()
|
|
317
|
-
asg = ZentaoClient.assignee_account(data)
|
|
318
|
-
allow = (asg == my_account)
|
|
319
|
-
if kind == 'bug' and not allow:
|
|
320
|
-
allow = (ZentaoClient.openedby_account(data) == my_account)
|
|
321
|
-
if not allow:
|
|
322
|
-
return '这条%s指派给 %s, 不是你(%s), 已拒绝改。' % (kind, asg or '空', my_account)
|
|
323
|
-
return None
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
# 禅道写操作的 HTTP 调度表: action -> (method, path_tpl, payload_builder, ok_msg)
|
|
327
|
-
# payload_builder(args_dict) -> payload_dict
|
|
328
|
-
def _do_action(client, my_account, action, item_id, params):
|
|
329
|
-
"""执行单个写操作。返回 (result_dict, exit_code)。"""
|
|
330
|
-
developer = get_developer()
|
|
331
|
-
my_account = my_account or (_resolve_my_account(developer)[0] if developer else None)
|
|
332
|
-
if not my_account:
|
|
333
|
-
return {'ok': False, 'error': '未识别禅道身份, 无法校验归属。请配 member.json 的 zentao_account。'}, 3
|
|
334
|
-
|
|
335
|
-
# action 分派
|
|
336
|
-
ACTIONS = {
|
|
337
|
-
# 任务类 (kind=task)
|
|
338
|
-
'start_task': ('task', 'POST', '/tasks/%s/start', {'left': 'left'}),
|
|
339
|
-
'pause_task': ('task', 'POST', '/tasks/%s/pause', {}),
|
|
340
|
-
'finish_task': ('task', 'POST', '/tasks/%s/finish', {'left': 'left', 'consumed': 'consumed'}),
|
|
341
|
-
'log_effort': ('task', 'POST', '/tasks/%s/estimate', {'consumed': 'consumed', 'left': 'left'}),
|
|
342
|
-
# Bug 类 (kind=bug)
|
|
343
|
-
'confirm_bug': ('bug', 'POST', '/bugs/%s/confirm', {}),
|
|
344
|
-
'resolve_bug': ('bug', 'POST', '/bugs/%s/resolve', {'resolution': 'resolution', 'comment': 'comment'}),
|
|
345
|
-
}
|
|
346
|
-
if action not in ACTIONS:
|
|
347
|
-
return {'ok': False, 'error': '未知操作: %s。支持: %s' % (action, ', '.join(ACTIONS.keys()))}, 3
|
|
348
|
-
|
|
349
|
-
kind, method, path_tpl, fields = ACTIONS[action]
|
|
350
|
-
path = path_tpl % item_id
|
|
351
|
-
|
|
352
|
-
# ① 归属校验 (防误改队友)
|
|
353
|
-
guard = _check_owner(client, my_account, kind, item_id)
|
|
354
|
-
if guard:
|
|
355
|
-
return {'ok': False, 'error': guard}, 4
|
|
356
|
-
|
|
357
|
-
# ② 拼 payload (只取该操作关心的字段, 缺省给合理默认)
|
|
358
|
-
payload = {}
|
|
359
|
-
for pkey, fkey in fields.items():
|
|
360
|
-
v = params.get(fkey)
|
|
361
|
-
if v is not None and v != '':
|
|
362
|
-
try:
|
|
363
|
-
payload[pkey] = float(v) if fkey in ('left', 'consumed') else v
|
|
364
|
-
except (ValueError, TypeError):
|
|
365
|
-
payload[pkey] = v
|
|
366
|
-
# finish 强制 left=0 (完成=没剩余工时); log_effort 的 consumed 必填兜底
|
|
367
|
-
if action == 'finish_task' and 'left' not in payload:
|
|
368
|
-
payload['left'] = 0
|
|
369
|
-
if action == 'resolve_bug' and 'resolution' not in payload:
|
|
370
|
-
payload['resolution'] = 'fixed'
|
|
371
|
-
|
|
372
|
-
# ③ 调禅道
|
|
373
|
-
r = client.api(method, path, payload)
|
|
374
|
-
if r.status_code in (200, 201):
|
|
375
|
-
label = {
|
|
376
|
-
'start_task': '已开始', 'pause_task': '已暂停', 'finish_task': '已完成',
|
|
377
|
-
'log_effort': '已记工时', 'confirm_bug': '已确认', 'resolve_bug': '已解决',
|
|
378
|
-
}[action]
|
|
379
|
-
return {'ok': True, 'data': {'action': action, 'id': item_id, 'message': '%s #%s %s' % (kind, item_id, label)}}, 0
|
|
380
|
-
return {'ok': False, 'error': '操作失败 (HTTP %s): %s。可能权限不足或当前状态不允许。' % (
|
|
381
|
-
r.status_code, ZentaoClient.err_text(r))}, 5
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
def perform_action(action, item_id, params=None):
|
|
385
|
-
"""写操作入口。返回 (result_dict, exit_code)。"""
|
|
386
|
-
params = params or {}
|
|
387
|
-
client = _zentao_creds()
|
|
388
|
-
if client is None:
|
|
389
|
-
return {'ok': False, 'error': '禅道凭据未配置。'}, 3
|
|
390
|
-
ok, msg = client.credentials_ok()
|
|
391
|
-
if not ok:
|
|
392
|
-
return {'ok': False, 'error': '禅道凭据不完整: %s' % msg}, 3
|
|
393
|
-
ok, msg = client.check_intranet()
|
|
394
|
-
if not ok:
|
|
395
|
-
return {'ok': False, 'error': '连不上禅道内网: %s' % msg}, 5
|
|
396
|
-
developer = get_developer()
|
|
397
|
-
my_account, err = _resolve_my_account(developer) if developer else (None, '未识别开发者身份')
|
|
398
|
-
if err and not my_account:
|
|
399
|
-
return {'ok': False, 'error': err}, 3
|
|
400
|
-
return _do_action(client, my_account, action, item_id, params)
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
def main():
|
|
404
|
-
parser = argparse.ArgumentParser(description='禅道任务面板数据接口 (输出 JSON 供 VS Code 扩展消费)')
|
|
405
|
-
parser.add_argument('--json', action='store_true', help='JSON 输出 (面板默认用这个)')
|
|
406
|
-
parser.add_argument('--include-closed', action='store_true', help='包含已关闭/取消的历史项 (默认只看活动项)')
|
|
407
|
-
parser.add_argument('--kind', choices=['tasks', 'bugs', 'stories'],
|
|
408
|
-
help='只取某一域 (默认三域全取)')
|
|
409
|
-
parser.add_argument('--pretty', action='store_true', help='JSON 美化缩进 (默认紧凑, 面板解析更快)')
|
|
410
|
-
# 写操作子命令 (v0.3)
|
|
411
|
-
parser.add_argument('--action', choices=['start_task', 'pause_task', 'finish_task',
|
|
412
|
-
'log_effort', 'confirm_bug', 'resolve_bug'],
|
|
413
|
-
help='执行禅道写操作 (面板按钮触发)')
|
|
414
|
-
parser.add_argument('--id', type=int, help='写操作的目标 task/bug ID')
|
|
415
|
-
parser.add_argument('--left', help='剩余工时 (start/finish/log_effort)')
|
|
416
|
-
parser.add_argument('--consumed', help='消耗工时 (finish/log_effort)')
|
|
417
|
-
parser.add_argument('--resolution', help='Bug 解决方式 (resolve_bug): fixed/bydesign/duplicate/notrepro/postpone')
|
|
418
|
-
parser.add_argument('--comment', help='Bug 解决备注 (resolve_bug)')
|
|
419
|
-
args = parser.parse_args()
|
|
420
|
-
|
|
421
|
-
# 写操作分支
|
|
422
|
-
if args.action:
|
|
423
|
-
if not args.id:
|
|
424
|
-
result = {'ok': False, 'error': '写操作必须提供 --id'}
|
|
425
|
-
code = 3
|
|
426
|
-
else:
|
|
427
|
-
params = {'left': args.left, 'consumed': args.consumed,
|
|
428
|
-
'resolution': args.resolution, 'comment': args.comment}
|
|
429
|
-
result, code = perform_action(args.action, args.id, params)
|
|
430
|
-
else:
|
|
431
|
-
result, code = fetch_workbench(include_closed=args.include_closed, kind=args.kind)
|
|
432
|
-
|
|
433
|
-
if args.json or not sys.stdout.isatty():
|
|
434
|
-
indent = 2 if args.pretty else None
|
|
435
|
-
print(json.dumps(result, ensure_ascii=False, default=str, indent=indent))
|
|
436
|
-
else:
|
|
437
|
-
if result.get('ok'):
|
|
438
|
-
if args.action:
|
|
439
|
-
print('✅ %s' % result['data'].get('message', '操作成功'))
|
|
440
|
-
else:
|
|
441
|
-
d, m = result['data'], result['meta']
|
|
442
|
-
print('📋 %s 的禅道工作台 — 任务 %d · 需求 %d · Bug %d (更新于 %s)' % (
|
|
443
|
-
d['account'], m['counts']['tasks'], m['counts']['stories'], m['counts']['bugs'],
|
|
444
|
-
m['fetched_at']))
|
|
445
|
-
else:
|
|
446
|
-
print('❌ %s' % result.get('error', '未知错误'))
|
|
447
|
-
sys.exit(code)
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
if __name__ == '__main__':
|
|
451
|
-
main()
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: wl-prd-full
|
|
3
|
-
description: "完整档 PRD + 原型(13 章,正经需求)。新模块/新业务/新流程用这个。支持 参考:<insight报告> 衔接探索站。先问平台!"
|
|
4
|
-
trigger: "user invokes /wl-* command explicitly"
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
## 🔧 仓库根定位(QoderWork 桌面端 vs Qoder IDE/CLI)
|
|
9
|
-
|
|
10
|
-
**后续脚本里的 `$R` 代表仓库根**,先确定它(QoderWork 桌面端工作目录不是仓库根,相对路径会失效):
|
|
11
|
-
```bash
|
|
12
|
-
R=$(python ~/.qoderwork/repo_root.py 2>/dev/null || python3 ~/.qoderwork/repo_root.py 2>/dev/null) || R=.
|
|
13
|
-
PY=$(python --version >/dev/null 2>&1 && echo python || echo python3)
|
|
14
|
-
```
|
|
15
|
-
> `repo_root.py` 从 `~/.qoderwork/mcp.json` 反推仓库根;失败回退 `.`(IDE/CLI 工作目录即仓库根)。找不到时先跑 `$PY "$R/.qoder/scripts/orchestration/wlkj.py" install-qw`。
|
|
16
|
-
|
|
17
|
-
# wl-prd-full — 完整档 PRD + 原型(/wl-prd-full 命令入口)
|
|
18
|
-
|
|
19
|
-
> 这是 `/wl-prd-full` 命令的 skill 入口。QoderWork 的 `/` 列表读 skills,
|
|
20
|
-
> 所以本 skill 让 QoderWork 也能用 `/wl-prd-full`,与 Qoder IDE 保持一致。
|
|
21
|
-
> 完整工作流见同名 command 文件 `.qoder/commands/wl-prd.md`,
|
|
22
|
-
> 功能实现层见 `.qoder/skills/prd-generator/SKILL.md`。
|
|
23
|
-
> **第一性原理:又快又准。**
|
|
24
|
-
|
|
25
|
-
## ⚠️ STEP 0: 必须先问平台(任何分析前)
|
|
26
|
-
|
|
27
|
-
```
|
|
28
|
-
这个需求是针对哪个平台?
|
|
29
|
-
1. Web 管理端 (fywl-ui) - Ant Design Vue + VxeGrid 风格
|
|
30
|
-
2. APP 移动端 (Carmg-H5) - Vant 风格
|
|
31
|
-
3. 两端都要
|
|
32
|
-
请选择 (1/2/3):
|
|
33
|
-
```
|
|
34
|
-
**绝不自动判断。绝不假设。绝不跳过。永远先问。等用户回答。**
|
|
35
|
-
|
|
36
|
-
| 回答 | 平台 | 搜索 flag | 原型模板 |
|
|
37
|
-
|------|------|-----------|----------|
|
|
38
|
-
| 1 / Web / PC / 管理端 | Web | `--platform web` | prototype-web.html |
|
|
39
|
-
| 2 / APP / H5 / 移动端 | APP | `--platform app` | prototype-app.html |
|
|
40
|
-
| 3 / 都要 / 两端 | Both | 都跑 | 两个模板,两份原型 |
|
|
41
|
-
|
|
42
|
-
## ⚠️ STEP 0.5: 原型要不要画?(融入 Step 2 批量确认,不另开轮次)
|
|
43
|
-
|
|
44
|
-
**不交给 AI 默默判断**(避免"AI 觉得不需要、用户其实想要"的扯皮),但也**不额外开一轮问答**——
|
|
45
|
-
合并进 Step 2「轻反思 + 批量确认」那一步,跟背景/目标/指标一起一次性问完。
|
|
46
|
-
|
|
47
|
-
### 关键词触发矩阵(给默认建议,用户可一键改)
|
|
48
|
-
|
|
49
|
-
| 模式 | 关键词信号 | 默认建议 | 例子 |
|
|
50
|
-
|------|-----------|---------|------|
|
|
51
|
-
| 评审 | (任何) | ❌ 不画 | 评审别人的 PRD,本就不画 |
|
|
52
|
-
| 快速 | 加字段/按钮/文案/导出/接口 | ❌ 不画 | "列表加个导出按钮" |
|
|
53
|
-
| 快速 | 含"页面/弹窗/表单/交互" | ✅ 画 | "弹窗里加 3 个字段" |
|
|
54
|
-
| 完整 | 含"页面/菜单/详情/列表/向导/看板/大屏" | ✅ 画 | "新出库单页面" |
|
|
55
|
-
| 完整 | 纯"规则/接口/定时/对账/计算/开关/参数/性能" | ❌ 不画 | "出交车点检固定为总部全局规则" |
|
|
56
|
-
| 完整 | 混合(既有页面又有规则) | ✅ 画(只画页面那部分) | "新增审批流 + 审批页" |
|
|
57
|
-
|
|
58
|
-
### Step 2 问法(合并到批量确认)
|
|
59
|
-
|
|
60
|
-
```
|
|
61
|
-
确认 3 件事就开工:
|
|
62
|
-
1. 背景/目标:……(AI 已补,要改吗?)
|
|
63
|
-
2. 关键指标:……(AI 已补,要改吗?)
|
|
64
|
-
3. 原型图:我建议【画/不画】,因为……(理由)。
|
|
65
|
-
要改吗?(画 / 不画 / 两端各画一份)
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
### 两端都画时的边界(v3.1)
|
|
69
|
-
|
|
70
|
-
「完整模式 + 两端都要」如果用户选了"画",**默认只画主端一份**(用户在 PRD 里标的主端),
|
|
71
|
-
另一端在 PRD 第 11 章写"复用主端规则,UI 适配另出"。用户明确说"两端各画一份"才画 2 份。
|
|
72
|
-
|
|
73
|
-
### 不画原型时的下游连锁(必须对齐)
|
|
74
|
-
|
|
75
|
-
| 产物 | 处理 |
|
|
76
|
-
|------|------|
|
|
77
|
-
| PRD 第 11 章「原型地址」 | 写 `无({原因},经用户确认豁免)`,**保留章节标题**不省略 |
|
|
78
|
-
| PRD 第 1 行 `@contract` 头 | 标记 `prototype: none` |
|
|
79
|
-
| eval_prd | 检测到 `prototype: none` → A2 自动跳过,满分从 100 降到 70,照样能 PASS |
|
|
80
|
-
| /wl-design | 读到 `prototype: none` 跳过原型环节 |
|
|
81
|
-
| /wl-code | 直接以 PRD 字段表为准 |
|
|
82
|
-
|
|
83
|
-
---
|
|
84
|
-
|
|
85
|
-
## 衔接 insight(参考模式)
|
|
86
|
-
|
|
87
|
-
**若用户说"按 insight 报告做 PRD""转 PRD"或带 `参考:<报告路径>`:**
|
|
88
|
-
Read 指定 insight 报告 → 把现状锚定 + 选中的机会/Gap 作为 PRD 现状背景输入 → 走完整档流程。
|
|
89
|
-
> 衔接语义:`/wl-prd-full 参考:{报告路径}#{机会编号}`
|
|
90
|
-
|
|
91
|
-
## 完整流程(详细见 command 文件)
|
|
92
|
-
|
|
93
|
-
1. **并行取全上下文**:先读当前角色(`$PY "$R/.qoder/scripts/orchestration/wlkj.py" role` 输出 pm/design/dev/test/admin 之一),用 prefetch 一次取全(完整档需求含多业务实体,prefetch 多词预测比单词 context 更准):
|
|
94
|
-
```bash
|
|
95
|
-
ROLE=$($PY "$R/.qoder/scripts/orchestration/wlkj.py" role 2>/dev/null || echo pm)
|
|
96
|
-
$PY "$R/.qoder/scripts/orchestration/wlkj.py" kg prefetch "<需求描述原句>" --platform <p>
|
|
97
|
-
```
|
|
98
|
-
> prefetch 把需求句拆成多个业务词批量搜、一次合并(例"考勤导入校验"→考勤/导入两词全搜)。
|
|
99
|
-
> 拿到后**按 $ROLE 视角看重点**:dev 看代码段、design 看页面+风格段、pm 看字段+历史PRD段。prefetch 之后不要再串行 search。
|
|
100
|
-
> 不硬编码 pm:dev 写 PRD 能拿到代码视角、设计师拿到页面+风格视角,比统一 PM 视角更"准"。
|
|
101
|
-
2. **轻反思 + 批量确认**:AI 自动补全背景/目标/指标,只把真缺失的 1-2 点一次性问完
|
|
102
|
-
3. **一次生成 + 3 道质量锁**:
|
|
103
|
-
- PRD(prd-full-template.md,13 章)一次 Write
|
|
104
|
-
- **原型按 Step 0.5 决策**:用户选"画"→ 走 prototype-generator(必须先 fill_prototype);
|
|
105
|
-
用户选"不画"→ 跳过,PRD 第 11 章写 `无` + contract 头标 `prototype: none`
|
|
106
|
-
- 3 道质量锁:背景锁/字段锁/闭环锁 → eval_prd(**不要加 `2>/dev/null`,stderr 阻断原因要看**)→ 不过自动修
|
|
107
|
-
- eval 命令:`$PY "$R/.qoder/scripts/orchestration/wlkj.py" eval <prd.md> [原型.html]`
|
|
108
|
-
(wlkj.py eval 内部映射 validation/metrics/eval_prd.py,路径不会漂移)
|
|
109
|
-
4. **发布 + 归档 + 同步**(3 合 1):collect_prds → team_sync push → archive_prd
|
|
110
|
-
|
|
111
|
-
## 🔒 3 道质量锁
|
|
112
|
-
|
|
113
|
-
- **锁① 背景锁**:背景回答了"为什么做+目标指标"
|
|
114
|
-
- **锁② 字段锁**:字段来自真实代码(复用 eval_prd A1),无发明
|
|
115
|
-
- **锁③ 闭环锁**:每个功能点有验收标准(Given-When-Then)
|
|
116
|
-
|
|
117
|
-
## REQ-ID 分配(并发安全)
|
|
118
|
-
|
|
119
|
-
```bash
|
|
120
|
-
python -c "import sys; sys.path.insert(0,'.qoder/scripts'); from foundation.io.reqid import allocate_req_id; n=allocate_req_id(); print('REQ-%d-%03d' % (__import__('datetime').date.today().year, n))"
|
|
121
|
-
```
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: wl-prd-quick
|
|
3
|
-
description: "小改动极速出 Mini-PRD(6 章)。加字段/加按钮/改文案/加导出等零星需求专用。先问平台!"
|
|
4
|
-
trigger: "user invokes /wl-* command explicitly"
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
## 🔧 仓库根定位(QoderWork 桌面端 vs Qoder IDE/CLI)
|
|
9
|
-
|
|
10
|
-
**后续脚本里的 `$R` 代表仓库根**,先确定它(QoderWork 桌面端工作目录不是仓库根,相对路径会失效):
|
|
11
|
-
```bash
|
|
12
|
-
R=$(python ~/.qoderwork/repo_root.py 2>/dev/null || python3 ~/.qoderwork/repo_root.py 2>/dev/null) || R=.
|
|
13
|
-
PY=$(python --version >/dev/null 2>&1 && echo python || echo python3)
|
|
14
|
-
```
|
|
15
|
-
> `repo_root.py` 从 `~/.qoderwork/mcp.json` 反推仓库根;失败回退 `.`(IDE/CLI 工作目录即仓库根)。找不到时先跑 `$PY "$R/.qoder/scripts/orchestration/wlkj.py" install-qw`。
|
|
16
|
-
|
|
17
|
-
# wl-prd-quick — 小改动极速出 Mini-PRD(/wl-prd-quick 命令入口)
|
|
18
|
-
|
|
19
|
-
> 这是 `/wl-prd-quick` 命令的 skill 入口。QoderWork 的 `/` 列表读 skills,
|
|
20
|
-
> 所以本 skill 让 QoderWork 也能用 `/wl-prd-quick`,与 Qoder IDE 保持一致。
|
|
21
|
-
> 完整工作流见同名 command 文件 `.qoder/commands/wl-prd.md`,
|
|
22
|
-
> 功能实现层见 `.qoder/skills/prd-generator/SKILL.md`(快档分支)。
|
|
23
|
-
|
|
24
|
-
## ⚠️ STEP 0: 必须先问平台(任何分析前)
|
|
25
|
-
|
|
26
|
-
```
|
|
27
|
-
这个需求是针对哪个平台?
|
|
28
|
-
1. Web 管理端 (fywl-ui) - Ant Design Vue + VxeGrid 风格
|
|
29
|
-
2. APP 移动端 (Carmg-H5) - Vant 风格
|
|
30
|
-
3. 两端都要
|
|
31
|
-
请选择 (1/2/3):
|
|
32
|
-
```
|
|
33
|
-
**绝不自动判断。问完就停。**
|
|
34
|
-
|
|
35
|
-
## 适用范围(只管小改动)
|
|
36
|
-
|
|
37
|
-
✅ 加/删/改字段、按钮、文案、导出、校验规则
|
|
38
|
-
❌ 新模块/新业务/新流程/多页面联动 → 走 `/wl-prd-full`
|
|
39
|
-
|
|
40
|
-
## 3 步极速流程(详细见 command 文件)
|
|
41
|
-
|
|
42
|
-
1. **先 prefetch(预测 + 批量预取,禁止串行 search)**:
|
|
43
|
-
```bash
|
|
44
|
-
$PY "$R/.qoder/scripts/orchestration/wlkj.py" prefetch "<需求描述>" --platform <p>
|
|
45
|
-
```
|
|
46
|
-
prefetch 一次返回代码/字段/API/Wiki/历史PRD 的合并结果。**之后不要再用 search/find/grep 逐个查词**——那会变成 6+ 次串行往返,是轮次爆炸的根因。prefetch 不够时才补调一次。
|
|
47
|
-
2. 一次 Write:Mini-PRD(prd-quick-template.md,6 章)+ 微型原型(只画 diff)
|
|
48
|
-
3. "出好了,确认发布吗?" → 归档 + push
|
|
49
|
-
|
|
50
|
-
> mode 标记 `<!-- mode: quick -->`,跳过 EVA 完整检查。只跑字段锁。
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: wl-prd-review
|
|
3
|
-
description: "评审 PRD 完整性与质量(7 项 checklist + EVA 评分)。"
|
|
4
|
-
trigger: "user invokes /wl-* command explicitly"
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
## 🔧 仓库根定位(QoderWork 桌面端 vs Qoder IDE/CLI)
|
|
9
|
-
|
|
10
|
-
**后续脚本里的 `$R` 代表仓库根**,先确定它(QoderWork 桌面端工作目录不是仓库根,相对路径会失效):
|
|
11
|
-
```bash
|
|
12
|
-
R=$(python ~/.qoderwork/repo_root.py 2>/dev/null || python3 ~/.qoderwork/repo_root.py 2>/dev/null) || R=.
|
|
13
|
-
PY=$(python --version >/dev/null 2>&1 && echo python || echo python3)
|
|
14
|
-
```
|
|
15
|
-
> `repo_root.py` 从 `~/.qoderwork/mcp.json` 反推仓库根;失败回退 `.`(IDE/CLI 工作目录即仓库根)。找不到时先跑 `$PY "$R/.qoder/scripts/orchestration/wlkj.py" install-qw`。
|
|
16
|
-
|
|
17
|
-
# wl-prd-review — 评审 PRD 质量(/wl-prd-review 命令入口)
|
|
18
|
-
|
|
19
|
-
> 这是 `/wl-prd-review` 命令的 skill 入口。
|
|
20
|
-
> 完整工作流见同名 command 文件 `.qoder/commands/wl-prd.md`,
|
|
21
|
-
> 评审实现细节见 `.qoder/skills/prd-review/SKILL.md`。
|
|
22
|
-
> **定位:评审已有 PRD,不产出新 PRD。**
|
|
23
|
-
|
|
24
|
-
## 评审做什么
|
|
25
|
-
|
|
26
|
-
拿到一份 PRD,检查它够不够格发布/交付下游:
|
|
27
|
-
1. **7 项 checklist**:用户故事清晰 / 验收可测 / 优先级已标 / 模块已识别 / 非功能已列 / 边界已考虑 / 依赖已识别
|
|
28
|
-
2. **EVA 客观评分**:`$PY "$R/.qoder/scripts/orchestration/wlkj.py" eval <PRD> [原型]`,≥80% 才 PASS
|
|
29
|
-
3. **合并报告**:PASS → 可发布;不 PASS → 修复清单
|
|
30
|
-
|
|
31
|
-
## 定位要评审的 PRD
|
|
32
|
-
|
|
33
|
-
用户给路径 → 直接 Read;没给 → 扫 `data/docs/prd/` + `workspace/members/*/drafts/` 列出最近 PRD 让用户选。
|
|
34
|
-
|
|
35
|
-
详细流程见 `.qoder/commands/wl-prd.md`。
|
|
36
|
-
|
|
37
|
-
## ✅ 评审完成后必须埋点(不要跳过)
|
|
38
|
-
|
|
39
|
-
评审报告给出 PASS/不 PASS 结论后,**立即**记录一条 `review_done` 事件——
|
|
40
|
-
这是「可用性指标 F2 转化漏斗」评审级数据的唯一来源,不埋则该级永远为 0:
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
$PY "$R/.qoder/scripts/orchestration/wlkj.py" learn record review_done "{\"prd\": \"<被评审PRD文件名>\", \"verdict\": \"PASS\"}"
|
|
44
|
-
```
|
|
45
|
-
- `verdict`:评审结论,`PASS` 或 `FAIL`
|
|
46
|
-
- 埋点失败不阻塞评审(包在 try 里无所谓),但能埋一定要埋。
|
|
47
|
-
|