@qzhuli/qzhuli-cli 0.3.0-rc.1 → 0.3.0
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/dist/cmd.js +53 -6
- package/package.json +1 -1
- package/skills/qzhuli-cli/SKILL.md +7 -5
package/dist/cmd.js
CHANGED
|
@@ -14183,14 +14183,41 @@ var SqliteConversationRepository = class {
|
|
|
14183
14183
|
"INSERT INTO conversation_profiles (conversation_id, data, user_ids, cached_at) VALUES (?, ?, ?, ?) ON CONFLICT(conversation_id) DO UPDATE SET data=?, user_ids=?, cached_at=?"
|
|
14184
14184
|
).run(conversationId, data, user_ids, cached_at, data, user_ids, cached_at);
|
|
14185
14185
|
}
|
|
14186
|
+
// MARK: Conversation Index (for incremental sync)
|
|
14187
|
+
upsertIndex(conv) {
|
|
14188
|
+
const members = JSON.stringify(conv.cids);
|
|
14189
|
+
const nicks = conv.nicks ? JSON.stringify(conv.nicks) : null;
|
|
14190
|
+
const im_version = conv.version;
|
|
14191
|
+
const cached_at = Date.now();
|
|
14192
|
+
this.db.prepare(
|
|
14193
|
+
"INSERT INTO conversations_index (conversation_id, im_version, members, nicks, cached_at) VALUES (?, ?, ?, ?, ?) ON CONFLICT(conversation_id) DO UPDATE SET im_version=?, members=?, nicks=?, cached_at=?"
|
|
14194
|
+
).run(conv.id, im_version, members, nicks, cached_at, im_version, members, nicks, cached_at);
|
|
14195
|
+
}
|
|
14196
|
+
getIndexById(conversationId) {
|
|
14197
|
+
return this.db.prepare(
|
|
14198
|
+
"SELECT conversation_id, im_version, members, nicks, cached_at FROM conversations_index WHERE conversation_id = ?"
|
|
14199
|
+
).get(conversationId);
|
|
14200
|
+
}
|
|
14201
|
+
getAllIds() {
|
|
14202
|
+
const rows = this.db.prepare("SELECT conversation_id FROM conversations_index").all();
|
|
14203
|
+
return rows.map((r) => r.conversation_id);
|
|
14204
|
+
}
|
|
14205
|
+
deleteStale(conversationIds) {
|
|
14206
|
+
if (conversationIds.length === 0) return;
|
|
14207
|
+
const placeholders = conversationIds.map(() => "?").join(",");
|
|
14208
|
+
this.db.prepare(`DELETE FROM conversation_profiles WHERE conversation_id NOT IN (${placeholders})`).run(...conversationIds);
|
|
14209
|
+
this.db.prepare(`DELETE FROM conversations_index WHERE conversation_id NOT IN (${placeholders})`).run(...conversationIds);
|
|
14210
|
+
}
|
|
14186
14211
|
invalidate(conversationId) {
|
|
14187
14212
|
this.db.prepare("DELETE FROM conversation_profiles WHERE conversation_id = ?").run(conversationId);
|
|
14213
|
+
this.db.prepare("DELETE FROM conversations_index WHERE conversation_id = ?").run(conversationId);
|
|
14188
14214
|
}
|
|
14189
14215
|
sync() {
|
|
14190
14216
|
return Promise.resolve();
|
|
14191
14217
|
}
|
|
14192
14218
|
clear() {
|
|
14193
14219
|
this.db.exec("DELETE FROM conversation_profiles");
|
|
14220
|
+
this.db.exec("DELETE FROM conversations_index");
|
|
14194
14221
|
this.db.exec("DELETE FROM cache_metadata WHERE key = 'last_sync_conversations'");
|
|
14195
14222
|
}
|
|
14196
14223
|
getStatus() {
|
|
@@ -14203,6 +14230,7 @@ var SqliteConversationRepository = class {
|
|
|
14203
14230
|
}
|
|
14204
14231
|
};
|
|
14205
14232
|
var CachedConversationRepository = class {
|
|
14233
|
+
// 30 minutes
|
|
14206
14234
|
constructor(local, remote) {
|
|
14207
14235
|
this.local = local;
|
|
14208
14236
|
this.remote = remote;
|
|
@@ -14210,6 +14238,7 @@ var CachedConversationRepository = class {
|
|
|
14210
14238
|
local;
|
|
14211
14239
|
remote;
|
|
14212
14240
|
syncInProgress = false;
|
|
14241
|
+
TTL_MS = 30 * 60 * 1e3;
|
|
14213
14242
|
async queryAll(options3) {
|
|
14214
14243
|
return this.remote.queryAll(options3);
|
|
14215
14244
|
}
|
|
@@ -14232,7 +14261,7 @@ var CachedConversationRepository = class {
|
|
|
14232
14261
|
async ensureSynced() {
|
|
14233
14262
|
if (this.syncInProgress) return;
|
|
14234
14263
|
const status = await this.local.getStatus();
|
|
14235
|
-
const isFresh = status.lastSyncAt !== null && Date.now() - status.lastSyncAt <
|
|
14264
|
+
const isFresh = status.lastSyncAt !== null && Date.now() - status.lastSyncAt < this.TTL_MS;
|
|
14236
14265
|
if (isFresh) return;
|
|
14237
14266
|
this.syncInProgress = true;
|
|
14238
14267
|
try {
|
|
@@ -14244,12 +14273,22 @@ var CachedConversationRepository = class {
|
|
|
14244
14273
|
async sync() {
|
|
14245
14274
|
const convResult = await this.remote.queryAll({ limit: 0, offset: 0 });
|
|
14246
14275
|
if (!convResult.ok || !convResult.data) return;
|
|
14247
|
-
|
|
14248
|
-
|
|
14249
|
-
|
|
14250
|
-
|
|
14276
|
+
const remoteConversations = convResult.data;
|
|
14277
|
+
const remoteIds = new Set(remoteConversations.map((c) => c.id));
|
|
14278
|
+
const cachedIds = new Set(this.local.getAllIds());
|
|
14279
|
+
const newIds = remoteConversations.filter((c) => !cachedIds.has(c.id)).map((c) => c.id);
|
|
14280
|
+
for (const conv of remoteConversations) {
|
|
14281
|
+
if (newIds.includes(conv.id)) {
|
|
14282
|
+
const profileResult = await this.remote.getProfile(conv.id);
|
|
14283
|
+
if (profileResult.ok) {
|
|
14284
|
+
this.local.upsertProfile(conv.id, profileResult.data);
|
|
14285
|
+
}
|
|
14251
14286
|
}
|
|
14252
14287
|
}
|
|
14288
|
+
for (const conv of remoteConversations) {
|
|
14289
|
+
this.local.upsertIndex(conv);
|
|
14290
|
+
}
|
|
14291
|
+
this.local.deleteStale([...remoteIds]);
|
|
14253
14292
|
this.local.db.prepare("INSERT OR REPLACE INTO cache_metadata (key, value) VALUES (?, ?)").run("last_sync_conversations", String(Date.now()));
|
|
14254
14293
|
}
|
|
14255
14294
|
invalidate(conversationId) {
|
|
@@ -14495,6 +14534,14 @@ CREATE TABLE IF NOT EXISTS messages_cache (
|
|
|
14495
14534
|
);
|
|
14496
14535
|
CREATE INDEX IF NOT EXISTS idx_msg_conv ON messages_cache(conversation_id);
|
|
14497
14536
|
|
|
14537
|
+
CREATE TABLE IF NOT EXISTS conversations_index (
|
|
14538
|
+
conversation_id TEXT PRIMARY KEY,
|
|
14539
|
+
im_version INTEGER NOT NULL,
|
|
14540
|
+
members TEXT NOT NULL,
|
|
14541
|
+
nicks TEXT,
|
|
14542
|
+
cached_at INTEGER NOT NULL
|
|
14543
|
+
);
|
|
14544
|
+
|
|
14498
14545
|
CREATE TABLE IF NOT EXISTS cache_metadata (
|
|
14499
14546
|
key TEXT PRIMARY KEY,
|
|
14500
14547
|
value TEXT
|
|
@@ -14760,7 +14807,7 @@ async function main() {
|
|
|
14760
14807
|
${t("cli.banner")}` : t("cli.banner");
|
|
14761
14808
|
program.addHelpText("beforeAll", `${banner}
|
|
14762
14809
|
`);
|
|
14763
|
-
program.name("qz").version(`v${"0.3.0
|
|
14810
|
+
program.name("qz").version(`v${"0.3.0"}`, "-v, --version", t("options.version")).helpOption("-h, --help", t("options.help")).option("-q, --jq <expr>", t("options.jq")).option("--dry-run", t("options.dryRun"));
|
|
14764
14811
|
program.usage("<command> [subcommand] [options]");
|
|
14765
14812
|
program.hook("preAction", () => {
|
|
14766
14813
|
const opts = program.opts();
|
package/package.json
CHANGED
|
@@ -60,13 +60,15 @@ actions, auth login/logout, and preference writes.
|
|
|
60
60
|
All read operations use a **Repository Pattern** with SQLite-backed caching:
|
|
61
61
|
|
|
62
62
|
- **Local-first**: reads hit a local SQLite database (`~/.qzhuli-cli/cache.db`) before the remote API
|
|
63
|
-
- **TTL-based expiration**: contacts/relations
|
|
64
|
-
- **
|
|
65
|
-
- **
|
|
63
|
+
- **TTL-based expiration**: conversations expire after 30 minutes, contacts/relations after 5 minutes, user profiles after 1 hour
|
|
64
|
+
- **Incremental sync**: sync compares the IM conversation list against a local index, only fetching profiles for *new* conversations
|
|
65
|
+
- **Auto-sync on miss**: cache miss triggers an incremental sync (not full refetch)
|
|
66
|
+
- **Manual sync**: use `qz cache sync` to sync only new data
|
|
66
67
|
|
|
67
68
|
Cache database tables:
|
|
68
69
|
|
|
69
|
-
- `
|
|
70
|
+
- `conversations_index` — lightweight IM metadata (id, version, members, nicks) for incremental sync
|
|
71
|
+
- `conversation_profiles` — full conversation profile data with user_ids index
|
|
70
72
|
- `contacts_cache` — full contacts list per owner UID
|
|
71
73
|
- `user_profiles` — individual user profiles
|
|
72
74
|
- `relations_cache` — friend relations (remark, type)
|
|
@@ -214,5 +216,5 @@ qz conversation search 10000 # now instant from cache
|
|
|
214
216
|
| Too much JSON | Use `--jq ".data"` or another simple dot path. |
|
|
215
217
|
| Need a no-op preview | Use `--dry-run`. |
|
|
216
218
|
| Message send cid error | Re-check `auth status` and choose `target-cid` from `conversation list`. |
|
|
217
|
-
| Slow queries | Run `qz cache sync` first, then retry — results come from local SQLite.
|
|
219
|
+
| Slow queries | Run `qz cache sync` first (incremental, fast), then retry — results come from local SQLite. |
|
|
218
220
|
| Cache corrupted | Run `qz cache clear` to reset, then retry (falls back to API). |
|