mslxdff 0.1.65 → 0.1.66

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.
Files changed (42) hide show
  1. package/bin/mslxdff.js +3 -3159
  2. package/package.json +1 -1
  3. package/src/cli/bootstrap.js +408 -0
  4. package/src/cli/commands/daemon.js +151 -0
  5. package/src/cli/commands/group.js +272 -0
  6. package/src/cli/commands/model.js +405 -0
  7. package/src/cli/commands/provider/add.js +106 -0
  8. package/src/cli/commands/provider/allowlist.js +99 -0
  9. package/src/cli/commands/provider/config.js +82 -0
  10. package/src/cli/commands/provider/index.js +193 -0
  11. package/src/cli/commands/provider/keys.js +135 -0
  12. package/src/cli/commands/provider/models.js +99 -0
  13. package/src/cli/commands/provider.js +1 -0
  14. package/src/cli/commands/sync.js +143 -0
  15. package/src/cli/commands/system.js +236 -0
  16. package/src/cli/commands/workbuddy.js +91 -0
  17. package/src/cli/format.js +119 -0
  18. package/src/cli/group-helpers.js +69 -0
  19. package/src/cli/help.js +66 -0
  20. package/src/cli/index.js +59 -0
  21. package/src/cli/interactive.js +84 -0
  22. package/src/cli/policy.js +119 -0
  23. package/src/cli/status.js +332 -0
  24. package/src/cli/util.js +24 -0
  25. package/src/providers/base.js +159 -0
  26. package/src/providers/generic.js +26 -166
  27. package/src/providers/openrouter.js +32 -162
  28. package/src/providers/workbuddy.js +7 -32
  29. package/src/routes/chat/gateway.js +301 -0
  30. package/src/routes/chat/index.js +1 -384
  31. package/src/state/facade.js +57 -0
  32. package/src/state/memory.js +180 -0
  33. package/src/state/persist.js +42 -0
  34. package/src/state/schemas/allowlist.js +87 -0
  35. package/src/state/schemas/group.js +31 -0
  36. package/src/state/schemas/model.js +53 -0
  37. package/src/state/schemas/peer.js +31 -0
  38. package/src/state/schemas/port.js +10 -0
  39. package/src/state/schemas/provider.js +254 -0
  40. package/src/state/schemas/token.js +43 -0
  41. package/src/state/store.js +202 -0
  42. package/src/state.js +1 -711
