dsh-project-deck 0.1.2 → 0.1.3
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/README.md +27 -0
- package/backend/__pycache__/panel.cpython-314.pyc +0 -0
- package/backend/deck +102 -9
- package/backend/panel.py +4 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,6 +67,33 @@ deck demo # 演示环境
|
|
|
67
67
|
无变化:官网改版、基础设施、…
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
## 通知渠道(可选,默认 wxpush)
|
|
71
|
+
|
|
72
|
+
播报通过 `report.delivery` 选择发送器,**不带任何内置账号**:
|
|
73
|
+
|
|
74
|
+
```jsonc
|
|
75
|
+
"report": {
|
|
76
|
+
"enabled": true,
|
|
77
|
+
"image": false, // 附图仅 wxpush 通道支持
|
|
78
|
+
"delivery": {
|
|
79
|
+
"type": "wxpush", // wxpush | serverchan | webhook | command
|
|
80
|
+
"url": "http://127.0.0.1:9098", // wxpush
|
|
81
|
+
"key": "", // ServerChan SendKey
|
|
82
|
+
"webhook": "", // 任意 webhook(企业微信机器人等)
|
|
83
|
+
"command": "" // 自写脚本:stdin 收文本,退出码 0 = 成功
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
| 类型 | 依赖 | 说明 |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `wxpush` | 本机 wxpush 服务 | 支持文字 + 看板图;token 过期时会入队等恢复 |
|
|
91
|
+
| `serverchan` | ServerChan SendKey | 纯 HTTP,无 token 过期问题 |
|
|
92
|
+
| `webhook` | 任意 URL | 发 `{"msgtype":"text","text":{"content":...}}`,兼容企业微信机器人 |
|
|
93
|
+
| `command` | 你的脚本 | 最灵活,退出码判定 |
|
|
94
|
+
|
|
95
|
+
`deck status` 会显示当前通道与连通状态;通道不通时播报不会堆积(附图除外,会有限重试)。
|
|
96
|
+
|
|
70
97
|
## 配置(`~/.project-deck/config.json`)
|
|
71
98
|
|
|
72
99
|
| key | 默认 | 说明 |
|
|
Binary file
|
package/backend/deck
CHANGED
|
@@ -1017,6 +1017,97 @@ def cmd_scan(argv):
|
|
|
1017
1017
|
|
|
1018
1018
|
|
|
1019
1019
|
# ── 发送 ────────────────────────────────────────────────────
|
|
1020
|
+
def delivery_cfg(cfg=None):
|
|
1021
|
+
cfg = cfg or load_json(CONFIG, {})
|
|
1022
|
+
d = (cfg.get('report') or {}).get('delivery') or {}
|
|
1023
|
+
return {
|
|
1024
|
+
'type': (d.get('type') or 'wxpush').lower(),
|
|
1025
|
+
'url': (d.get('url') or 'http://127.0.0.1:9098').rstrip('/'),
|
|
1026
|
+
'key': d.get('key') or '',
|
|
1027
|
+
'webhook': d.get('webhook') or '',
|
|
1028
|
+
'command': d.get('command') or '',
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
def _http_json(url, payload, timeout=20, headers=None):
|
|
1033
|
+
body = json.dumps(payload, ensure_ascii=False).encode()
|
|
1034
|
+
req = urllib.request.Request(url, data=body,
|
|
1035
|
+
headers={'Content-Type': 'application/json', **(headers or {})})
|
|
1036
|
+
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
1037
|
+
return r.status, json.loads(r.read().decode() or '{}')
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
def delivery_send(text):
|
|
1041
|
+
"""按 report.delivery.type 发送一条文本。返回 (0|2|3, 说明)。"""
|
|
1042
|
+
d = delivery_cfg()
|
|
1043
|
+
t = d['type']
|
|
1044
|
+
try:
|
|
1045
|
+
if t == 'wxpush':
|
|
1046
|
+
if not d['url']:
|
|
1047
|
+
return 3, 'wxpush url 未配置'
|
|
1048
|
+
code, body = _http_json(d['url'] + '/send', {'message': text})
|
|
1049
|
+
if code == 200:
|
|
1050
|
+
return 0, '已送达(wxpush)'
|
|
1051
|
+
if code == 202:
|
|
1052
|
+
return 2, '已入队(wxpush 未接通)'
|
|
1053
|
+
return 3, f'wxpush HTTP {code}'
|
|
1054
|
+
if t == 'serverchan':
|
|
1055
|
+
if not d['key']:
|
|
1056
|
+
return 2, 'ServerChan key 未配置'
|
|
1057
|
+
url = f"https://sctapi.ftqq.com/{d['key']}.send"
|
|
1058
|
+
import urllib.parse
|
|
1059
|
+
data = urllib.parse.urlencode({'title': '项目播报', 'desp': text}).encode()
|
|
1060
|
+
req = urllib.request.Request(url, data=data)
|
|
1061
|
+
with urllib.request.urlopen(req, timeout=20) as r:
|
|
1062
|
+
j = json.loads(r.read().decode() or '{}')
|
|
1063
|
+
if j.get('code') in (0, '0'):
|
|
1064
|
+
return 0, '已送达(ServerChan)'
|
|
1065
|
+
return 3, f"ServerChan 返回 {str(j)[:120]}"
|
|
1066
|
+
if t == 'webhook':
|
|
1067
|
+
if not d['webhook']:
|
|
1068
|
+
return 2, 'webhook 未配置'
|
|
1069
|
+
code, body = _http_json(d['webhook'], {
|
|
1070
|
+
'msgtype': 'text', 'text': {'content': text}, 'message': text})
|
|
1071
|
+
return (0, '已送达(webhook)') if code in (200, 204) else (3, f'webhook HTTP {code}')
|
|
1072
|
+
if t == 'command':
|
|
1073
|
+
if not d['command']:
|
|
1074
|
+
return 2, 'command 未配置'
|
|
1075
|
+
r = subprocess.run(d['command'], shell=True, input=text, capture_output=True, text=True, timeout=60)
|
|
1076
|
+
if r.returncode == 0:
|
|
1077
|
+
return 0, '已送达(command)'
|
|
1078
|
+
return 3, f"command 退出码 {r.returncode}: {(r.stderr or r.stdout)[:120]}"
|
|
1079
|
+
return 3, f'未知 delivery.type: {t}'
|
|
1080
|
+
except Exception as e:
|
|
1081
|
+
return 3, f'{t} 发送失败: {e}'
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
def delivery_status():
|
|
1085
|
+
"""给面板/status 用:返回 {ok, stale, detail, pending}。"""
|
|
1086
|
+
d = delivery_cfg()
|
|
1087
|
+
out = {'type': d['type'], 'ok': False, 'detail': '', 'pending': 0}
|
|
1088
|
+
try:
|
|
1089
|
+
if d['type'] == 'wxpush':
|
|
1090
|
+
with urllib.request.urlopen(d['url'] + '/health', timeout=5) as r:
|
|
1091
|
+
j = json.loads(r.read().decode() or '{}')
|
|
1092
|
+
out['ok'] = bool(j.get('ready') or j.get('authorized'))
|
|
1093
|
+
out['pending'] = int((j.get('outbox') or {}).get('pending') or 0)
|
|
1094
|
+
out['detail'] = 'wxpush 已接通' if out['ok'] else ('wxpush 未接通:需要扫码授权或给 bot 发消息刷新')
|
|
1095
|
+
if j.get('token_dead'):
|
|
1096
|
+
out['detail'] = 'wxpush 登录态已失效:需要重新扫码'
|
|
1097
|
+
elif d['type'] == 'serverchan':
|
|
1098
|
+
out['ok'] = bool(d['key'])
|
|
1099
|
+
out['detail'] = 'ServerChan 已配置' if out['ok'] else 'ServerChan key 未配置'
|
|
1100
|
+
elif d['type'] == 'webhook':
|
|
1101
|
+
out['ok'] = bool(d['webhook'])
|
|
1102
|
+
out['detail'] = 'webhook 已配置' if out['ok'] else 'webhook 未配置'
|
|
1103
|
+
elif d['type'] == 'command':
|
|
1104
|
+
out['ok'] = bool(d['command'])
|
|
1105
|
+
out['detail'] = f"command: {d['command']}" if out['ok'] else 'command 未配置'
|
|
1106
|
+
except Exception as e:
|
|
1107
|
+
out['detail'] = f'{d["type"]} 不可达: {e}'
|
|
1108
|
+
return out
|
|
1109
|
+
|
|
1110
|
+
|
|
1020
1111
|
def channel_state():
|
|
1021
1112
|
return load_json(CHANNEL_STATE, {'stale': False, 'fails': 0})
|
|
1022
1113
|
|
|
@@ -1054,15 +1145,12 @@ def mark_channel(ok, err=''):
|
|
|
1054
1145
|
|
|
1055
1146
|
|
|
1056
1147
|
def send_wechat(text):
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
r = subprocess.run([WXSEND], input=text, capture_output=True, text=True, timeout=30)
|
|
1060
|
-
msg = (r.stdout or r.stderr).strip()
|
|
1061
|
-
if r.returncode == 0:
|
|
1148
|
+
code, msg = delivery_send(text)
|
|
1149
|
+
if code == 0:
|
|
1062
1150
|
mark_channel(True)
|
|
1063
|
-
elif
|
|
1064
|
-
mark_channel(False, msg
|
|
1065
|
-
return
|
|
1151
|
+
elif code == 2:
|
|
1152
|
+
mark_channel(False, msg[:120])
|
|
1153
|
+
return code, msg
|
|
1066
1154
|
|
|
1067
1155
|
|
|
1068
1156
|
def refresh_local_node(cfg=None):
|
|
@@ -1132,6 +1220,8 @@ def do_report_with_image(cfg, quiet=False, force=None):
|
|
|
1132
1220
|
"""播报(文字)+ 可选附图;force=True/False 覆盖 config,None 跟随 config。"""
|
|
1133
1221
|
code, text, msg = do_report(cfg, quiet)
|
|
1134
1222
|
want = cfg.get('report', {}).get('image') if force is None else force
|
|
1223
|
+
if delivery_cfg(cfg)['type'] != 'wxpush':
|
|
1224
|
+
want = False
|
|
1135
1225
|
if code == 0 and want:
|
|
1136
1226
|
try:
|
|
1137
1227
|
png = render_image(cfg)
|
|
@@ -1203,6 +1293,8 @@ def flush_pending_image():
|
|
|
1203
1293
|
p = load_json(PENDING_IMG, None)
|
|
1204
1294
|
if not p:
|
|
1205
1295
|
return False
|
|
1296
|
+
if delivery_cfg()['type'] != 'wxpush':
|
|
1297
|
+
return False
|
|
1206
1298
|
if channel_stale():
|
|
1207
1299
|
return False
|
|
1208
1300
|
# 失败后 2 分钟内不重复尝试,避免空转
|
|
@@ -1597,7 +1689,8 @@ def cmd_status():
|
|
|
1597
1689
|
nxt += 1800
|
|
1598
1690
|
print(f"下次播报: {datetime.fromtimestamp(nxt):%Y-%m-%d %H:%M}")
|
|
1599
1691
|
wx = subprocess.run([WXSEND, '--status'], capture_output=True, text=True)
|
|
1600
|
-
|
|
1692
|
+
st = delivery_status()
|
|
1693
|
+
print(f"通知渠道: {st['type']} · " + ('✅ ' if st['ok'] else '⚠️ ') + st['detail'])
|
|
1601
1694
|
return 0 if pid else 1
|
|
1602
1695
|
|
|
1603
1696
|
|
package/backend/panel.py
CHANGED
|
@@ -48,12 +48,10 @@ P = load_deck()
|
|
|
48
48
|
|
|
49
49
|
# ── 数据汇总 ────────────────────────────────────────────────
|
|
50
50
|
def wechat_state():
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
except (OSError, subprocess.SubprocessError):
|
|
56
|
-
return {'ok': False, 'raw': 'wxsend 不可用', 'code': 3}
|
|
51
|
+
st = P.delivery_status()
|
|
52
|
+
return {'ok': bool(st.get('ok')), 'raw': st.get('detail', ''),
|
|
53
|
+
'code': 0 if st.get('ok') else 2, 'type': st.get('type'),
|
|
54
|
+
'pending': st.get('pending', 0)}
|
|
57
55
|
|
|
58
56
|
|
|
59
57
|
def project_view(p):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-project-deck",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "DeepSeek Harness plugin: a project deck for your repos — registry, dashboard, scheduled WeChat reports, auto-discovery, multi-node scanning.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|