feat: 统一阶段通知模板并支持逐项提醒负责人
This commit is contained in:
1 parent
1d85de4c7c
commit
3fb9c0658d
10 files changed
+301
-58
No files matched your search
+40
-12
@@ -16,8 +16,8 @@ import time
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
PREFIX = 'DINGTALK_PROGRESS_'
|
||||
KEYS = {'ENABLED', 'PROFILE', 'TARGET', 'PROJECT'}
|
||||
EVENTS = {'start': '开始推进', 'milestone': '阶段里程碑', 'blocked': '遇到阻塞',
|
||||
'complete': '本轮完成', 'failed': '执行失败'}
|
||||
EVENTS = ('start', 'milestone', 'blocked', 'complete', 'failed')
|
||||
INLINE_MENTION = re.compile(r'@\{([^{}\r\n]+)\}')
|
||||
DNS_ERROR = re.compile(r'lookup [\w.-]+(?: on [^:\s]+(?::\d+)?)?: [^\n]*'
|
||||
r'(?:i/o timeout|no such host|server misbehaving|temporary failure in name resolution)', re.I)
|
||||
|
||||
@@ -123,12 +123,36 @@ def check_login(cli, profile, run, sleep):
|
||||
raise NoticeError('个人 DWS 未登录或授权已失效,请负责人完成登录;本次未发送。')
|
||||
|
||||
|
||||
def mention_names(names):
|
||||
names = list(dict.fromkeys(n.strip() for value in names for n in value.replace(',', ',').split(',')))
|
||||
if any(not name for name in names):
|
||||
raise NoticeError('提醒对象不能为空。')
|
||||
return names
|
||||
|
||||
|
||||
def format_mentions(text, names, people=None):
|
||||
names = mention_names(names)
|
||||
placed = {match.group(1).strip() for match in INLINE_MENTION.finditer(text)}
|
||||
if '@{' in INLINE_MENTION.sub('', text) or not placed.issubset(names):
|
||||
raise NoticeError('正文中的 @{完整姓名} 必须完整填写,并在 --at 中列出同一姓名。')
|
||||
by_name = {name: person for person, name in people.items()} if people is not None else None
|
||||
if by_name is not None and not set(names).issubset(by_name):
|
||||
raise NoticeError('提醒对象未全部唯一匹配,本次停止,不删掉 @ 后发送。')
|
||||
|
||||
def marker(name):
|
||||
return f'<@{by_name[name]}>' if by_name is not None else f'@{name}'
|
||||
|
||||
text = INLINE_MENTION.sub(lambda match: marker(match.group(1).strip()), text)
|
||||
remaining = [marker(name) for name in names if name not in placed]
|
||||
if remaining:
|
||||
text += '\n\n' + ' '.join(remaining)
|
||||
return text
|
||||
|
||||
|
||||
def mentions(names, cli, profile, run, sleep):
|
||||
people = {}
|
||||
names = list(dict.fromkeys(n.strip() for value in names for n in value.replace(',', ',').split(',')))
|
||||
names = mention_names(names)
|
||||
for name in names:
|
||||
if not name:
|
||||
raise NoticeError('提醒对象不能为空。')
|
||||
data = invoke([cli, 'aisearch', 'person', '--query', name, '--dimension', 'name',
|
||||
'--profile', profile, '--format', 'json'], run, sleep)
|
||||
candidates = {}
|
||||
@@ -187,6 +211,7 @@ def parse_args(argv):
|
||||
init.add_argument('--replace', action='store_true')
|
||||
progress = commands.add_parser('progress')
|
||||
progress.add_argument('event', choices=EVENTS)
|
||||
progress.add_argument('--stage', required=True, help='项目当前阶段编号及中文名称;每次从有效项目记录取得')
|
||||
progress.add_argument('summary', nargs='*')
|
||||
progress.add_argument('--file')
|
||||
message = commands.add_parser('message')
|
||||
@@ -196,7 +221,7 @@ def parse_args(argv):
|
||||
body.add_argument('--file')
|
||||
check = commands.add_parser('check', help='检查固定账号登录及可选姓名,不发送')
|
||||
for command in (progress, message, check):
|
||||
command.add_argument('--at', action='append', default=[], help='完整姓名;可重复或用逗号分隔')
|
||||
command.add_argument('--at', action='append', default=[], help='完整姓名;正文可用 @{完整姓名} 定位,可重复或用逗号分隔')
|
||||
for command in (progress, message):
|
||||
command.add_argument('--dry-run', action='store_true', help='完全离线,只预览中文正文')
|
||||
return parser.parse_args(argv)
|
||||
@@ -215,12 +240,13 @@ def render(args, config, root):
|
||||
text = ' '.join(args.summary) if args.command == 'progress' else args.text
|
||||
if not text.strip() or len(text.encode('utf-8')) > 12000:
|
||||
raise NoticeError('请提供不超过 12000 字节的简短中文摘要。')
|
||||
title = EVENTS[args.event] if args.command == 'progress' else args.title.strip()
|
||||
if not title or len(title) > 200 or '\n' in title or '\r' in title:
|
||||
raw_title = args.stage if args.command == 'progress' else args.title
|
||||
title = raw_title.strip()
|
||||
if not title or len(title) > 200 or any(ord(c) < 32 or ord(c) == 127 for c in raw_title):
|
||||
raise NoticeError('请使用不超过 200 字的单行标题。')
|
||||
project = config.get('PROJECT') or root.name
|
||||
timestamp = dt.datetime.now().astimezone().strftime('%Y-%m-%d %H:%M %z')
|
||||
return f'## {project}|{title}\n\n项目进度自动汇报 · {timestamp}\n\n{text}'
|
||||
return f'**{project}|{title}**\n\n项目进度自动汇报 · {timestamp}\n\n{text}'
|
||||
|
||||
|
||||
def main(argv=None, root=ROOT, run=subprocess.run, sleep=time.sleep):
|
||||
@@ -236,7 +262,8 @@ def main(argv=None, root=ROOT, run=subprocess.run, sleep=time.sleep):
|
||||
return 0
|
||||
config = read_config(root)
|
||||
if args.command != 'check' and args.dry_run:
|
||||
emit('preview', '仅离线预览,账号、群与 @ 效果未验证。', text=render(args, config, root), mentions=args.at)
|
||||
text = format_mentions(render(args, config, root), args.at)
|
||||
emit('preview', '仅离线预览,账号、群与 @ 效果未验证。', text=text, mentions=mention_names(args.at))
|
||||
return 0
|
||||
if args.command != 'check' and config.get('ENABLED') != '1':
|
||||
emit('skipped', '通知尚未初始化或未启用;本次未发送,主任务继续。')
|
||||
@@ -244,6 +271,8 @@ def main(argv=None, root=ROOT, run=subprocess.run, sleep=time.sleep):
|
||||
if not config.get('PROFILE') or not config.get('TARGET'):
|
||||
raise NoticeError('请由 Hub 先确认个人账号和项目群并执行 init。')
|
||||
text = render(args, config, root) if args.command != 'check' else None
|
||||
if text is not None:
|
||||
format_mentions(text, args.at) # Validate inline names before any DWS call.
|
||||
cli = dws_path()
|
||||
profile = config['PROFILE']
|
||||
if args.command == 'check':
|
||||
@@ -252,8 +281,7 @@ def main(argv=None, root=ROOT, run=subprocess.run, sleep=time.sleep):
|
||||
if args.command == 'check':
|
||||
emit('checked', '登录及所请求的姓名查询通过;未发送,未验证实际提醒效果。', names=list(people.values()))
|
||||
return 0
|
||||
if people:
|
||||
text += '\n\n' + ' '.join(f'<@{person}>' for person in people)
|
||||
text = format_mentions(text, args.at, people)
|
||||
command = [cli, 'chat', '+messages-send', '--as', 'user', '--group', config['TARGET'],
|
||||
'--markdown', text, '--profile', profile, '--yes', '--format', 'json']
|
||||
if people:
|
||||
|
||||
Reference in new issue
Block a user