oh-my-dws 0.0.2 → 0.0.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 +17 -5
- package/bin/oh-my-dws.js +2 -2
- package/docs//344/275/277/347/224/250/350/257/264/346/230/216.md +104 -5
- package/package.json +1 -1
- package/src/actions.js +590 -63
- package/src/ai.js +300 -0
- package/src/archive.js +17 -8
- package/src/auth.js +33 -1
- package/src/avatars.js +143 -0
- package/src/contacts.js +563 -0
- package/src/layout.js +63 -0
- package/src/listen.js +289 -0
- package/src/media.js +12 -1
- package/src/rev.js +23 -0
- package/src/server.js +385 -7
- package/src/settings.js +51 -0
- package/src/sync.js +213 -0
- package/web/index.html +2136 -74
package/src/sync.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 自动同步:定时查「钉钉里哪些会话有未读」,只对已归档的会话做增量拉取。
|
|
5
|
+
*
|
|
6
|
+
* 为什么这么绕:`+recent-conversations` 一页要十几秒,而 `+unread-chats` 只要 1~2 秒,
|
|
7
|
+
* 所以用未读列表当「有变化」的信号,再按 openConversationId 映射到本地归档去 pull。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const path = require('node:path');
|
|
12
|
+
|
|
13
|
+
const P = require('./paths');
|
|
14
|
+
const { runDws } = require('./dws');
|
|
15
|
+
const { ensureDir, fmtSecond } = require('./util');
|
|
16
|
+
const { shortNames, archiveKind, conversationId, groupTitle } = require('./archive');
|
|
17
|
+
const A = require('./actions');
|
|
18
|
+
|
|
19
|
+
const SETTINGS_FILE = path.join(P.DATA_DIR, 'settings.json');
|
|
20
|
+
|
|
21
|
+
const DEFAULTS = { enabled: true, intervalSec: 60, minIntervalSec: 20, maxIntervalSec: 1800 };
|
|
22
|
+
|
|
23
|
+
const state = {
|
|
24
|
+
enabled: DEFAULTS.enabled,
|
|
25
|
+
intervalSec: DEFAULTS.intervalSec,
|
|
26
|
+
timer: null,
|
|
27
|
+
running: false,
|
|
28
|
+
lastRunAt: '',
|
|
29
|
+
lastAdded: 0,
|
|
30
|
+
lastTouched: [],
|
|
31
|
+
lastError: '',
|
|
32
|
+
nextRunAt: '',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------- 设置持久化
|
|
36
|
+
|
|
37
|
+
function readSettings() {
|
|
38
|
+
try {
|
|
39
|
+
const j = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
|
|
40
|
+
const s = j && j.autoSync ? j.autoSync : {};
|
|
41
|
+
return {
|
|
42
|
+
enabled: s.enabled === undefined ? DEFAULTS.enabled : Boolean(s.enabled),
|
|
43
|
+
intervalSec: clampInterval(s.intervalSec),
|
|
44
|
+
};
|
|
45
|
+
} catch {
|
|
46
|
+
return { enabled: DEFAULTS.enabled, intervalSec: DEFAULTS.intervalSec };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function clampInterval(v) {
|
|
51
|
+
const n = Number(v);
|
|
52
|
+
if (!Number.isFinite(n)) return DEFAULTS.intervalSec;
|
|
53
|
+
return Math.min(DEFAULTS.maxIntervalSec, Math.max(DEFAULTS.minIntervalSec, Math.round(n)));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function writeSettings() {
|
|
57
|
+
ensureDir(P.DATA_DIR);
|
|
58
|
+
let all = {};
|
|
59
|
+
try {
|
|
60
|
+
all = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8')) || {};
|
|
61
|
+
} catch {}
|
|
62
|
+
all.autoSync = { enabled: state.enabled, intervalSec: state.intervalSec };
|
|
63
|
+
fs.writeFileSync(SETTINGS_FILE, `${JSON.stringify(all, null, 2)}\n`, 'utf8');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------- 同步逻辑
|
|
67
|
+
|
|
68
|
+
/** 已归档会话里,cid → short 映射 */
|
|
69
|
+
function archiveByCid() {
|
|
70
|
+
const map = new Map();
|
|
71
|
+
for (const short of shortNames()) {
|
|
72
|
+
if (archiveKind(short) === 'mentions') continue;
|
|
73
|
+
const cid = conversationId(short);
|
|
74
|
+
if (cid) map.set(cid, short);
|
|
75
|
+
}
|
|
76
|
+
return map;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function unreadConversations() {
|
|
80
|
+
const data = runDws(['chat', '+unread-chats', '--count', '100', '--format', 'json'], { timeout: 30000 });
|
|
81
|
+
const list = (data && data.conversations) || (data && data.data && data.data.conversations) || [];
|
|
82
|
+
return list
|
|
83
|
+
.filter((c) => c && c.conversationId)
|
|
84
|
+
.map((c) => ({ cid: String(c.conversationId), name: String(c.name || '') }));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 跑一轮:有未读且已归档的会话,逐个增量拉取 */
|
|
88
|
+
async function runOnce(reason = 'timer') {
|
|
89
|
+
if (state.running) return { skipped: 'running' };
|
|
90
|
+
state.running = true;
|
|
91
|
+
const touched = [];
|
|
92
|
+
let added = 0;
|
|
93
|
+
let error = '';
|
|
94
|
+
try {
|
|
95
|
+
let unread = [];
|
|
96
|
+
try {
|
|
97
|
+
unread = unreadConversations();
|
|
98
|
+
} catch (e) {
|
|
99
|
+
error = e.message || String(e);
|
|
100
|
+
throw e;
|
|
101
|
+
}
|
|
102
|
+
const byCid = archiveByCid();
|
|
103
|
+
const targets = [];
|
|
104
|
+
for (const u of unread) {
|
|
105
|
+
const short = byCid.get(u.cid);
|
|
106
|
+
if (short && !targets.includes(short)) targets.push(short);
|
|
107
|
+
}
|
|
108
|
+
if (targets.length) {
|
|
109
|
+
const out = await A.withLock(async () => {
|
|
110
|
+
const detail = [];
|
|
111
|
+
for (const short of targets) {
|
|
112
|
+
try {
|
|
113
|
+
const r = A.pullGroup(short);
|
|
114
|
+
detail.push({ short, added: r.added || 0 });
|
|
115
|
+
added += r.added || 0;
|
|
116
|
+
if (r.added) touched.push(short);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
detail.push({ short, error: e.message });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return detail;
|
|
122
|
+
});
|
|
123
|
+
state.lastTouched = out.filter((x) => x.added || x.error);
|
|
124
|
+
} else {
|
|
125
|
+
state.lastTouched = [];
|
|
126
|
+
}
|
|
127
|
+
state.lastError = '';
|
|
128
|
+
} catch (e) {
|
|
129
|
+
state.lastError = error || e.message || String(e);
|
|
130
|
+
state.lastTouched = [];
|
|
131
|
+
} finally {
|
|
132
|
+
state.running = false;
|
|
133
|
+
state.lastRunAt = fmtSecond(new Date());
|
|
134
|
+
state.lastAdded = added;
|
|
135
|
+
state.nextRunAt = state.enabled
|
|
136
|
+
? fmtSecond(new Date(Date.now() + state.intervalSec * 1000))
|
|
137
|
+
: '';
|
|
138
|
+
if (reason !== 'manual') saveSettingsQuiet();
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
ranAt: state.lastRunAt,
|
|
142
|
+
added,
|
|
143
|
+
touched: state.lastTouched,
|
|
144
|
+
error: state.lastError,
|
|
145
|
+
unreadCount: state.lastTouched.length,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function saveSettingsQuiet() {
|
|
150
|
+
try {
|
|
151
|
+
writeSettings();
|
|
152
|
+
} catch {}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------- 定时器
|
|
156
|
+
|
|
157
|
+
function schedule() {
|
|
158
|
+
if (state.timer) {
|
|
159
|
+
clearInterval(state.timer);
|
|
160
|
+
state.timer = null;
|
|
161
|
+
}
|
|
162
|
+
if (!state.enabled) {
|
|
163
|
+
state.nextRunAt = '';
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
state.timer = setInterval(() => {
|
|
167
|
+
runOnce('timer').catch(() => {});
|
|
168
|
+
}, state.intervalSec * 1000);
|
|
169
|
+
if (state.timer.unref) state.timer.unref();
|
|
170
|
+
state.nextRunAt = fmtSecond(new Date(Date.now() + state.intervalSec * 1000));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function status() {
|
|
174
|
+
return {
|
|
175
|
+
enabled: state.enabled,
|
|
176
|
+
intervalSec: state.intervalSec,
|
|
177
|
+
minIntervalSec: DEFAULTS.minIntervalSec,
|
|
178
|
+
maxIntervalSec: DEFAULTS.maxIntervalSec,
|
|
179
|
+
running: state.running,
|
|
180
|
+
lastRunAt: state.lastRunAt,
|
|
181
|
+
nextRunAt: state.nextRunAt,
|
|
182
|
+
lastAdded: state.lastAdded,
|
|
183
|
+
lastTouched: state.lastTouched,
|
|
184
|
+
lastError: state.lastError,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function configure({ enabled, intervalSec } = {}) {
|
|
189
|
+
if (enabled !== undefined) state.enabled = Boolean(enabled);
|
|
190
|
+
if (intervalSec !== undefined) state.intervalSec = clampInterval(intervalSec);
|
|
191
|
+
writeSettings();
|
|
192
|
+
schedule();
|
|
193
|
+
return status();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** 服务启动时调用:读配置 + 起定时器(默认开,60s) */
|
|
197
|
+
function start() {
|
|
198
|
+
const s = readSettings();
|
|
199
|
+
state.enabled = s.enabled;
|
|
200
|
+
state.intervalSec = s.intervalSec;
|
|
201
|
+
schedule();
|
|
202
|
+
// 启动后先跑一轮,把重启期间漏掉的补上
|
|
203
|
+
if (state.enabled) setTimeout(() => runOnce('boot').catch(() => {}), 5000).unref?.();
|
|
204
|
+
return status();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function stop() {
|
|
208
|
+
if (state.timer) clearInterval(state.timer);
|
|
209
|
+
state.timer = null;
|
|
210
|
+
state.nextRunAt = '';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = { SETTINGS_FILE, start, stop, configure, status, runOnce, archiveByCid };
|