Files
dingtalk-workspace-cli/skills/mono/scripts/contact_dept_members.py
Ari 109ad13844 fix: honor global --jq/--fields on product commands; round-2 QA fixes (#575)
* fix: honor global --jq/--fields on product commands; round-2 QA fixes

Make the global --jq / --fields output filters actually work for the
product (MCP) commands. The helper Formatter used by every product
command ignored them, so they were silent no-ops there (they already
worked for `dws api`). Expose Fields()/JQ() on the ToolCaller interface
and apply the existing output.WriteFiltered path in the helper
Formatter's PrintJSON. The handful of bespoke utility commands
(auth/config/profile/...) still encode directly and are documented as
such.

Additional CLI fixes surfaced by the second real-machine QA pass:
- sheet write-image: emit clean JSON under --format json (suppress the
  progress lines that leaked onto stdout, same as media-upload/export)
- sheet range batch-set-style: under --format json, collect per-item
  results into a single JSON object instead of printing N separate ones
- chat download-media: create the output directory when missing and
  strip URL-encoded path separators from the inferred filename so the
  file actually lands instead of failing on a missing subdirectory
- pat chmod, aitable, sheet, chat, attendance: correct --help text
  (real scope names, non-existent subcommands, flag requiredness,
  alxs -> axls typo)

Helper scripts (mono and multi):
- minutes_extract_todos: parse dingtalkTodoList/actions (there is no
  todos key), so todos are no longer silently dropped
- sync the multi copies of chat_export_messages / chat_history_with_user
  (were crashing with AttributeError), minutes_list_parse /
  minutes_recent_summary, and calendar_free_slot_finder to the fixed
  mono versions

Skill docs (mono and multi): correct return-structure keys, flag names,
deprecated command routing (doc download -> drive download), enum values
and server-side limitations across products; update the global
reference to note --jq/--fields now apply to product commands.

* fix(skill): make skill setup --dry-run a no-op preview; doc/help fixups

skill setup ignored the global --dry-run flag and always wrote the skill
files (overwriting an existing install). Short-circuit into a preview
that lists the source, target dirs and selected sub-skills without
touching the filesystem.

Also correct a few doc/help mismatches found in the round-3 health check:
- attendance vacation balance/records quick-reference examples were
  missing the required --leave-code flag
- mail mailbox list --help described the returned field as "mailboxes"
  but the real field is "emailAccounts"

* docs: clarify --fields projects top-level/list keys, use --jq for nested

* docs: drop QA voice ("真机") and don't state env-specific quirks as absolute rules

The QA-driven doc/comment edits leaked test-process narration ("真机实测")
and this environment/account's quirks stated as universal rules into the
skill files, which are general-purpose instructions for any org/account.
Strip the "真机" narration everywhere; reword environment-specific findings
(PUBLIC sharing disabled by org policy, transient 1002, sender-open-dingtalk-id
behaviour) from absolute bans into conditional hints; keep genuinely
universal command behaviour, just without the QA voice.
2026-07-08 14:06:57 +08:00

142 lines
4.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
按部门名称搜索并列出所有成员(自动 deptId 解析)
用法:
python contact_dept_members.py --query "技术部"
python contact_dept_members.py --query "产品" --dry-run
"""
import sys
import re
import json
import subprocess
import argparse
from typing import List, Any, Optional
def strip_highlight(text: str) -> str:
"""去除 dept search 返回名称中的 <red>…</red> 高亮标签。"""
if not isinstance(text, str):
return text
return re.sub(r'</?red>', '', text)
def run_dws(
args: List[str], dry_run: bool = False,
) -> Optional[Any]:
cmd = ['dws'] + args
if dry_run:
print(f"[dry-run] {' '.join(cmd)}")
return None
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=60
)
if result.returncode != 0:
print(f"错误:{result.stderr.strip()}", file=sys.stderr)
return None
return json.loads(result.stdout)
except (subprocess.TimeoutExpired, json.JSONDecodeError,
FileNotFoundError) as e:
print(f"错误:{e}", file=sys.stderr)
return None
def main():
parser = argparse.ArgumentParser(
description='按部门名称搜索并列出所有成员'
)
parser.add_argument(
'--query', required=True, help='部门名称关键词'
)
parser.add_argument('--dry-run', action='store_true')
args = parser.parse_args()
print(f'🔍 搜索部门: {args.query}')
dept_data = run_dws([
'contact', 'dept', 'search',
'--query', args.query, '--format', 'json',
], dry_run=args.dry_run)
if args.dry_run:
run_dws([
'contact', 'dept', 'list-members',
'--ids', '<DEPT_ID>', '--format', 'json',
], dry_run=True)
return
if not dept_data:
print('未找到匹配部门')
sys.exit(1)
# dept search 返回顶层 deptList;兼容 result 包裹与历史 items/result 键。
if isinstance(dept_data, list):
depts = dept_data
else:
inner = dept_data.get('result', dept_data) if isinstance(dept_data, dict) else {}
if not isinstance(inner, dict):
inner = dept_data if isinstance(dept_data, dict) else {}
depts = (inner.get('deptList')
or dept_data.get('deptList')
or dept_data.get('items')
or [])
if not depts:
print('未找到匹配部门')
sys.exit(1)
for dept in depts:
dept_id = dept.get('id') or dept.get('deptId')
dept_name = strip_highlight(
dept.get('name') or dept.get('deptName', '未知')
)
if not dept_id:
continue
print(f"\n📂 {dept_name} (ID: {dept_id})")
print('-' * 40)
members_data = run_dws([
'contact', 'dept', 'list-members',
'--ids', str(dept_id), '--format', 'json',
])
if not members_data:
print(' 无法获取成员列表')
continue
# list-members 返回 deptUserList;兼容 result 包裹与历史 userlist 键。
if isinstance(members_data, list):
members = members_data
else:
m_inner = (members_data.get('result', members_data)
if isinstance(members_data, dict) else {})
if not isinstance(m_inner, dict):
m_inner = members_data if isinstance(members_data, dict) else {}
members = (m_inner.get('deptUserList')
or members_data.get('deptUserList')
or members_data.get('userlist')
or [])
if not members:
print(' (暂无成员)')
continue
for m in members:
# list-members 每项形如 {"userInfo": {"name":..., "userId":...}},
# 成员字段嵌在 userInfo 下;兼容历史扁平结构。
info = m.get('userInfo', m)
name = info.get('name') or info.get('userName', '未知')
title = info.get('title') or info.get('position', '')
uid = info.get('userId') or info.get('userid', '')
line = f" 👤 {name}"
if title:
line += f" ({title})"
if uid:
line += f" [ID: {uid}]"
print(line)
print(f" 共 {len(members)} 人")
if __name__ == '__main__':
main()