dsh-project-deck 0.1.3 → 0.1.4
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 +15 -3
- package/backend/__pycache__/panel.cpython-314.pyc +0 -0
- package/backend/deck +198 -2
- package/backend/panel.html +86 -0
- package/backend/panel.py +60 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,9 +67,21 @@ deck demo # 演示环境
|
|
|
67
67
|
无变化:官网改版、基础设施、…
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
##
|
|
70
|
+
## 通知渠道(可选)
|
|
71
71
|
|
|
72
|
-
播报通过 `report.delivery`
|
|
72
|
+
播报通过 `report.delivery` 选择发送器,**默认不依赖任何外部服务**:
|
|
73
|
+
|
|
74
|
+
| 类型 | 说明 |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `ilink` | **内置微信连接**:面板点「连接微信」→ 扫码 → 在微信里给 bot 发一句话即可推送,不需要额外服务 |
|
|
77
|
+
| `wxpush` | 复用本机 [wxpush](https://github.com/) 网关(支持附图) |
|
|
78
|
+
| `serverchan` | ServerChan SendKey |
|
|
79
|
+
| `webhook` | 任意 URL(企业微信机器人等) |
|
|
80
|
+
| `command` | 自写脚本,stdin 收文本,退出码 0 = 成功 |
|
|
81
|
+
|
|
82
|
+
面板顶部有「通知渠道」下拉,选完保存即可;`ilink` 模式下会直接显示二维码。
|
|
83
|
+
|
|
84
|
+
```jsonc
|
|
73
85
|
|
|
74
86
|
```jsonc
|
|
75
87
|
"report": {
|
|
@@ -77,7 +89,7 @@ deck demo # 演示环境
|
|
|
77
89
|
"image": false, // 附图仅 wxpush 通道支持
|
|
78
90
|
"delivery": {
|
|
79
91
|
"type": "wxpush", // wxpush | serverchan | webhook | command
|
|
80
|
-
"url": "http://127.0.0.1:9098", // wxpush
|
|
92
|
+
"url": "http://127.0.0.1:9098", // wxpush(ilink 模式忽略)
|
|
81
93
|
"key": "", // ServerChan SendKey
|
|
82
94
|
"webhook": "", // 任意 webhook(企业微信机器人等)
|
|
83
95
|
"command": "" // 自写脚本:stdin 收文本,退出码 0 = 成功
|
|
Binary file
|
package/backend/deck
CHANGED
|
@@ -1017,6 +1017,171 @@ def cmd_scan(argv):
|
|
|
1017
1017
|
|
|
1018
1018
|
|
|
1019
1019
|
# ── 发送 ────────────────────────────────────────────────────
|
|
1020
|
+
# ── 内置微信通道(iLink,扫码即用)────────────────────────────
|
|
1021
|
+
WECHAT_STATE = os.path.join(BASE, 'wechat.json')
|
|
1022
|
+
WECHAT_QR = os.path.join(BASE, 'wechat-qr.png')
|
|
1023
|
+
ILINK_BASE = 'https://ilinkai.weixin.qq.com/ilink'
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
def wechat_state():
|
|
1027
|
+
return load_json(WECHAT_STATE, {})
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
def save_wechat_state(d):
|
|
1031
|
+
save_json(WECHAT_STATE, d)
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def _ilink_headers(token=None):
|
|
1035
|
+
uin = base64.b64encode(str(secrets.randbelow(2**32)).encode()).decode()
|
|
1036
|
+
h = {'Content-Type': 'application/json', 'AuthorizationType': 'ilink_bot_token',
|
|
1037
|
+
'X-WECHAT-UIN': uin, 'iLink-App-Id': 'bot', 'iLink-App-ClientVersion': '132105'}
|
|
1038
|
+
if token:
|
|
1039
|
+
h['Authorization'] = f'Bearer {token}'
|
|
1040
|
+
return h
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
def _ilink_request(path, body=None, method='POST', params=None, token=None, timeout=30):
|
|
1044
|
+
url = ILINK_BASE + path
|
|
1045
|
+
if params:
|
|
1046
|
+
url += '?' + urllib.parse.urlencode(params)
|
|
1047
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
1048
|
+
req = urllib.request.Request(url, data=data, headers=_ilink_headers(token), method=method)
|
|
1049
|
+
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
1050
|
+
return json.loads(r.read().decode() or '{}')
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
def _extract_ctx(data):
|
|
1054
|
+
found = {}
|
|
1055
|
+
|
|
1056
|
+
def walk(node):
|
|
1057
|
+
if isinstance(node, dict):
|
|
1058
|
+
for k, v in node.items():
|
|
1059
|
+
if k == 'context_token' and isinstance(v, str) and v and 'ctx' not in found:
|
|
1060
|
+
found['ctx'] = v
|
|
1061
|
+
if k in ('from_user_id', 'user_id', 'ilink_user_id') and isinstance(v, str) and v and 'uid' not in found:
|
|
1062
|
+
found['uid'] = v
|
|
1063
|
+
walk(v)
|
|
1064
|
+
elif isinstance(node, list):
|
|
1065
|
+
for it in node:
|
|
1066
|
+
walk(it)
|
|
1067
|
+
walk(data)
|
|
1068
|
+
return found.get('ctx'), found.get('uid')
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
def wechat_qr_png(url):
|
|
1072
|
+
"""用本机 Python 生成二维码 PNG(面板运行在系统 Python 上,可能没有 qrcode 模块)。"""
|
|
1073
|
+
for py in ('/usr/local/bin/python3', sys.executable, '/usr/bin/python3'):
|
|
1074
|
+
try:
|
|
1075
|
+
r = subprocess.run([py, '-c', 'import qrcode,sys;qrcode.make(sys.argv[1]).save(sys.argv[2])', url, WECHAT_QR],
|
|
1076
|
+
capture_output=True, timeout=20)
|
|
1077
|
+
if r.returncode == 0 and os.path.exists(WECHAT_QR):
|
|
1078
|
+
return True
|
|
1079
|
+
except Exception:
|
|
1080
|
+
continue
|
|
1081
|
+
return False
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
def wechat_start_login():
|
|
1085
|
+
r = _ilink_request('/bot/get_bot_qrcode', method='GET', params={'bot_type': 3})
|
|
1086
|
+
ticket = r.get('qrcode') or r.get('qrcode_id')
|
|
1087
|
+
url = r.get('qrcode_img_content') or r.get('qrcode_url') or r.get('url')
|
|
1088
|
+
st = wechat_state()
|
|
1089
|
+
st.update({'qr_ticket': ticket, 'qr_url': url, 'qr_at': time.time(),
|
|
1090
|
+
'bot_token': None, 'context_token': None, 'last_error': None})
|
|
1091
|
+
save_wechat_state(st)
|
|
1092
|
+
if url:
|
|
1093
|
+
wechat_qr_png(url)
|
|
1094
|
+
return st
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
def wechat_check_login():
|
|
1098
|
+
st = wechat_state()
|
|
1099
|
+
ticket = st.get('qr_ticket')
|
|
1100
|
+
if not ticket:
|
|
1101
|
+
return {'status': 'none'}
|
|
1102
|
+
try:
|
|
1103
|
+
r = _ilink_request('/bot/get_qrcode_status', method='GET', params={'qrcode': ticket}, timeout=15)
|
|
1104
|
+
except Exception as e:
|
|
1105
|
+
st['last_error'] = f'扫码状态查询失败: {e}'
|
|
1106
|
+
save_wechat_state(st)
|
|
1107
|
+
return {'status': 'error', 'error': str(e)}
|
|
1108
|
+
status = str(r.get('status') or '')
|
|
1109
|
+
if status == 'confirmed':
|
|
1110
|
+
token = r.get('bot_token')
|
|
1111
|
+
uid = r.get('ilink_user_id')
|
|
1112
|
+
st.update({'bot_token': token, 'user_id': uid, 'authorized_at': time.time(),
|
|
1113
|
+
'qr_ticket': None, 'qr_url': None, 'last_error': None})
|
|
1114
|
+
save_wechat_state(st)
|
|
1115
|
+
log('[wechat] 扫码授权成功')
|
|
1116
|
+
return {'status': 'confirmed'}
|
|
1117
|
+
if status == 'expired':
|
|
1118
|
+
wechat_start_login()
|
|
1119
|
+
return {'status': 'expired'}
|
|
1120
|
+
return {'status': status or 'wait'}
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
def wechat_poll_once(timeout=60):
|
|
1124
|
+
st = wechat_state()
|
|
1125
|
+
token = st.get('bot_token')
|
|
1126
|
+
if not token:
|
|
1127
|
+
return
|
|
1128
|
+
body = {'timeout': timeout, 'limit': 100,
|
|
1129
|
+
'get_updates_buf': st.get('updates_buf') or '',
|
|
1130
|
+
'base_info': {'channel_version': '1.0.0'}}
|
|
1131
|
+
try:
|
|
1132
|
+
r = _ilink_request('/bot/getupdates', body, token=token, timeout=timeout + 10)
|
|
1133
|
+
except Exception:
|
|
1134
|
+
return
|
|
1135
|
+
ctx, uid = _extract_ctx(r)
|
|
1136
|
+
if r.get('get_updates_buf'):
|
|
1137
|
+
st['updates_buf'] = r.get('get_updates_buf')
|
|
1138
|
+
if ctx:
|
|
1139
|
+
st['context_token'] = ctx
|
|
1140
|
+
log('[wechat] 捕获 context_token,可主动推送')
|
|
1141
|
+
if uid:
|
|
1142
|
+
st['user_id'] = uid
|
|
1143
|
+
save_wechat_state(st)
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
def wechat_send_text(text):
|
|
1147
|
+
st = wechat_state()
|
|
1148
|
+
token = st.get('bot_token')
|
|
1149
|
+
ctx = st.get('context_token')
|
|
1150
|
+
uid = st.get('user_id')
|
|
1151
|
+
if not token:
|
|
1152
|
+
return 2, '微信未登录:请在面板扫码'
|
|
1153
|
+
if not ctx:
|
|
1154
|
+
return 2, '缺少 context_token:请在微信里给 bot 发一条消息'
|
|
1155
|
+
body = {'msg': {'from_user_id': '', 'to_user_id': uid or '',
|
|
1156
|
+
'client_id': f'deck:{int(time.time())}:{secrets.randbelow(999999)}',
|
|
1157
|
+
'message_type': 2, 'message_state': 2, 'context_token': ctx,
|
|
1158
|
+
'item_list': [{'type': 1, 'text_item': {'text': text}}]},
|
|
1159
|
+
'base_info': {'channel_version': '1.0.0'}}
|
|
1160
|
+
try:
|
|
1161
|
+
r = _ilink_request('/bot/sendmessage', body, token=token, timeout=25)
|
|
1162
|
+
except Exception as e:
|
|
1163
|
+
return 3, f'微信发送失败: {e}'
|
|
1164
|
+
if r.get('message_id'):
|
|
1165
|
+
return 0, '已送达(微信)'
|
|
1166
|
+
if r.get('ret', 0) == -2:
|
|
1167
|
+
return 2, 'context_token 失效:请在微信里给 bot 发一条消息'
|
|
1168
|
+
return 2, f'未被投递: {str(r)[:120]}'
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
def wechat_status():
|
|
1172
|
+
st = wechat_state()
|
|
1173
|
+
logged = bool(st.get('bot_token'))
|
|
1174
|
+
has_ctx = bool(st.get('context_token'))
|
|
1175
|
+
return {'logged': logged, 'hasContext': has_ctx,
|
|
1176
|
+
'qrUrl': st.get('qr_url') if not logged else None,
|
|
1177
|
+
'qrAt': st.get('qr_at') if not logged else None,
|
|
1178
|
+
'authorizedAt': st.get('authorized_at'),
|
|
1179
|
+
'lastError': st.get('last_error'),
|
|
1180
|
+
'detail': ('微信已连接,可主动推送' if logged and has_ctx else
|
|
1181
|
+
('微信已扫码,但还没收到你的消息(去给 bot 发一句)' if logged else
|
|
1182
|
+
('等待扫码' if st.get('qr_url') else '未连接:点「连接微信」生成二维码')))}
|
|
1183
|
+
|
|
1184
|
+
|
|
1020
1185
|
def delivery_cfg(cfg=None):
|
|
1021
1186
|
cfg = cfg or load_json(CONFIG, {})
|
|
1022
1187
|
d = (cfg.get('report') or {}).get('delivery') or {}
|
|
@@ -1042,6 +1207,8 @@ def delivery_send(text):
|
|
|
1042
1207
|
d = delivery_cfg()
|
|
1043
1208
|
t = d['type']
|
|
1044
1209
|
try:
|
|
1210
|
+
if t == 'ilink':
|
|
1211
|
+
return wechat_send_text(text)
|
|
1045
1212
|
if t == 'wxpush':
|
|
1046
1213
|
if not d['url']:
|
|
1047
1214
|
return 3, 'wxpush url 未配置'
|
|
@@ -1086,6 +1253,11 @@ def delivery_status():
|
|
|
1086
1253
|
d = delivery_cfg()
|
|
1087
1254
|
out = {'type': d['type'], 'ok': False, 'detail': '', 'pending': 0}
|
|
1088
1255
|
try:
|
|
1256
|
+
if d['type'] == 'ilink':
|
|
1257
|
+
w = wechat_status()
|
|
1258
|
+
out['ok'] = bool(w['logged'] and w['hasContext'])
|
|
1259
|
+
out['detail'] = w['detail']
|
|
1260
|
+
return out
|
|
1089
1261
|
if d['type'] == 'wxpush':
|
|
1090
1262
|
with urllib.request.urlopen(d['url'] + '/health', timeout=5) as r:
|
|
1091
1263
|
j = json.loads(r.read().decode() or '{}')
|
|
@@ -1220,7 +1392,7 @@ def do_report_with_image(cfg, quiet=False, force=None):
|
|
|
1220
1392
|
"""播报(文字)+ 可选附图;force=True/False 覆盖 config,None 跟随 config。"""
|
|
1221
1393
|
code, text, msg = do_report(cfg, quiet)
|
|
1222
1394
|
want = cfg.get('report', {}).get('image') if force is None else force
|
|
1223
|
-
if delivery_cfg(cfg)['type']
|
|
1395
|
+
if delivery_cfg(cfg)['type'] not in ('wxpush', 'ilink'):
|
|
1224
1396
|
want = False
|
|
1225
1397
|
if code == 0 and want:
|
|
1226
1398
|
try:
|
|
@@ -1246,6 +1418,9 @@ GRAPHICS = os.path.join(BASE, 'graphics')
|
|
|
1246
1418
|
|
|
1247
1419
|
|
|
1248
1420
|
def _wx_state():
|
|
1421
|
+
if delivery_cfg()['type'] == 'ilink':
|
|
1422
|
+
st = wechat_state()
|
|
1423
|
+
return st.get('bot_token'), st.get('context_token'), st.get('user_id'), ILINK_BASE
|
|
1249
1424
|
st = load_json(WXPUSH_STATE, {})
|
|
1250
1425
|
return st.get('bot_token'), st.get('context_token'), st.get('ilink_user_id'), \
|
|
1251
1426
|
(st.get('api_base') or 'https://ilinkai.weixin.qq.com/ilink')
|
|
@@ -1293,7 +1468,7 @@ def flush_pending_image():
|
|
|
1293
1468
|
p = load_json(PENDING_IMG, None)
|
|
1294
1469
|
if not p:
|
|
1295
1470
|
return False
|
|
1296
|
-
if delivery_cfg()['type']
|
|
1471
|
+
if delivery_cfg()['type'] not in ('wxpush', 'ilink'):
|
|
1297
1472
|
return False
|
|
1298
1473
|
if channel_stale():
|
|
1299
1474
|
return False
|
|
@@ -1739,6 +1914,25 @@ def cmd_daemon():
|
|
|
1739
1914
|
time.sleep(30)
|
|
1740
1915
|
|
|
1741
1916
|
|
|
1917
|
+
def cmd_wechat(argv):
|
|
1918
|
+
action = argv[0] if argv else 'status'
|
|
1919
|
+
if action == 'login':
|
|
1920
|
+
st = wechat_start_login()
|
|
1921
|
+
print('二维码链接:', st.get('qr_url'))
|
|
1922
|
+
print('二维码图片:', WECHAT_QR if os.path.exists(WECHAT_QR) else '(生成失败,可用链接自行转二维码)')
|
|
1923
|
+
print('扫码后运行: deck wechat status')
|
|
1924
|
+
return 0
|
|
1925
|
+
if action == 'logout':
|
|
1926
|
+
save_wechat_state({})
|
|
1927
|
+
print('已清除微信登录态')
|
|
1928
|
+
return 0
|
|
1929
|
+
w = wechat_status()
|
|
1930
|
+
print(f"微信: {w['detail']}")
|
|
1931
|
+
if w.get('qrUrl'):
|
|
1932
|
+
print('二维码:', w['qrUrl'])
|
|
1933
|
+
return 0
|
|
1934
|
+
|
|
1935
|
+
|
|
1742
1936
|
def cmd_demo(argv):
|
|
1743
1937
|
"""deck demo:生成一个演示环境(假项目 + 独立面板端口),不碰真实注册表。"""
|
|
1744
1938
|
import shutil
|
|
@@ -1837,6 +2031,8 @@ def main():
|
|
|
1837
2031
|
return cmd_approvals(rest)
|
|
1838
2032
|
if cmd == 'decisions':
|
|
1839
2033
|
return cmd_decisions(rest)
|
|
2034
|
+
if cmd == 'wechat':
|
|
2035
|
+
return cmd_wechat(rest)
|
|
1840
2036
|
if cmd == 'demo':
|
|
1841
2037
|
return cmd_demo(rest)
|
|
1842
2038
|
if cmd == 'panel':
|
package/backend/panel.html
CHANGED
|
@@ -184,6 +184,31 @@
|
|
|
184
184
|
<div class="hint" id="imgHint" style="margin-top:6px"></div>
|
|
185
185
|
</div>
|
|
186
186
|
|
|
187
|
+
<div class="controls" id="wxBox" style="padding:10px 12px">
|
|
188
|
+
<span class="pill" style="margin:0"><i class="dot" id="wxDot"></i><b id="wxTitle">通知渠道</b></span>
|
|
189
|
+
<select id="wxType" onchange="onWxTypeChange()" style="font-size:12px;padding:5px 8px">
|
|
190
|
+
<option value="ilink">内置微信(扫码)</option>
|
|
191
|
+
<option value="wxpush">wxpush 网关</option>
|
|
192
|
+
<option value="serverchan">ServerChan</option>
|
|
193
|
+
<option value="webhook">Webhook</option>
|
|
194
|
+
<option value="command">自定义命令</option>
|
|
195
|
+
</select>
|
|
196
|
+
<input id="wxField" placeholder="" style="flex:1;min-width:160px;display:none">
|
|
197
|
+
<button class="ghost" id="wxSaveBtn" onclick="saveWx()" style="display:none">保存</button>
|
|
198
|
+
<span class="hint" id="wxDetail"></span>
|
|
199
|
+
<button class="ghost" id="wxLoginBtn" onclick="wxLogin()" style="display:none">连接微信</button>
|
|
200
|
+
<button class="ghost" id="wxLogoutBtn" onclick="wxLogout()" style="display:none">断开</button>
|
|
201
|
+
</div>
|
|
202
|
+
<div id="wxQrWrap" style="display:none;margin:-6px 0 18px;display:none">
|
|
203
|
+
<div style="display:flex;gap:14px;align-items:center;background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:12px">
|
|
204
|
+
<img id="wxQr" style="width:150px;height:150px;border-radius:8px;background:#fff;padding:6px">
|
|
205
|
+
<div>
|
|
206
|
+
<div style="color:#e6edf3;font-size:13px;margin-bottom:6px">用微信扫码连接</div>
|
|
207
|
+
<div class="hint">扫码确认后,<b>还要在微信里给这个 bot 发任意一句话</b>(iLink 需要你发一次消息才能主动推送)。二维码约 2 分钟过期,会自动刷新。</div>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
187
212
|
<div class="controls" style="padding:9px 12px">
|
|
188
213
|
<input id="q" placeholder="搜索项目(名称 / id / 路径 / 说明)" style="flex:1;min-width:180px" oninput="render()">
|
|
189
214
|
<select id="fStatus" onchange="render()">
|
|
@@ -581,6 +606,67 @@ function sendToPlugin(payload){
|
|
|
581
606
|
};
|
|
582
607
|
}
|
|
583
608
|
|
|
609
|
+
let WX = null, wxTimer = null;
|
|
610
|
+
|
|
611
|
+
async function refreshWechat(){
|
|
612
|
+
try{
|
|
613
|
+
const r = await fetch('/api/wechat'); const j = await r.json();
|
|
614
|
+
WX = j.wechat || {};
|
|
615
|
+
const dot = $('wxDot'); const title = $('wxTitle');
|
|
616
|
+
if (dot) dot.className = 'dot ' + (WX.ok ? 'ok' : 'bad');
|
|
617
|
+
if (title) title.textContent = WX.mode === 'ilink' ? '微信' : (WX.mode || '通知');
|
|
618
|
+
$('wxDetail').textContent = WX.detail || '';
|
|
619
|
+
const curType = (S && S.config && S.config.delivery && S.config.delivery.type) || WX.mode || 'ilink';
|
|
620
|
+
if ($('wxType') && $('wxType').value !== curType) { $('wxType').value = curType; onWxTypeChange(); }
|
|
621
|
+
const needQr = WX.mode === 'ilink' && !WX.logged && WX.qrUrl;
|
|
622
|
+
$('wxQrWrap').style.display = needQr ? 'block' : 'none';
|
|
623
|
+
if (needQr) $('wxQr').src = '/api/wechat/qr.png?t=' + Date.now();
|
|
624
|
+
$('wxLoginBtn').style.display = (WX.mode === 'ilink' && !WX.logged) ? '' : 'none';
|
|
625
|
+
$('wxLogoutBtn').style.display = (WX.mode === 'ilink' && WX.logged) ? '' : 'none';
|
|
626
|
+
if (WX.mode === 'ilink' && !WX.ok) {
|
|
627
|
+
if (!wxTimer) wxTimer = setInterval(refreshWechat, 2500);
|
|
628
|
+
} else if (wxTimer) { clearInterval(wxTimer); wxTimer = null; }
|
|
629
|
+
if (S && S.wechat) { S.wechat.ok = WX.ok; render(); }
|
|
630
|
+
}catch(e){ /* 忽略 */ }
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
async function wxLogin(){ try{ await api('/api/wechat/login', {}); refreshWechat(); }catch(e){ toast('生成二维码失败:' + e.message, true); } }
|
|
634
|
+
async function wxLogout(){ try{ await api('/api/wechat/logout', {}); refreshWechat(); toast('已断开微信'); }catch(e){ toast('断开失败:' + e.message, true); } }
|
|
635
|
+
|
|
636
|
+
const WX_FIELD = {
|
|
637
|
+
serverchan: ['key', 'ServerChan SendKey(SCT...)'],
|
|
638
|
+
webhook: ['webhook', 'webhook URL(企业微信机器人等)'],
|
|
639
|
+
command: ['command', '命令(stdin 收文本,退出码 0=成功)'],
|
|
640
|
+
wxpush: ['url', 'wxpush 地址,默认 http://127.0.0.1:9098'],
|
|
641
|
+
ilink: null,
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
function onWxTypeChange(){
|
|
645
|
+
const type = $('wxType').value;
|
|
646
|
+
const f = WX_FIELD[type];
|
|
647
|
+
$('wxField').style.display = f ? '' : 'none';
|
|
648
|
+
$('wxSaveBtn').style.display = f ? '' : 'none';
|
|
649
|
+
if (f) {
|
|
650
|
+
const cur = (S && S.config && S.config.delivery) || {};
|
|
651
|
+
$('wxField').placeholder = f[1];
|
|
652
|
+
$('wxField').value = cur[f[0]] || '';
|
|
653
|
+
}
|
|
654
|
+
const isIlink = type === 'ilink';
|
|
655
|
+
$('wxLoginBtn').style.display = isIlink ? '' : 'none';
|
|
656
|
+
$('wxLogoutBtn').style.display = 'none';
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async function saveWx(){
|
|
660
|
+
const type = $('wxType').value;
|
|
661
|
+
const f = WX_FIELD[type];
|
|
662
|
+
if (!f) return;
|
|
663
|
+
const delivery = { type, [f[0]]: $('wxField').value.trim() };
|
|
664
|
+
try{
|
|
665
|
+
const r = await api('/api/config', { delivery });
|
|
666
|
+
S = r.state; render(); refreshWechat(); toast('通知渠道已更新');
|
|
667
|
+
}catch(e){ toast('保存失败:' + e.message, true); }
|
|
668
|
+
}
|
|
669
|
+
|
|
584
670
|
function setFabPos(pos){
|
|
585
671
|
if (!inPluginFrame()) { toast('请在 DSH 内嵌面板里使用(浏览器打开时无效)', true); return; }
|
|
586
672
|
window.parent.postMessage({ type: 'deck:set-fab-pos', pos }, '*');
|
package/backend/panel.py
CHANGED
|
@@ -47,6 +47,26 @@ P = load_deck()
|
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
# ── 数据汇总 ────────────────────────────────────────────────
|
|
50
|
+
def wechat_view():
|
|
51
|
+
d = P.delivery_cfg()
|
|
52
|
+
if d['type'] == 'ilink':
|
|
53
|
+
P.wechat_check_login() if P.wechat_state().get('qr_ticket') else None
|
|
54
|
+
return {'mode': 'ilink', **P.wechat_status()}
|
|
55
|
+
st = P.delivery_status()
|
|
56
|
+
return {'mode': d['type'], **st}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def wechat_poller():
|
|
60
|
+
while True:
|
|
61
|
+
try:
|
|
62
|
+
if P.delivery_cfg()['type'] == 'ilink' and P.wechat_state().get('bot_token'):
|
|
63
|
+
P.wechat_poll_once(50)
|
|
64
|
+
else:
|
|
65
|
+
time.sleep(4)
|
|
66
|
+
except Exception:
|
|
67
|
+
time.sleep(5)
|
|
68
|
+
|
|
69
|
+
|
|
50
70
|
def wechat_state():
|
|
51
71
|
st = P.delivery_status()
|
|
52
72
|
return {'ok': bool(st.get('ok')), 'raw': st.get('detail', ''),
|
|
@@ -102,6 +122,7 @@ def snapshot():
|
|
|
102
122
|
'format': rep.get('format', 'markdown'),
|
|
103
123
|
'image': bool(rep.get('image', False)),
|
|
104
124
|
'web': cfg.get('web', {}).get('baseUrl', 'http://127.0.0.1:43120'),
|
|
125
|
+
'delivery': P.delivery_cfg(cfg),
|
|
105
126
|
'scan': cfg.get('scan', {})},
|
|
106
127
|
'daemon': {'running': bool(P.daemon_status()), 'pid': P.daemon_status() or 0,
|
|
107
128
|
'lastRun': last, 'nextRun': (last + interval * 3600) if last else 0},
|
|
@@ -566,6 +587,11 @@ def set_config(payload):
|
|
|
566
587
|
rep['maxChars'] = int(payload['maxChars'])
|
|
567
588
|
if 'image' in payload:
|
|
568
589
|
rep['image'] = bool(payload['image'])
|
|
590
|
+
if 'delivery' in payload and isinstance(payload['delivery'], dict):
|
|
591
|
+
dl = rep.setdefault('delivery', {})
|
|
592
|
+
for k in ('type', 'url', 'key', 'webhook', 'command'):
|
|
593
|
+
if k in payload['delivery']:
|
|
594
|
+
dl[k] = payload['delivery'][k]
|
|
569
595
|
if 'scan' in payload and isinstance(payload['scan'], dict):
|
|
570
596
|
sc = cfg.setdefault('scan', {})
|
|
571
597
|
for k in ('enabled', 'autoAdd'):
|
|
@@ -785,6 +811,33 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|
|
785
811
|
return self._json({'ok': False, 'error': 'forbidden'}, 403)
|
|
786
812
|
if path in ('/', '/index.html'):
|
|
787
813
|
return self._html(HTML)
|
|
814
|
+
if path == '/api/wechat':
|
|
815
|
+
return self._json({'ok': True, 'wechat': wechat_view()})
|
|
816
|
+
if path == '/api/wechat/qr.png':
|
|
817
|
+
fp = P.WECHAT_QR
|
|
818
|
+
if not os.path.isfile(fp):
|
|
819
|
+
return self._json({'ok': False, 'error': '二维码尚未生成'}, 404)
|
|
820
|
+
body = open(fp, 'rb').read()
|
|
821
|
+
self.send_response(200)
|
|
822
|
+
self.send_header('Content-Type', 'image/png')
|
|
823
|
+
self.send_header('Cache-Control', 'no-store')
|
|
824
|
+
self.send_header('Content-Length', str(len(body)))
|
|
825
|
+
self.end_headers()
|
|
826
|
+
self.wfile.write(body)
|
|
827
|
+
return
|
|
828
|
+
if path == '/icons':
|
|
829
|
+
fp = os.path.join(BASE, 'icons.html')
|
|
830
|
+
if not os.path.isfile(fp):
|
|
831
|
+
return self._json({'ok': False, 'error': 'icons.html 不存在'}, 404)
|
|
832
|
+
body = open(fp, 'rb').read()
|
|
833
|
+
self.send_response(200)
|
|
834
|
+
self.send_header('Content-Type', 'text/html; charset=utf-8')
|
|
835
|
+
self.send_header('Content-Length', str(len(body)))
|
|
836
|
+
self.end_headers()
|
|
837
|
+
self.wfile.write(body)
|
|
838
|
+
return
|
|
839
|
+
if path in ('/api/node/scanner.py', '/api/node/scanner.js', '/api/node/scanner.ps1'):
|
|
840
|
+
pass
|
|
788
841
|
if path == '/api/state':
|
|
789
842
|
try:
|
|
790
843
|
return self._json(snapshot())
|
|
@@ -891,6 +944,12 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|
|
891
944
|
return self._json({'ok': False, 'error': '请求体不是合法 JSON'}, 400)
|
|
892
945
|
|
|
893
946
|
try:
|
|
947
|
+
if path == '/api/wechat/login':
|
|
948
|
+
P.wechat_start_login()
|
|
949
|
+
return self._json({'ok': True, 'wechat': wechat_view()})
|
|
950
|
+
if path == '/api/wechat/logout':
|
|
951
|
+
P.save_wechat_state({})
|
|
952
|
+
return self._json({'ok': True, 'wechat': wechat_view()})
|
|
894
953
|
if path == '/api/autonomy':
|
|
895
954
|
return self._json({'ok': True, 'state': set_autonomy(payload.get('project'), payload.get('mode'), payload.get('maxPerDay'))})
|
|
896
955
|
if path == '/api/approval':
|
|
@@ -939,6 +998,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|
|
939
998
|
def main():
|
|
940
999
|
import threading
|
|
941
1000
|
threading.Thread(target=lambda: session_index(), daemon=True).start() # 预热会话索引
|
|
1001
|
+
threading.Thread(target=wechat_poller, daemon=True).start() # 微信内置通道轮询
|
|
942
1002
|
httpd = http.server.ThreadingHTTPServer((HOST, PORT), Handler)
|
|
943
1003
|
P.log(f'[panel] listening http://{HOST}:{PORT}')
|
|
944
1004
|
try:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-project-deck",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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",
|