@@ -0,0 +1,99 @@
1
+ import { join } from "node:path";
2
+ import { defaultStateFile } from "../../../state.js";
3
+ import { logDir } from "../../../logs.js";
4
+
5
+ export async function handleProviderModels(id, sub, args, rest) {
6
+ if (!(sub === "models" || sub === "show-models" || sub === "list-models" || sub === "ls")) return false;
7
+ const { loadProviderConfig, loadProviderKeys, isModelAllowed } = await import("../../../state.js");
8
+ const wantsJson = args.includes("--json") || args.includes("-json");
9
+ const cfg = loadProviderConfig(id);
10
+ if (id === "opencode" || id === "oc") {
11
+ try {
12
+ const cacheFile = join(logDir(), "models.json");
13
+ const { readFileSync } = await import("node:fs");
14
+ const raw = JSON.parse(readFileSync(cacheFile, "utf8"));
15
+ const ids = (raw.data || []).map((m) => m.id).filter((x) => !String(x).includes("/"));
16
+ if (wantsJson) {
17
+ console.log(JSON.stringify({ object: "list", data: ids.map((id) => ({ id, object: "model" })) }, null, 2));
18
+ } else {
19
+ console.log(`opencode models (${ids.length}):`);
20
+ for (const mid of ids) console.log(` ${mid}`);
21
+ }
22
+ } catch (e) {
23
+ console.error(`could not read models cache: ${String(e?.message || e)}`);
24
+ process.exit(1);
25
+ }
26
+ process.exit(0);
27
+ }
28
+ try {
29
+ const { createGenericProvider } = await import("../../../providers/generic.js");
30
+ const { createWorkbuddyProvider } = await import("../../../providers/workbuddy.js");
31
+ const baseUrl = cfg?.baseUrl || (id === "workbuddy" ? "https://copilot.tencent.com" : "");
32
+ const keys = loadProviderKeys(id);
33
+ const auths = cfg?.auths || [];
34
+ let provider;
35
+ if (id === "workbuddy") {
36
+ provider = createWorkbuddyProvider({ baseUrl, apiKeys: keys, auths, file: defaultStateFile() });
37
+ } else {
38
+ if (!baseUrl) {
39
+ console.error(`provider ${id}: missing baseUrl — set via: mslxdff -provider ${id} set-url <baseUrl>`);
40
+ process.exit(1);
41
+ }
42
+ provider = createGenericProvider({ id, baseUrl, apiKeys: keys, file: defaultStateFile() });
43
+ }
44
+ const all = await provider.listModels();
45
+ const markAllowed = (mid) => {
46
+ const raw = String(mid || "").includes("/") ? String(mid).split("/").slice(1).join("/") : String(mid);
47
+ const checkRaw = mid.startsWith(`${id}/`) ? mid.slice(id.length + 1) : raw;
48
+ return isModelAllowed(id, checkRaw);
49
+ };
50
+ if (wantsJson) {
51
+ const filtered = all.filter((m) => markAllowed(m.id));
52
+ console.log(JSON.stringify({ object: "list", data: filtered }, null, 2));
53
+ } else {
54
+ const allowedCount = all.filter((m) => markAllowed(m.id)).length;
55
+ console.log(`${id} models (${all.length} total, ${allowedCount} ✓ allowed${allowedCount !== all.length ? `, ${all.length - allowedCount} x blocked by allowlist` : ""}):`);
56
+ const fmtPrice = (m) => {
57
+ const c = String(m.credits || "").trim();
58
+ if (c) {
59
+ const m0 = c.match(/x\s*([\d.]+)/i);
60
+ if (m0) return `x${m0[1]}`;
61
+ return c.replace(/\s*credits\s*/gi, "").trim().replace(/\s+/g, " ");
62
+ }
63
+ if (m.pricing && typeof m.pricing === "object") {
64
+ const p = m.pricing.prompt ?? m.pricing.input ?? m.pricing.completion ?? "";
65
+ if (p) return String(p);
66
+ }
67
+ if (m.price != null && String(m.price).trim()) return String(m.price).trim();
68
+ if (String(m.id).endsWith("/auto")) return "浮动";
69
+ return "—";
70
+ };
71
+ const fmtBadge = (m) => {
72
+ const tags = Array.isArray(m.tags) ? m.tags : [];
73
+ const b = tags.find((t) => String(t).includes("限时免费") || String(t).toLowerCase().includes("free"));
74
+ if (!b) return "";
75
+ const part = String(b).split(":")[1];
76
+ return part ? ` [${part}]` : ` [${b}]`;
77
+ };
78
+ const idW = Math.max(22, ...all.map((m) => String(m.id).length)) + 2;
79
+ const priceW = Math.max(6, ...all.map((m) => fmtPrice(m).length)) + 2;
80
+ for (const m of all) {
81
+ const ok = markAllowed(m.id);
82
+ const price = fmtPrice(m);
83
+ const badge = fmtBadge(m);
84
+ const name = m.name ? ` ${m.name}` : "";
85
+ const blocked = ok ? "" : " [blocked — allowlist]";
86
+ const line = ` ${ok ? "✓" : "x"} ${String(m.id).padEnd(idW)}${String(price).padEnd(priceW)}${name}${badge}${blocked}`;
87
+ console.log(line);
88
+ }
89
+ if (!all.length) console.log(` (no models — check baseUrl/keys or try: curl ${baseUrl}/models)`);
90
+ else if (allowedCount === 0) console.log(` tip: all blocked — mslxdff -provider ${id} allowAny on 或 allowlist set <model...>`);
91
+ else if (allowedCount !== all.length) console.log(` tip: blocked 仅影响 /v1/chat 调用,展示已全量列出`);
92
+ }
93
+ try { await provider.close?.(); } catch {}
94
+ } catch (e) {
95
+ console.error(`could not list ${id} models: ${String(e?.message || e)}`);
96
+ process.exit(1);
97
+ }
98
+ process.exit(0);
99
+ }
@@ -0,0 +1 @@
1
+ export * from "./provider/index.js";
@@ -0,0 +1,143 @@
1
+ import { join } from "node:path";
2
+ import { loadToken, getPort, savePreferredModel, loadPreferredModel } from "../../state.js";
3
+ import { getPreferredModel as getPref } from "../../auto.js";
4
+ import { normalizeModel } from "../../reasoning.js";
5
+ import { syncToWorkbuddy, workbuddyModelsPath } from "../../sync-workbuddy.js";
6
+ import { syncToOpencode, opencodeConfigPath, toExternalAlias, toInternalId } from "../../sync-opencode.js";
7
+ import { createModelsService } from "../../models.js";
8
+ import { createUpstreamClient } from "../../upstream.js";
9
+ import { logDir } from "../../logs.js";
10
+
11
+ export async function handleSetto(args) {
12
+ if (!(args.includes("-setto") || args.includes("--setto"))) return false;
13
+ const idx = args.findIndex((x) => x === "-setto" || x === "--setto");
14
+ const target = args[idx + 1];
15
+ if (!["workbuddy", "opencode"].includes(target)) {
16
+ console.error("usage: mslxdff -setto workbuddy [modelId] | mslxdff -setto opencode [modelId]");
17
+ process.exit(1);
18
+ }
19
+ if (target === "opencode") {
20
+ const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
21
+ let id;
22
+ let internal;
23
+ if (raw) {
24
+ if (raw === "auto" || !raw) {
25
+ console.error("modelId 不能为 auto 或空");
26
+ process.exit(1);
27
+ }
28
+ const norm = normalizeModel(raw);
29
+ if (!norm) {
30
+ console.error("modelId 不能为空");
31
+ process.exit(1);
32
+ }
33
+ savePreferredModel(norm);
34
+ console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
35
+ internal = toInternalId(norm);
36
+ id = toExternalAlias(internal);
37
+ } else {
38
+ const pref = loadPreferredModel() || getPref();
39
+ if (!pref) {
40
+ console.error("no preferred model set; use: mslxdff -setto opencode <modelId>");
41
+ process.exit(1);
42
+ }
43
+ const norm = normalizeModel(pref);
44
+ if (!norm) {
45
+ console.error("modelId 不能为空");
46
+ process.exit(1);
47
+ }
48
+ internal = toInternalId(norm);
49
+ id = toExternalAlias(internal);
50
+ }
51
+ try {
52
+ const cacheFile = join(logDir(), "models.json");
53
+ const models = createModelsService({
54
+ baseUrl: process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
55
+ headers: createUpstreamClient({}).headers,
56
+ refreshMs: 0,
57
+ cacheFile,
58
+ });
59
+ const fresh = await Promise.race([
60
+ models.get(),
61
+ new Promise((_, rej) => setTimeout(() => rej(new Error("refresh timeout")), 4000)),
62
+ ]);
63
+ if (fresh?.data?.length) {
64
+ const ids = fresh.data.map((m) => m.id);
65
+ if (!ids.includes(internal) && !ids.includes(id)) {
66
+ console.log(`warn: "${internal}" not in current free list (${ids.length} models), still syncing to opencode (alias ${id})`);
67
+ }
68
+ }
69
+ } catch {}
70
+ try {
71
+ const { token } = await loadToken();
72
+ const persisted = getPort();
73
+ const envPort = Number(process.env.MSLXDFF_PORT);
74
+ const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
75
+ const file = opencodeConfigPath();
76
+ const r = await syncToOpencode({ id, token, port, file });
77
+ const aliasLabel = r.alias && r.alias !== r.id ? ` (alias ${r.alias} 对应内部 ${r.internal})` : (r.id !== r.internal ? ` (alias for "${r.internal}", 原名仍兼容)` : ` (原名兼容)`);
78
+ console.log(`synced to opencode: ${r.action} "${r.id}"${aliasLabel} @ ${file}`);
79
+ console.log(` url: http://127.0.0.1:${port}/v1`);
80
+ if (r.id !== r.internal) console.log(` alias: ${r.id} -> ${r.internal} (opencode 选 mslxdff/${r.id} 直达本地 ${r.internal})`);
81
+ else console.log(` alias: ${r.internal} (原名直用,opencode 选 mslxdff/${r.id} 直达本地 ${r.internal})`);
82
+ } catch (err) {
83
+ console.error(`failed to sync to opencode: ${String(err?.message || err)}`);
84
+ process.exit(1);
85
+ }
86
+ process.exit(0);
87
+ }
88
+ const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
89
+ let id;
90
+ if (raw) {
91
+ if (raw === "auto" || !raw) {
92
+ console.error("modelId 不能为 auto 或空");
93
+ process.exit(1);
94
+ }
95
+ const norm = normalizeModel(raw);
96
+ if (!norm) {
97
+ console.error("modelId 不能为空");
98
+ process.exit(1);
99
+ }
100
+ savePreferredModel(norm);
101
+ console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
102
+ id = norm;
103
+ } else {
104
+ id = loadPreferredModel() || getPref();
105
+ if (!id) {
106
+ console.error("no preferred model set; use: mslxdff -setto workbuddy <modelId>");
107
+ process.exit(1);
108
+ }
109
+ }
110
+ try {
111
+ const cacheFile = join(logDir(), "models.json");
112
+ const models = createModelsService({
113
+ baseUrl: process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
114
+ headers: createUpstreamClient({}).headers,
115
+ refreshMs: 0,
116
+ cacheFile,
117
+ });
118
+ const fresh = await Promise.race([
119
+ models.get(),
120
+ new Promise((_, rej) => setTimeout(() => rej(new Error("refresh timeout")), 4000)),
121
+ ]);
122
+ if (fresh?.data?.length) {
123
+ const ids = fresh.data.map((m) => m.id);
124
+ if (!ids.includes(id)) {
125
+ console.log(`warn: "${id}" not in current free list (${ids.length} models), still syncing to WorkBuddy`);
126
+ }
127
+ }
128
+ } catch {}
129
+ try {
130
+ const { token } = await loadToken();
131
+ const persisted = getPort();
132
+ const envPort = Number(process.env.MSLXDFF_PORT);
133
+ const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
134
+ const file = workbuddyModelsPath();
135
+ const r = await syncToWorkbuddy({ id, token, port, file });
136
+ console.log(`synced to WorkBuddy: ${r.action} "${id}" @ ${file}`);
137
+ console.log(` url: http://127.0.0.1:${port}/v1/chat/completions`);
138
+ } catch (err) {
139
+ console.error(`failed to sync to WorkBuddy: ${String(err?.message || err)}`);
140
+ process.exit(1);
141
+ }
142
+ process.exit(0);
143
+ }
@@ -0,0 +1,236 @@
1
+ import { existsSync, readFileSync, rmSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { defaultStateFile } from "../../state.js";
5
+ import { loadToken, refreshToken } from "../../state.js";
6
+ import { stopDaemon, pidFile, logFile } from "../../daemon.js";
7
+ import { logDir, eventsFile, callsFile, errorsFile, recentEvents } from "../../logs.js";
8
+ import { fmtEvent } from "../format.js";
9
+ import { printHelp } from "../help.js";
10
+ import { printStatus } from "../status.js";
11
+ import { loadPlugins, resolvePluginDirs } from "../../plugins.js";
12
+
13
+ export async function handleHelp(args, VERSION) {
14
+ if (!(args.includes("-help") || args.includes("--help") || args.includes("-h"))) return false;
15
+ printHelp(VERSION);
16
+ process.exit(0);
17
+ }
18
+
19
+ export async function handleUpdate(args, VERSION) {
20
+ if (!(args.includes("-update") || args.includes("--update"))) return false;
21
+ const { npmCmd, run } = await import("../util.js");
22
+ const { compareSemver } = await import("../policy.js");
23
+ const { resolvePort } = await import("../../server.js");
24
+ const { readPid } = await import("../../daemon.js");
25
+ const { startDaemon, stopDaemon } = await import("../../daemon.js");
26
+ const { waitForHealth } = await import("../policy.js");
27
+ console.log(`mslxdff v${VERSION} — checking for updates…`);
28
+ const info = await run(npmCmd(), ["view", "mslxdff", "dist-tags.latest", "--json"]);
29
+ if (info.err) {
30
+ console.error(`could not query npm: ${info.err.message || String(info.stderr || "").slice(0, 500)}`);
31
+ process.exit(1);
32
+ }
33
+ let latest = "";
34
+ try {
35
+ latest = JSON.parse(String(info.stdout || "").trim());
36
+ if (Array.isArray(latest)) latest = latest[latest.length - 1];
37
+ latest = String(latest || "").trim();
38
+ } catch {
39
+ const m = String(info.stdout || "").trim().match(/(\d+\.\d+\.\d+[^\s'"]*)/);
40
+ latest = m ? m[1] : "";
41
+ }
42
+ latest = String(latest || "").replace(/['"]/g, "").trim();
43
+ const version = VERSION;
44
+ console.log(` installed: ${version}`);
45
+ console.log(` latest: ${latest || "unknown"}`);
46
+ if (!latest || version === latest || compareSemver(latest, version) <= 0) {
47
+ console.log("already up to date");
48
+ process.exit(0);
49
+ }
50
+ console.log(`updating to ${latest}…`);
51
+ const up = await run(npmCmd(), ["install", "-g", `mslxdff@${latest}`], { stdio: "inherit" });
52
+ if (up.err) {
53
+ console.error(`update failed: ${up.err.message}`);
54
+ process.exit(1);
55
+ }
56
+ console.log(`updated to ${latest}`);
57
+ const daemon = readPid();
58
+ if (daemon) {
59
+ console.log("restarting daemon on the new version…");
60
+ stopDaemon();
61
+ startDaemon([]);
62
+ await waitForHealth(resolvePort(), 4000);
63
+ console.log(`restarted (pid ${readPid()})`);
64
+ }
65
+ process.exit(0);
66
+ }
67
+
68
+ export async function handleRefreshToken(args) {
69
+ if (!(args.includes("-refresh-token") || args.includes("--refresh-token"))) return false;
70
+ const token = await refreshToken();
71
+ console.log(token);
72
+ process.exit(0);
73
+ }
74
+
75
+ export async function handleShowToken(args) {
76
+ if (!(args.includes("-showtoken") || args.includes("--showtoken"))) return false;
77
+ const { token } = await loadToken();
78
+ console.log(token);
79
+ process.exit(0);
80
+ }
81
+
82
+ export async function handleUninstall(args) {
83
+ if (!(args.includes("-uninstall") || args.includes("--uninstall"))) return false;
84
+ const { stopped, pid } = stopDaemon();
85
+ if (stopped) console.log(`mslxdff daemon stopped (pid ${pid})`);
86
+ else console.log("mslxdff daemon not running");
87
+ const stateFile = defaultStateFile();
88
+ const dir = dirname(stateFile);
89
+ const removed = [];
90
+ for (const f of [
91
+ stateFile,
92
+ pidFile(),
93
+ logFile(),
94
+ join(dir, "calls.log"),
95
+ join(dir, "errors.log"),
96
+ join(dir, "events.log"),
97
+ ]) {
98
+ try {
99
+ rmSync(f, { force: true });
100
+ removed.push(f);
101
+ } catch {}
102
+ }
103
+ if (removed.length) console.log(`removed ${removed.length} file(s):\n ${removed.join("\n ")}`);
104
+ else console.log("no state/log files to remove");
105
+ console.log("\npackage still installed — finish with:");
106
+ console.log(" npm uninstall -g mslxdff");
107
+ process.exit(0);
108
+ }
109
+
110
+ export async function handleLog(args) {
111
+ if (!(args.includes("-log") || args.includes("--log") || args.includes("-logs") || args.includes("--logs"))) return false;
112
+ const idx = args.findIndex((x) => x === "-log" || x === "--log" || x === "-logs" || x === "--logs");
113
+ const raw = args[idx + 1];
114
+ const n = Number(raw);
115
+ const count = Number.isInteger(n) && n > 0 ? n : 10;
116
+ const file = eventsFile();
117
+ const dir = logDir();
118
+ console.log(`log dir: ${dir}`);
119
+ console.log(`events: ${file}`);
120
+ const evts = recentEvents(count);
121
+ if (!evts.length) {
122
+ console.log(`(no events yet — file empty or not found)`);
123
+ } else {
124
+ console.log(`--- last ${evts.length} event(s) ---`);
125
+ for (const e of evts) console.log(fmtEvent(e));
126
+ }
127
+ if (count <= 10) {
128
+ console.log(`\nhint: mslxdff -log 100 | calls: ${callsFile()} errors: ${errorsFile()} daemon: ${logFile()}`);
129
+ }
130
+ process.exit(0);
131
+ }
132
+
133
+ export async function handlePlugins(args) {
134
+ if (!(args.includes("-plugins") || args.includes("--plugins"))) return false;
135
+ const pkgRoot = dirname(dirname(fileURLToPath(import.meta.url)));
136
+ const dirs = resolvePluginDirs({ pkgRoot: dirname(pkgRoot) });
137
+ // Actually pkgRoot is src/cli -> need to go to root: bin/mslxdff.js used dirname(dirname(...)). For src/cli/commands/system.js, need 3 levels.
138
+ // Simpler: compute from import.meta.url
139
+ const root = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
140
+ const dirs2 = resolvePluginDirs({ pkgRoot: root });
141
+ const labels = ["official (bundled)", "user"];
142
+ if (dirs2.length === 1) labels[0] = "dir";
143
+ console.log(`plugin dirs:`);
144
+ dirs2.forEach((d, i) => console.log(` [${labels[i] || `dir${i + 1}`}] ${d}${existsSync(d) ? "" : " (not created yet)"}`));
145
+ const { plugins, errors } = await loadPlugins({ dirs: dirs2 });
146
+ if (!plugins.length && !errors.length) {
147
+ console.log("(no plugins — drop *.mjs files into a dir above, see docs/plugins.md)");
148
+ }
149
+ for (const p of plugins) {
150
+ const hooks = Object.keys(p.hooks || {});
151
+ const src = p.file.startsWith(root) ? "official" : "user";
152
+ console.log(` ${p.name}${p.version ? `@${p.version}` : ""} [${hooks.join(", ") || "no hooks"}] (${src})`);
153
+ if (p.description) console.log(` ${p.description}`);
154
+ }
155
+ for (const e of errors) console.log(` load error: ${e.file} — ${e.error}`);
156
+ process.exit(0);
157
+ }
158
+
159
+ export async function handleChat(args) {
160
+ if (!(args.includes("-chat") || args.includes("--chat"))) return false;
161
+ const idx = args.findIndex((x) => x === "-chat" || x === "--chat");
162
+ const rest = args.slice(idx + 1);
163
+ const singleShot = rest.length ? rest.join(" ").trim() : null;
164
+ const { startChat } = await import("../../chat/index.js");
165
+ await startChat({ singleShot: singleShot || undefined });
166
+ process.exit(0);
167
+ }
168
+
169
+ export async function handleStatus(args, VERSION) {
170
+ if (!(args.includes("-status") || args.includes("--status") || args.includes("-s"))) return false;
171
+ await printStatus(VERSION);
172
+ process.exit(0);
173
+ }
174
+
175
+ export async function handleFree(args) {
176
+ if (!(args.includes("-free") || args.includes("--free") || args.includes("-free-check") || args.includes("--free-check") || args.includes("-free-watch") || args.includes("--free-watch"))) return false;
177
+ const isWatch = args.includes("-free-watch") || args.includes("--free-watch");
178
+ const { fetchV2exFree } = await import("../../free-watcher.js");
179
+ const show = async () => {
180
+ const hits = await fetchV2exFree({ timeoutMs: 6000 });
181
+ const ts = new Date().toISOString().replace("T", " ").slice(0, 19);
182
+ console.log(`[V2EX] free check @ ${ts} — ${hits.length} hit(s)`);
183
+ if (!hits.length) {
184
+ console.log("(暂无命中 — 关键词:白嫖|限免|免费额度|注册送|羊毛,来源:/api/topics/latest.json + hot.json)");
185
+ } else {
186
+ for (const h of hits) console.log(` ${h.title} | ${h.url} | ${h.node} ${h.replies}回复`);
187
+ }
188
+ };
189
+ if (!isWatch) {
190
+ try { await show(); } catch (err) { console.error(`V2EX 拉取失败: ${err?.message || err}`); process.exit(1); }
191
+ process.exit(0);
192
+ }
193
+ console.log("V2EX 白嫖雷达 watch 模式 — 每 5 分钟拉一次 Ctrl+C 退出");
194
+ const run = async () => {
195
+ try { await show(); } catch (err) { console.error(`[${new Date().toISOString().slice(11,19)}] 拉取失败: ${err?.message || err}`); }
196
+ console.log("---");
197
+ };
198
+ await run();
199
+ const timer = setInterval(run, 5 * 60 * 1000);
200
+ timer.unref?.();
201
+ await new Promise(() => {});
202
+ }
203
+
204
+ export async function handleAutostart(args) {
205
+ if (!(args.includes("-enable-autostart") || args.includes("--enable-autostart") || args.includes("-disable-autostart") || args.includes("--disable-autostart") || args.includes("-autostart") || args.includes("--autostart"))) return false;
206
+ const { enableAutostart, disableAutostart, getAutostartStatus, autostartHelp } = await import("../../autostart.js");
207
+ if (args.includes("-enable-autostart") || args.includes("--enable-autostart")) {
208
+ const r = await enableAutostart();
209
+ if (r.ok) {
210
+ console.log(`autostart 已启用 · ${r.method || autostartHelp()}`);
211
+ console.log(`验证: mslxdff -autostart status`);
212
+ } else {
213
+ console.error(`启用自启失败: ${r.error || "unknown"}`);
214
+ console.error(`提示: Windows 需允许任务计划,Linux 需 systemd --user`);
215
+ process.exit(1);
216
+ }
217
+ process.exit(0);
218
+ }
219
+ if (args.includes("-disable-autostart") || args.includes("--disable-autostart")) {
220
+ const r = await disableAutostart();
221
+ if (r.ok) console.log(`autostart 已禁用`);
222
+ else { console.error(`禁用失败: ${r.error}`); process.exit(1); }
223
+ process.exit(0);
224
+ }
225
+ const idx = args.findIndex((x) => x === "-autostart" || x === "--autostart");
226
+ const sub = args[idx + 1];
227
+ if (!sub || sub === "status" || sub === "list") {
228
+ const s = await getAutostartStatus();
229
+ console.log(`autostart: ${s.enabled ? "已启用" : "未启用"} · ${s.detail || autostartHelp()}`);
230
+ if (s.taskToRun) console.log(` task: ${s.taskToRun}`);
231
+ if (s.unit) console.log(` unit: ${s.unit}`);
232
+ process.exit(0);
233
+ }
234
+ console.error("usage: mslxdff -enable-autostart | mslxdff -disable-autostart | mslxdff -autostart status");
235
+ process.exit(1);
236
+ }
@@ -0,0 +1,91 @@
1
+ import { join, dirname } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ export async function handleWorkbuddy(args) {
5
+ if (!(args.includes("-workbuddy") || args.includes("--workbuddy") || args.includes("-wb"))) return false;
6
+ const idx = args.findIndex((x) => x === "-workbuddy" || x === "--workbuddy" || x === "-wb");
7
+ const sub = args[idx + 1];
8
+ if (!sub || sub === "checkin" || sub === "daily-checkin" || sub === "check-in") {
9
+ const { spawn } = await import("node:child_process");
10
+ const script = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "workbuddy-checkin.js");
11
+ // for src/cli/commands/workbuddy.js -> ../../../workbuddy-checkin.js is root/workbuddy-checkin.js
12
+ const child = spawn(process.execPath, [script, ...args.slice(idx + 2)], { stdio: "inherit" });
13
+ child.on("close", (code) => process.exit(code ?? 0));
14
+ child.on("error", (err) => { console.error(`workbuddy checkin failed: ${err.message}`); process.exit(1); });
15
+ await new Promise(() => {});
16
+ } else if (sub === "balance" || sub === "balances" || sub === "credit" || sub === "credits") {
17
+ const asJson = args.includes("--json") || args.includes("-json");
18
+ const { loadProviderConfigs } = await import("../../state.js");
19
+ const { fetchBalance } = await import("../../providers/workbuddy-balance.js");
20
+ const cfg = loadProviderConfigs().workbuddy || {};
21
+ const auths = Array.isArray(cfg.auths) ? cfg.auths : [];
22
+ const keys = Array.isArray(cfg.keys) ? cfg.keys : [];
23
+ let items = auths.map((a,i)=> ({ uid:a.uid, domain:a.domain, key: keys[i]||"" , auth:a }));
24
+ if (!items.length) {
25
+ try {
26
+ const { readdirSync, readFileSync, existsSync } = await import("node:fs");
27
+ const { join } = await import("node:path");
28
+ const dir = process.env.WORKBUDDY_AUTH_DIR || join(process.cwd(), "auths");
29
+ if (existsSync(dir)) {
30
+ for (const f of readdirSync(dir).filter(x=>x.startsWith("workbuddy-")&&x.endsWith(".json"))) {
31
+ try { const j=JSON.parse(readFileSync(join(dir,f),"utf8")); if(j?.account?.uid) items.push({uid:j.account.uid, domain:j.auth.domain||"www.codebuddy.cn", key:j.auth.accessToken, auth:{uid:j.account.uid, domain:j.auth.domain||"www.codebuddy.cn", enterpriseId:j.account.enterpriseId||""}}); } catch {}
32
+ }
33
+ }
34
+ } catch {}
35
+ }
36
+ if (!items.length) { console.log("no workbuddy accounts — run node workbuddy-token-auto.js"); process.exit(0); }
37
+ const results=[];
38
+ for (const it of items) {
39
+ const b = await fetchBalance({ uid: it.uid, key: it.key, auth: it.auth }).catch(()=>null);
40
+ results.push({ uid: it.uid, domain: it.domain, balance: b });
41
+ }
42
+ if (asJson) { console.log(JSON.stringify({ results }, null, 2)); process.exit(0); }
43
+ console.log(`workbuddy balances (${results.length}):`);
44
+ for (const r of results) {
45
+ const b=r.balance;
46
+ if (!b) console.log(` ${r.uid} (balance unavailable) domain=${r.domain}`);
47
+ else console.log(` ${r.uid} total=${b.totalStr||b.total} dailyPacks=${b.dailyPacks} active=${b.activeCount} nextExpire=${b.nextExpire||"-"} domain=${r.domain}`);
48
+ }
49
+ process.exit(0);
50
+ } else if (sub === "list" || sub === "ls" || sub === "status") {
51
+ const { loadProviderConfigs } = await import("../../state.js");
52
+ const cfg = loadProviderConfigs().workbuddy || {};
53
+ const auths = Array.isArray(cfg.auths) ? cfg.auths : [];
54
+ const keys = Array.isArray(cfg.keys) ? cfg.keys : [];
55
+ if (!auths.length) { console.log("no workbuddy accounts in state — check auths/workbuddy-*.json"); process.exit(0); }
56
+ console.log(`workbuddy accounts (${auths.length}):`);
57
+ auths.forEach((a,i)=> {
58
+ const k=(keys[i]||"").slice(0,4);
59
+ console.log(` [${i+1}] uid=${a.uid} domain=${a.domain||"www.codebuddy.cn"} enterprise=${a.enterpriseId||"-"} key=${k?k+"…":"(none)"} refresh=${a.refreshToken?"yes":"no"}`);
60
+ });
61
+ process.exit(0);
62
+ } else if (sub === "remove" || sub === "rm" || sub === "del" || sub === "delete") {
63
+ const target = args[idx+2];
64
+ if (!target) { console.error("usage: mslxdff -workbuddy remove <uid> [--keep-file]"); process.exit(1); }
65
+ const keep = args.includes("--keep-file");
66
+ const { loadProviderConfigs, saveProviderConfig } = await import("../../state.js");
67
+ const cfg = loadProviderConfigs().workbuddy || {};
68
+ let auths = Array.isArray(cfg.auths)? [...cfg.auths]:[];
69
+ let keys = Array.isArray(cfg.keys)? [...cfg.keys]:[];
70
+ let rmIdx = auths.findIndex(a=> a.uid===target || a.uid.startsWith(target));
71
+ if (rmIdx<0) { console.error(`uid not found: ${target}`); process.exit(1); }
72
+ const uid = auths[rmIdx].uid;
73
+ auths.splice(rmIdx,1); keys.splice(rmIdx,1);
74
+ saveProviderConfig("workbuddy", { baseUrl: cfg.baseUrl||"https://copilot.tencent.com", keys, auths });
75
+ if (!keep) {
76
+ try {
77
+ const { existsSync, unlinkSync } = await import("node:fs");
78
+ const { join } = await import("node:path");
79
+ const dir = process.env.WORKBUDDY_AUTH_DIR || join(process.cwd(), "auths");
80
+ const fp = join(dir, `workbuddy-${uid}.json`);
81
+ if (existsSync(fp)) { unlinkSync(fp); console.log(`removed file ${fp}`); }
82
+ } catch {}
83
+ }
84
+ try { const { getBalanceCache } = await import("../../providers/workbuddy-balance.js"); getBalanceCache().delete(uid); } catch {}
85
+ console.log(`removed workbuddy ${uid} (now ${auths.length} account(s))`);
86
+ process.exit(0);
87
+ } else {
88
+ console.error("usage: mslxdff -workbuddy checkin | balance [--json] | list | remove <uid>");
89
+ process.exit(1);
90
+ }
91
+ }