@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 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 < 5 * 60 * 1e3;
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
- for (const conv of convResult.data) {
14248
- const profileResult = await this.remote.getProfile(conv.id);
14249
- if (profileResult.ok) {
14250
- this.local.upsertProfile(conv.id, profileResult.data);
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-rc.1"}`, "-v, --version", t("options.version")).helpOption("-h, --help", t("options.help")).option("-q, --jq <expr>", t("options.jq")).option("--dry-run", t("options.dryRun"));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qzhuli/qzhuli-cli",
3
- "version": "0.3.0-rc.1",
3
+ "version": "0.3.0",
4
4
  "description": "CLI tool for Q助理 (QZhuli)",
5
5
  "main": "dist/cmd.js",
6
6
  "bin": {
@@ -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 expire after 5 minutes, user profiles after 1 hour
64
- - **Auto-sync on miss**: cache miss triggers a remote fetch + automatic cache write-back
65
- - **Manual sync**: use `qz cache sync` to pre-fetch all data
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
- - `conversation_profiles` — conversation metadata with user_ids index
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). |