更新 shenyan-app: 双桥通信 + 定制横幅推送 + APNs Key
- 双桥:Mac桥(Claude Code异步) + IDC桥(DeepSeek同步) - 推送:自制定制Animated横幅 + 震动,替代Alert弹窗 - APNs:Key已创建(3VJ5X6V9Q8),桥端apns.js已写好 - 图标:已替换为定制图标 - BlurView/notifee:iOS 26兼容问题已回退 - 记忆库更新 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
64
shenyan-app/bridge-local/apns.js
Normal file
64
shenyan-app/bridge-local/apns.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const http2 = require('http2');
|
||||
|
||||
const TEAM_ID = 'ZZMLW32PHH';
|
||||
const KEY_ID = '3VJ5X6V9Q8';
|
||||
const BUNDLE_ID = 'com.shenyan.room';
|
||||
const KEY_PATH = path.join(__dirname, 'AuthKey_3VJ5X6V9Q8.p8');
|
||||
|
||||
// cache JWT for ~50min (max 1hr)
|
||||
let cachedToken = null;
|
||||
let tokenExpiry = 0;
|
||||
|
||||
function getJWT() {
|
||||
if (cachedToken && Date.now() < tokenExpiry) return cachedToken;
|
||||
const key = fs.readFileSync(KEY_PATH, 'utf-8');
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const payload = { iss: TEAM_ID, iat: now };
|
||||
const headers = { alg: 'ES256', kid: KEY_ID };
|
||||
cachedToken = jwt.sign(payload, key, { algorithm: 'ES256', header: headers, expiresIn: '50m' });
|
||||
tokenExpiry = now + 3000; // 50 min
|
||||
return cachedToken;
|
||||
}
|
||||
|
||||
function sendAPNs(deviceToken, title, body) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const payload = JSON.stringify({
|
||||
aps: {
|
||||
alert: { title: title || '沈晏', body: body || '' },
|
||||
sound: 'default',
|
||||
badge: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const client = http2.connect('https://api.sandbox.push.apple.com:443');
|
||||
client.on('error', err => reject(err));
|
||||
|
||||
const req = client.request({
|
||||
':method': 'POST',
|
||||
':path': `/3/device/${deviceToken}`,
|
||||
'authorization': `bearer ${getJWT()}`,
|
||||
'apns-topic': BUNDLE_ID,
|
||||
'apns-push-type': 'alert',
|
||||
'apns-expiration': '0',
|
||||
});
|
||||
|
||||
req.on('response', (headers) => {
|
||||
const status = headers[':status'];
|
||||
let data = '';
|
||||
req.on('data', c => data += c);
|
||||
req.on('end', () => {
|
||||
client.close();
|
||||
if (status === 200) resolve({ ok: true, status });
|
||||
else reject(new Error(`APNs ${status}: ${data}`));
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', err => { client.close(); reject(err); });
|
||||
req.end(payload);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { sendAPNs };
|
||||
@@ -1,12 +1,14 @@
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { sendAPNs } = require('./apns');
|
||||
|
||||
const PORT = 3003;
|
||||
const DATA_DIR = path.join(require('os').homedir(), '.shenyan-bridge');
|
||||
const INBOX = path.join(DATA_DIR, 'inbox.jsonl');
|
||||
const OUTBOX = path.join(DATA_DIR, 'outbox.jsonl');
|
||||
const PUSHBOX = path.join(DATA_DIR, 'pushbox.jsonl');
|
||||
const DEVICE_TOKEN_FILE = path.join(DATA_DIR, 'device-token.txt');
|
||||
|
||||
// ensure data dir and files
|
||||
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
@@ -104,13 +106,44 @@ const server = http.createServer((req, res) => {
|
||||
if (req.method === 'POST' && req.url === '/push') {
|
||||
let body = '';
|
||||
req.on('data', c => body += c);
|
||||
req.on('end', () => {
|
||||
req.on('end', async () => {
|
||||
try {
|
||||
const { title, body: msgBody } = JSON.parse(body);
|
||||
if (!title && !msgBody) return json(res, { error: 'empty' }, 400);
|
||||
const entry = { id: getNextId(PUSHBOX), title: title || '', body: msgBody || '', time: Date.now() };
|
||||
appendJSONL(PUSHBOX, entry);
|
||||
json(res, { ok: true, id: entry.id });
|
||||
|
||||
// try APNs if device token is registered
|
||||
let apnsOk = false;
|
||||
try {
|
||||
if (fs.existsSync(DEVICE_TOKEN_FILE)) {
|
||||
const token = fs.readFileSync(DEVICE_TOKEN_FILE, 'utf-8').trim();
|
||||
if (token) {
|
||||
await sendAPNs(token, title, msgBody);
|
||||
apnsOk = true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('APNs failed:', e.message);
|
||||
}
|
||||
|
||||
json(res, { ok: true, id: entry.id, apns: apnsOk });
|
||||
} catch (e) { json(res, { error: e.message }, 400); }
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- app registers its device token ----
|
||||
if (req.method === 'POST' && req.url === '/register-device') {
|
||||
let body = '';
|
||||
req.on('data', c => body += c);
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const { token } = JSON.parse(body);
|
||||
if (!token) return json(res, { error: 'no token' }, 400);
|
||||
fs.writeFileSync(DEVICE_TOKEN_FILE, token.trim(), 'utf-8');
|
||||
console.log('device registered:', token.slice(0, 16) + '...');
|
||||
json(res, { ok: true });
|
||||
} catch (e) { json(res, { error: e.message }, 400); }
|
||||
});
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user