* 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.
142 lines
4.4 KiB
Python
142 lines
4.4 KiB
Python
#!/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()
|