For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Let form controls dept and ownerDept participate in workflow condition branches with person-parity contains matching (exact department id intersection) and a tree multi-select value picker.
Architecture: Mirror the existing person/post path: whitelist types in front/back CONDITION_ELIGIBLE_TYPES; add extract_dept_ids / dept_contains_match in oa_dept_select.py; OR them into oa_engine._eval_condition_rules contains branch; extend ConditionRuleEditor with el-tree-select fed by fetchDeptTree. Matching is exact id only — tree UI is for picking, not hierarchical match.
Tech Stack: FastAPI · pytest · Vue 3 · Element Plus · existing ConditionRuleEditor / oa-condition-fields / oa_engine contains path
Spec: docs/superpowers/specs/2026-08-06-oa-dept-condition-design.md
| Path | Responsibility |
|---|---|
Modify: backend/app/services/oa_dept_select.py |
extract_dept_ids + dept_contains_match |
Modify: backend/tests/test_oa_dept_select.py |
Unit tests for extract/contains |
Modify: backend/app/services/oa_engine.py |
OR dept_contains_match into contains eval |
Modify: backend/tests/test_oa_engine.py |
Engine condition tests for dept |
Modify: backend/app/services/oa_publish.py |
Add dept / ownerDept to CONDITION_ELIGIBLE_TYPES |
Modify: backend/tests/test_oa_publish.py |
Publish allows flagged dept field in rules |
Modify: web-admin/src/components/oa-approval/oa-condition-fields.ts |
Frontend whitelist + ops |
Modify: web-admin/src/components/oa-approval/flow-tree/ConditionRuleEditor.vue |
Dept-like RHS tree multi-select |
Optional: docs/superpowers/specs/2026-07-30-oa-owner-fields-design.md |
Strike “ownerDept 暂不进条件” note |
Modify: docs/superpowers/specs/2026-08-06-oa-dept-condition-design.md |
Mark status implemented when done |
dept_contains_match helpers (TDD)Files:
backend/app/services/oa_dept_select.pyModify: backend/tests/test_oa_dept_select.py
[ ] Step 1: Write failing tests — append to TestOaDeptSelect in backend/tests/test_oa_dept_select.py:
from app.services.oa_dept_select import (
extract_dept_ids,
dept_contains_match,
validate_dept_form_values,
)
def test_extract_and_contains(self):
self.assertEqual(extract_dept_ids({"id": "d1", "name": "研发"}), {"d1"})
self.assertEqual(
extract_dept_ids([{"id": "d1", "name": "A"}, {"id": "d2", "name": "B"}]),
{"d1", "d2"},
)
self.assertTrue(
dept_contains_match(
[{"id": "d1", "name": "A"}],
["d1", "d9"],
)
)
self.assertFalse(
dept_contains_match(
[{"id": "d3", "name": "C"}],
["d1", "d2"],
)
)
# 精确匹配:下级 id 不等于上级 id
self.assertFalse(
dept_contains_match(
{"id": "d1-child", "name": "前端组"},
["d1"],
)
)
Update the import at the top of the test file to include extract_dept_ids and dept_contains_match (keep existing validate_dept_form_values import).
Run:
cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && pytest tests/test_oa_dept_select.py::TestOaDeptSelect::test_extract_and_contains -v
Expected: FAIL (import error or AttributeError: extract_dept_ids / dept_contains_match missing).
backend/app/services/oa_dept_select.py (after validate_dept_form_values, mirror oa_person_select.py):def extract_dept_ids(raw: Any) -> Set[str]:
ids: Set[str] = set()
items = raw if isinstance(raw, list) else ([raw] if isinstance(raw, dict) else [])
for item in items:
if not isinstance(item, dict):
continue
did = str(item.get("id") or "").strip()
if did:
ids.add(did)
return ids
def dept_contains_match(form_value: Any, cond_ids: Any) -> bool:
form_ids = extract_dept_ids(form_value)
if isinstance(cond_ids, str):
want = {cond_ids.strip()} if cond_ids.strip() else set()
elif isinstance(cond_ids, list):
want = {str(x).strip() for x in cond_ids if str(x).strip()}
else:
want = set()
return bool(form_ids & want)
Add to the module imports at top:
from typing import Any, Dict, List, Optional, Set
(Set is new; keep existing imports.)
cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && pytest tests/test_oa_dept_select.py -v
Expected: PASS (all tests in file).
git add backend/app/services/oa_dept_select.py backend/tests/test_oa_dept_select.py
git commit -m "$(cat <<'EOF'
feat(oa): add dept_contains_match for condition id intersection
EOF
)"
Files:
backend/app/services/oa_engine.py (_eval_condition_rules)Modify: backend/tests/test_oa_engine.py
[ ] Step 1: Write failing engine tests — append after test_condition_contains_person_single_object in TestOaEngineHelpers:
def test_condition_contains_dept(self):
groups = [
{
"name": "hit",
"rules": [
{"field": "Fdept", "op": "contains", "value": ["d1", "d2"]}
],
"child": {"id": "n1", "type": "end"},
},
{"name": "default", "rules": [], "child": {"id": "n2", "type": "end"}},
]
g = _match_condition_group(
groups,
{"Fdept": [{"id": "d2", "name": "研发"}]},
)
self.assertEqual(g["name"], "hit")
g2 = _match_condition_group(
groups,
{"Fdept": [{"id": "d9", "name": "其他"}]},
)
self.assertEqual(g2["name"], "default")
def test_condition_contains_owner_dept_single_object(self):
groups = [
{
"name": "hit",
"rules": [{"field": "Fod", "op": "contains", "value": ["d1"]}],
"child": {"id": "n1", "type": "end"},
},
{"name": "default", "isDefault": True, "child": {"id": "n2", "type": "end"}},
]
g = _match_condition_group(
groups, {"Fod": {"id": "d1", "name": "研发"}}
)
self.assertEqual(g["name"], "hit")
cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && pytest tests/test_oa_engine.py::TestOaEngineHelpers::test_condition_contains_dept tests/test_oa_engine.py::TestOaEngineHelpers::test_condition_contains_owner_dept_single_object -v
Expected: FAIL — dept form values do not match (person/post helpers return False → group falls through to default).
backend/app/services/oa_engine.py function _eval_condition_rules, change the contains branch imports and OR:Find:
from app.services.oa_person_select import person_contains_match
from app.services.oa_post_select import post_contains_match
Replace with:
from app.services.oa_person_select import person_contains_match
from app.services.oa_post_select import post_contains_match
from app.services.oa_dept_select import dept_contains_match
Find:
form_val = form_data.get(field)
if not (person_contains_match(form_val, val) or post_contains_match(form_val, val)):
return False
Replace with:
form_val = form_data.get(field)
if not (
person_contains_match(form_val, val)
or post_contains_match(form_val, val)
or dept_contains_match(form_val, val)
):
return False
Also update the docstring of _eval_condition_rules from 「人员/职位」to 「人员/职位/部门」.
cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && pytest tests/test_oa_engine.py::TestOaEngineHelpers::test_condition_contains_person tests/test_oa_engine.py::TestOaEngineHelpers::test_condition_contains_person_single_object tests/test_oa_engine.py::TestOaEngineHelpers::test_condition_contains_dept tests/test_oa_engine.py::TestOaEngineHelpers::test_condition_contains_owner_dept_single_object tests/test_oa_engine.py::TestOaEngineHelpers::test_condition_contains_initiator -v
Expected: PASS (dept new + person/initiator regression).
git add backend/app/services/oa_engine.py backend/tests/test_oa_engine.py
git commit -m "$(cat <<'EOF'
feat(oa): evaluate dept contains in condition rules
EOF
)"
Files:
backend/app/services/oa_publish.pyModify: backend/tests/test_oa_publish.py
[ ] Step 1: Write failing publish test — append near other condition field tests in TestConditionPublish (backend/tests/test_oa_publish.py):
def test_dept_condition_field_allowed(self):
errs = validate_condition_rules_against_form(
{
"rule": [
{
"type": "dept",
"field": "Fdept",
"title": "部门",
"useInCondition": True,
"$required": True,
"props": {"multiple": True},
}
]
},
_condition_flow(
rules=[
{"field": "Fdept", "op": "contains", "value": ["d1", "d2"]}
]
),
)
self.assertEqual(errs, [])
def test_owner_dept_condition_field_allowed(self):
errs = validate_condition_rules_against_form(
{
"rule": [
{
"type": "ownerDept",
"field": "Fod",
"title": "归属部门",
"useInCondition": True,
"$required": True,
}
]
},
_condition_flow(
rules=[{"field": "Fod", "op": "contains", "value": ["d1"]}]
),
)
self.assertEqual(errs, [])
Ensure validate_condition_rules_against_form and _condition_flow are already imported/defined in that file (they are).
cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && pytest tests/test_oa_publish.py::TestConditionPublish::test_dept_condition_field_allowed tests/test_oa_publish.py::TestConditionPublish::test_owner_dept_condition_field_allowed -v
Expected: FAIL — 条件引用了无效字段:Fdept (or Fod) because type not in CONDITION_ELIGIBLE_TYPES.
backend/app/services/oa_publish.py, extend:CONDITION_ELIGIBLE_TYPES = frozenset(
{
"input",
"inputNumber",
"number",
"money",
"radio",
"select",
"datePicker",
"date",
"person",
"ownerPerson",
"post",
"dept",
"ownerDept",
}
)
cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && pytest tests/test_oa_publish.py -k "dept_condition or initiator_condition or field_not_flagged" -v
Expected: PASS.
git add backend/app/services/oa_publish.py backend/tests/test_oa_publish.py
git commit -m "$(cat <<'EOF'
feat(oa): allow dept and ownerDept in condition publish whitelist
EOF
)"
Files:
Modify: web-admin/src/components/oa-approval/oa-condition-fields.ts
[ ] Step 1: Extend CONDITION_ELIGIBLE_TYPES
In web-admin/src/components/oa-approval/oa-condition-fields.ts, change the Set to include dept types:
export const CONDITION_ELIGIBLE_TYPES = new Set([
'input',
'inputNumber',
'number',
'money',
'person',
'ownerPerson',
'dept',
'ownerDept',
'post',
'radio',
'select',
'datePicker',
'date'
])
opsForFieldTypeFind:
if (type === 'person' || type === 'ownerPerson' || type === 'post' || type === 'initiator') {
return [{ value: 'contains', label: '包含' }]
}
Replace with:
if (
type === 'person' ||
type === 'ownerPerson' ||
type === 'dept' ||
type === 'ownerDept' ||
type === 'post' ||
type === 'initiator'
) {
return [{ value: 'contains', label: '包含' }]
}
(oa-fc-host/App.vue already gates the switch on CONDITION_ELIGIBLE_TYPES — no host change required.)
cd /Users/wuzilong/work/00code/zkxw/web-admin && rg "CONDITION_ELIGIBLE_TYPES|opsForFieldType" src/components/oa-approval/oa-condition-fields.ts
Expected: both dept and ownerDept appear in the Set and ops branch.
git add web-admin/src/components/oa-approval/oa-condition-fields.ts
git commit -m "$(cat <<'EOF'
feat(oa): whitelist dept and ownerDept for condition participation
EOF
)"
Files:
Modify: web-admin/src/components/oa-approval/flow-tree/ConditionRuleEditor.vue
[ ] Step 1: Import fetchDeptTree and add dept tree state
In script imports, change:
import { fetchPostList, fetchUserList } from '@/api/admin'
to:
import { fetchDeptTree, fetchPostList, fetchUserList } from '@/api/admin'
After postOptions ref, add:
type DeptTreeNode = { value: string; label: string; children?: DeptTreeNode[] }
const deptTreeData = ref<DeptTreeNode[]>([])
isPostField: function isDeptLikeField(field: string) {
const t = meta(field)?.type
return t === 'dept' || t === 'ownerDept'
}
function mapDeptTree(nodes: Api.Dept.DeptItem[]): DeptTreeNode[] {
return (nodes || []).map((n) => ({
value: String(n.id),
label: String(n.deptName || n.id),
children: n.children?.length ? mapDeptTree(n.children) : undefined
}))
}
Update defaultValueForType:
function defaultValueForType(type: string, opts?: ConditionCandidate['options']) {
if (
type === 'person' ||
type === 'ownerPerson' ||
type === 'dept' ||
type === 'ownerDept' ||
type === 'post' ||
type === 'initiator'
) {
return [] as string[]
}
return opts?.[0]?.value ?? ''
}
loadDepts + mount/watch async function loadDepts() {
try {
const tree = (await fetchDeptTree()) || []
deptTreeData.value = mapDeptTree(tree as Api.Dept.DeptItem[])
} catch {
deptTreeData.value = []
}
}
In onMounted, after the post block:
if (props.candidates.some((c) => c.type === 'dept' || c.type === 'ownerDept')) {
loadDepts()
}
Add watch:
watch(
() => props.candidates.some((c) => c.type === 'dept' || c.type === 'ownerDept'),
(need) => {
if (need && !deptTreeData.value.length) loadDepts()
}
)
el-select block and before the post el-select (or after post; order among non-person is free as long as v-else-if chain is correct). Recommended chain:isPersonLikeField → user select (unchanged)isPostField → post select (unchanged)isDeptLikeField → tree select (new)New block:
<el-tree-select
v-else-if="isDeptLikeField(rule.field)"
:model-value="personValueIds(rule.value)"
multiple
filterable
clearable
check-strictly
collapse-tags
collapse-tags-tooltip
default-expand-all
:data="deptTreeData"
:props="{ value: 'value', label: 'label', children: 'children' }"
placeholder="选择部门(任一命中)"
:disabled="disabled"
class="cond-rule-editor__dept-select"
@update:model-value="(v) => onValueChange(idx, Array.isArray(v) ? v.map(String) : [])"
/>
Reuse personValueIds for normalizing value to string[] (same shape).
Optional style (mirror post):
.cond-rule-editor__dept-select :deep(.el-select__selection) {
flex-wrap: wrap;
}
git add web-admin/src/components/oa-approval/flow-tree/ConditionRuleEditor.vue
git commit -m "$(cat <<'EOF'
feat(oa): tree multi-select for dept condition values
EOF
)"
Files:
docs/superpowers/specs/2026-07-30-oa-owner-fields-design.md (one line)docs/superpowers/specs/2026-08-06-oa-dept-condition-design.mdOptional: checkboxes in this plan file as tasks complete
[ ] Step 1: Update owner-fields historical note
In docs/superpowers/specs/2026-07-30-oa-owner-fields-design.md §4.2, replace:
- 条件分支:`ownerPerson` 与 `person` 同属人员类(`contains`);`ownerDept` 暂不进条件候选(与现有 `dept` 一致)。
with:
- 条件分支:`ownerPerson` 与 `person` 同属人员类(`contains`);`ownerDept` 与 `dept` 同属部门类(`contains`,精确 id)。详见 `2026-08-06-oa-dept-condition-design.md`。
In docs/superpowers/specs/2026-08-06-oa-dept-condition-design.md header, set:
**状态:** 已实现
Check off §8 验收标准 items that were verified.
git add docs/superpowers/specs/2026-07-30-oa-owner-fields-design.md docs/superpowers/specs/2026-08-06-oa-dept-condition-design.md
git commit -m "$(cat <<'EOF'
docs(oa): mark dept condition spec implemented
EOF
)"
cd /Users/wuzilong/work/00code/zkxw/backend && source venv/bin/activate && pytest tests/test_oa_dept_select.py tests/test_oa_engine.py tests/test_oa_publish.py tests/test_oa_person_select.py tests/test_oa_post_select.py -v
Expected: PASS.
After frontend changes, if graph update is required by repo rules:
cd /Users/wuzilong/work/00code/zkxw && ./scripts/update-graph.sh frontend
| Spec requirement | Task |
|---|---|
Whitelist dept / ownerDept (form switch) |
Task 4 (+ host already gated) |
Exact id contains match |
Task 1–2 |
| Condition value tree multi-select | Task 5 |
| Publish eligible types | Task 3 |
| Tests extract/engine/publish | Task 1–3 |
| Non-goals (subtree, initiator dept, ==) | Not implemented (intentional) |
| Update owner-fields note | Task 6 |
No TBD placeholders. Function names consistent: extract_dept_ids, dept_contains_match across tasks.