mvframe 1.0.73 → 1.0.75

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mvframe",
3
3
  "packageManager": "yarn@4.4.1",
4
- "version": "1.0.73",
4
+ "version": "1.0.75",
5
5
  "author": "matt avis",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -18,6 +18,10 @@
18
18
  "import": "./dist/maps.js",
19
19
  "require": "./dist/maps.js"
20
20
  },
21
+ "./notify": {
22
+ "import": "./dist/notify.js",
23
+ "require": "./dist/notify.js"
24
+ },
21
25
  "./store": {
22
26
  "import": "./dist/store.js",
23
27
  "require": "./dist/store.js"
@@ -36,6 +40,8 @@
36
40
  },
37
41
  "scripts": {
38
42
  "dev": "vite",
43
+ "d": "node scripts/dev-with-notify.js",
44
+ "notify": "node scripts/notify-server.js",
39
45
  "build": "node scripts/prebuild.js",
40
46
  "install-cursor-skill": "node scripts/install-cursor-skill.js",
41
47
  "install-codex-rules": "node scripts/install-codex-agents.js",
@@ -44,16 +50,20 @@
44
50
  "b": "node scripts/build-host.js"
45
51
  },
46
52
  "bin": {
53
+ "mvframe-b": "scripts/build-host.js",
54
+ "mvframe-d": "scripts/dev-with-notify.js",
47
55
  "mvframe-init-app": "scripts/scaffold-app.js",
48
- "mvframe-install-cursor-skill": "scripts/install-cursor-skill.js",
49
56
  "mvframe-install-codex-rules": "scripts/install-codex-agents.js",
50
- "mvframe-b": "scripts/build-host.js"
57
+ "mvframe-install-cursor-skill": "scripts/install-cursor-skill.js",
58
+ "mvframe-notify": "scripts/notify-server.js"
51
59
  },
52
60
  "files": [
53
61
  "dist/*",
54
62
  "scripts/build-host.js",
63
+ "scripts/dev-with-notify.js",
55
64
  "scripts/install-codex-agents.js",
56
65
  "scripts/install-cursor-skill.js",
66
+ "scripts/notify-server.js",
57
67
  "scripts/scaffold-app.js",
58
68
  ".cursor/skills/mvframe-app-init",
59
69
  ".cursor/rules"
@@ -75,7 +85,6 @@
75
85
  "devDependencies": {
76
86
  "@vitejs/plugin-vue": "^5.2.1",
77
87
  "@vue/shared": "^3.5.25",
78
- "devrq": "file:../devrq",
79
88
  "element-plus": "^2.13.6",
80
89
  "rollup-plugin-terser": "^7.0.2",
81
90
  "sass-embedded": "^1.97.3",
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Run Vite and the MVFrame DingTalk notify service together.
4
+ * This mirrors `mvframe-b`: the package owns the command, the host may opt in
5
+ * with `yarn exec mvframe-d` or a local script.
6
+ */
7
+ const { spawn } = require("child_process");
8
+ const path = require("path");
9
+ const fs = require("fs");
10
+ const { createRequire } = require("module");
11
+
12
+ const children = new Set();
13
+ let shuttingDown = false;
14
+
15
+ function spawnChild(name, command, args, options = {}) {
16
+ const child = spawn(command, args, {
17
+ cwd: process.cwd(),
18
+ env: process.env,
19
+ stdio: "inherit",
20
+ shell: false,
21
+ ...options,
22
+ });
23
+
24
+ children.add(child);
25
+
26
+ child.on("exit", (code, signal) => {
27
+ children.delete(child);
28
+ if (shuttingDown) return;
29
+ if (code !== 0) {
30
+ console.error(`[mvframe-d] ${name} exited with ${signal || code}`);
31
+ shutdown(code || 1);
32
+ }
33
+ });
34
+
35
+ child.on("error", (error) => {
36
+ console.error(`[mvframe-d] failed to start ${name}: ${error.message}`);
37
+ shutdown(1);
38
+ });
39
+
40
+ return child;
41
+ }
42
+
43
+ function shutdown(code = 0) {
44
+ if (shuttingDown) return;
45
+ shuttingDown = true;
46
+ for (const child of children) {
47
+ if (!child.killed) {
48
+ child.kill("SIGTERM");
49
+ }
50
+ }
51
+ setTimeout(() => process.exit(code), 200);
52
+ }
53
+
54
+ process.on("SIGINT", () => shutdown(0));
55
+ process.on("SIGTERM", () => shutdown(0));
56
+
57
+ const notifyArgs = process.argv.slice(2);
58
+ const notifyScript = path.join(__dirname, "notify-server.js");
59
+
60
+ function resolveViteCli(cwd) {
61
+ let vpkg = "";
62
+ try {
63
+ const projectRequire = createRequire(path.join(cwd, "package.json"));
64
+ vpkg = projectRequire.resolve("vite/package.json");
65
+ } catch {
66
+ // Fall back to node_modules for non-PnP projects.
67
+ }
68
+
69
+ if (!vpkg) {
70
+ vpkg = path.join(cwd, "node_modules", "vite", "package.json");
71
+ }
72
+
73
+ if (fs.existsSync(vpkg)) {
74
+ const p = require(vpkg);
75
+ const bin = typeof p.bin === "string" ? p.bin : p.bin && p.bin.vite;
76
+ if (bin) {
77
+ const cli = path.join(path.dirname(vpkg), bin);
78
+ if (fs.existsSync(cli)) return cli;
79
+ }
80
+ }
81
+ throw new Error(
82
+ "未在当前项目找到 Vite(请先安装依赖;宿主项目可执行 yarn install 后再运行 mvframe-d)",
83
+ );
84
+ }
85
+
86
+ spawnChild("notify", process.execPath, [notifyScript, ...notifyArgs]);
87
+ try {
88
+ const viteEntry = resolveViteCli(process.cwd());
89
+ spawnChild("vite", process.execPath, [viteEntry, "--host", "0.0.0.0"]);
90
+ } catch {
91
+ spawnChild("vite", "yarn", ["vite", "--host", "0.0.0.0"]);
92
+ }
@@ -0,0 +1,405 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MVFrame DingTalk notify service.
4
+ *
5
+ * Secrets stay in the Node process. Browser code should only call this local
6
+ * service with `MVFRAME_NOTIFY_TOKEN`, never hold the DingTalk webhook/secret.
7
+ */
8
+ const http = require("http");
9
+ const https = require("https");
10
+ const crypto = require("crypto");
11
+ const fs = require("fs");
12
+ const path = require("path");
13
+
14
+ const DEFAULT_ENV_FILE = ".env.mvframe-notify";
15
+
16
+ function parseArgs(argv) {
17
+ const out = {};
18
+ for (let i = 0; i < argv.length; i += 1) {
19
+ const arg = argv[i];
20
+ if (!arg.startsWith("--")) continue;
21
+ const eq = arg.indexOf("=");
22
+ if (eq > 0) {
23
+ out[arg.slice(2, eq)] = arg.slice(eq + 1);
24
+ continue;
25
+ }
26
+ const key = arg.slice(2);
27
+ const next = argv[i + 1];
28
+ if (next && !next.startsWith("--")) {
29
+ out[key] = next;
30
+ i += 1;
31
+ } else {
32
+ out[key] = true;
33
+ }
34
+ }
35
+ return out;
36
+ }
37
+
38
+ function trimEnvValue(value) {
39
+ let out = String(value ?? "").trim();
40
+ if (out.length >= 2) {
41
+ const first = out[0];
42
+ const last = out[out.length - 1];
43
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
44
+ out = out.slice(1, -1);
45
+ }
46
+ }
47
+ return out;
48
+ }
49
+
50
+ function loadEnvFile(file) {
51
+ if (!file || !fs.existsSync(file)) return;
52
+ const content = fs.readFileSync(file, "utf8");
53
+ for (const raw of content.split(/\r?\n/)) {
54
+ const line = raw.trim();
55
+ if (!line || line.startsWith("#")) continue;
56
+ const normalized = line.startsWith("export ") ? line.slice(7).trim() : line;
57
+ const eq = normalized.indexOf("=");
58
+ if (eq <= 0) continue;
59
+ const key = normalized.slice(0, eq).trim();
60
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
61
+ if (process.env[key] == null) {
62
+ process.env[key] = trimEnvValue(normalized.slice(eq + 1));
63
+ }
64
+ }
65
+ }
66
+
67
+ function splitList(value) {
68
+ return String(value || "")
69
+ .split(",")
70
+ .map((item) => item.trim())
71
+ .filter(Boolean);
72
+ }
73
+
74
+ function parseBoolean(value) {
75
+ return /^(1|true|yes|on)$/i.test(String(value || ""));
76
+ }
77
+
78
+ function readStdin() {
79
+ return new Promise((resolve, reject) => {
80
+ let body = "";
81
+ process.stdin.setEncoding("utf8");
82
+ process.stdin.on("data", (chunk) => {
83
+ body += chunk;
84
+ });
85
+ process.stdin.on("end", () => resolve(body));
86
+ process.stdin.on("error", reject);
87
+ });
88
+ }
89
+
90
+ function readRequestBody(req, limit = 64 * 1024) {
91
+ return new Promise((resolve, reject) => {
92
+ let body = "";
93
+ req.setEncoding("utf8");
94
+ req.on("data", (chunk) => {
95
+ body += chunk;
96
+ if (Buffer.byteLength(body) > limit) {
97
+ reject(new Error("request body too large"));
98
+ req.destroy();
99
+ }
100
+ });
101
+ req.on("end", () => resolve(body));
102
+ req.on("error", reject);
103
+ });
104
+ }
105
+
106
+ function writeJson(res, status, payload, corsOrigin) {
107
+ res.writeHead(status, {
108
+ "Content-Type": "application/json; charset=utf-8",
109
+ "Access-Control-Allow-Origin": corsOrigin,
110
+ "Access-Control-Allow-Methods": "GET,POST,OPTIONS",
111
+ "Access-Control-Allow-Headers": "content-type,authorization,x-mvframe-notify-token",
112
+ });
113
+ if (status === 204) {
114
+ res.end();
115
+ return;
116
+ }
117
+ res.end(`${JSON.stringify(payload, null, 2)}\n`);
118
+ }
119
+
120
+ function sanitizeStatus(config) {
121
+ return {
122
+ provider: "dingtalk",
123
+ enabled: Boolean(config.webhook),
124
+ signed: Boolean(config.secret),
125
+ port: config.port,
126
+ host: config.host,
127
+ hasWebhook: Boolean(config.webhook),
128
+ hasSecret: Boolean(config.secret),
129
+ hasToken: Boolean(config.token),
130
+ atMobileCount: config.atMobiles.length,
131
+ atUserIdCount: config.atUserIds.length,
132
+ atAll: config.atAll,
133
+ timeoutMs: config.timeoutMs,
134
+ };
135
+ }
136
+
137
+ function buildDingTalkUrl(config) {
138
+ const url = new URL(config.webhook);
139
+ if (!config.secret) return url.toString();
140
+
141
+ const timestamp = Date.now().toString();
142
+ const sign = crypto
143
+ .createHmac("sha256", config.secret)
144
+ .update(`${timestamp}\n${config.secret}`, "utf8")
145
+ .digest("base64");
146
+
147
+ url.searchParams.set("timestamp", timestamp);
148
+ url.searchParams.set("sign", sign);
149
+ return url.toString();
150
+ }
151
+
152
+ function postJson(url, payload, timeoutMs) {
153
+ return new Promise((resolve, reject) => {
154
+ const target = new URL(url);
155
+ const body = JSON.stringify(payload);
156
+ const client = target.protocol === "https:" ? https : http;
157
+ const req = client.request(
158
+ target,
159
+ {
160
+ method: "POST",
161
+ timeout: timeoutMs,
162
+ headers: {
163
+ "Content-Type": "application/json",
164
+ "Content-Length": Buffer.byteLength(body),
165
+ },
166
+ },
167
+ (response) => {
168
+ let data = "";
169
+ response.setEncoding("utf8");
170
+ response.on("data", (chunk) => {
171
+ data += chunk;
172
+ });
173
+ response.on("end", () => {
174
+ let parsed = data;
175
+ try {
176
+ parsed = data ? JSON.parse(data) : null;
177
+ } catch {
178
+ // Keep raw response body.
179
+ }
180
+ resolve({
181
+ status: response.statusCode || 0,
182
+ data: parsed,
183
+ });
184
+ });
185
+ },
186
+ );
187
+
188
+ req.on("timeout", () => {
189
+ req.destroy(new Error(`dingtalk timeout after ${timeoutMs}ms`));
190
+ });
191
+ req.on("error", reject);
192
+ req.write(body);
193
+ req.end();
194
+ });
195
+ }
196
+
197
+ async function sendDingTalk(config, message, options = {}) {
198
+ if (!config.webhook) {
199
+ return {
200
+ ok: false,
201
+ message: "missing DINGTALK_WEBHOOK",
202
+ };
203
+ }
204
+
205
+ const atMobiles = Array.isArray(options.atMobiles)
206
+ ? options.atMobiles
207
+ : config.atMobiles;
208
+ const atUserIds = Array.isArray(options.atUserIds)
209
+ ? options.atUserIds
210
+ : config.atUserIds;
211
+ const atAll = options.atAll == null ? config.atAll : Boolean(options.atAll);
212
+
213
+ const response = await postJson(
214
+ buildDingTalkUrl(config),
215
+ {
216
+ msgtype: "text",
217
+ text: {
218
+ content: String(message || ""),
219
+ },
220
+ at: {
221
+ atMobiles,
222
+ atUserIds,
223
+ isAtAll: atAll,
224
+ },
225
+ },
226
+ config.timeoutMs,
227
+ );
228
+
229
+ const data = response.data;
230
+ const ok =
231
+ response.status >= 200 &&
232
+ response.status < 300 &&
233
+ (!data || typeof data !== "object" || data.errcode === undefined || data.errcode === 0);
234
+
235
+ return {
236
+ ok,
237
+ httpStatus: response.status,
238
+ code: data && typeof data === "object" ? data.errcode : undefined,
239
+ message:
240
+ data && typeof data === "object"
241
+ ? data.errmsg || (ok ? "sent" : "dingtalk rejected")
242
+ : ok
243
+ ? "sent"
244
+ : "dingtalk rejected",
245
+ data,
246
+ };
247
+ }
248
+
249
+ function getAuthToken(req) {
250
+ const headerToken = req.headers["x-mvframe-notify-token"];
251
+ if (typeof headerToken === "string" && headerToken.trim()) {
252
+ return headerToken.trim();
253
+ }
254
+
255
+ const authorization = req.headers.authorization;
256
+ if (typeof authorization === "string") {
257
+ const match = authorization.match(/^Bearer\s+(.+)$/i);
258
+ if (match) return match[1].trim();
259
+ }
260
+
261
+ return "";
262
+ }
263
+
264
+ function ensureAuthorized(req, config) {
265
+ if (!config.token) return true;
266
+ return getAuthToken(req) === config.token;
267
+ }
268
+
269
+ function parseMessageFromUrl(url) {
270
+ return url.searchParams.get("msg") || url.searchParams.get("message") || "";
271
+ }
272
+
273
+ async function parseNotifyPayload(req, url) {
274
+ if (req.method === "GET") {
275
+ return {
276
+ message: parseMessageFromUrl(url),
277
+ };
278
+ }
279
+
280
+ const raw = await readRequestBody(req);
281
+ const contentType = String(req.headers["content-type"] || "").toLowerCase();
282
+ if (contentType.includes("application/json")) {
283
+ const parsed = raw ? JSON.parse(raw) : {};
284
+ return {
285
+ message: parsed.message ?? parsed.msg ?? "",
286
+ atMobiles: Array.isArray(parsed.atMobiles) ? parsed.atMobiles : undefined,
287
+ atUserIds: Array.isArray(parsed.atUserIds) ? parsed.atUserIds : undefined,
288
+ atAll: parsed.atAll,
289
+ };
290
+ }
291
+
292
+ const params = new URLSearchParams(raw);
293
+ return {
294
+ message: params.get("message") || params.get("msg") || parseMessageFromUrl(url),
295
+ };
296
+ }
297
+
298
+ function createConfig(args) {
299
+ const envFile = path.resolve(
300
+ process.cwd(),
301
+ String(args.env || process.env.MVFRAME_NOTIFY_ENV || DEFAULT_ENV_FILE),
302
+ );
303
+ loadEnvFile(envFile);
304
+
305
+ return {
306
+ envFile,
307
+ port: Number(args.port || process.env.MVFRAME_NOTIFY_PORT || process.env.NOTIFY_PORT || 3300),
308
+ host: String(args.host || process.env.MVFRAME_NOTIFY_HOST || "127.0.0.1"),
309
+ token: String(args.token || process.env.MVFRAME_NOTIFY_TOKEN || "").trim(),
310
+ webhook: String(args.webhook || process.env.DINGTALK_WEBHOOK || "").trim(),
311
+ secret: String(args.secret || process.env.DINGTALK_SECRET || "").trim(),
312
+ atMobiles: splitList(args.atMobiles || process.env.DINGTALK_AT_MOBILES || ""),
313
+ atUserIds: splitList(args.atUserIds || process.env.DINGTALK_AT_USER_IDS || ""),
314
+ atAll: parseBoolean(args.atAll || process.env.DINGTALK_AT_ALL || ""),
315
+ timeoutMs: Number(args.timeout || process.env.MVFRAME_NOTIFY_TIMEOUT_MS || 5000),
316
+ corsOrigin: String(args.corsOrigin || process.env.MVFRAME_NOTIFY_CORS_ORIGIN || "*"),
317
+ };
318
+ }
319
+
320
+ async function runOnce(config, args) {
321
+ const message = args.message || args.msg || (await readStdin()).trim();
322
+ if (!message) {
323
+ console.error("[mvframe-notify] missing message");
324
+ process.exit(1);
325
+ }
326
+ const result = await sendDingTalk(config, message);
327
+ console.log(JSON.stringify(result, null, 2));
328
+ process.exit(result.ok ? 0 : 1);
329
+ }
330
+
331
+ function startServer(config) {
332
+ const server = http.createServer(async (req, res) => {
333
+ const url = new URL(req.url || "/", `http://${req.headers.host || "127.0.0.1"}`);
334
+
335
+ if (req.method === "OPTIONS") {
336
+ writeJson(res, 204, null, config.corsOrigin);
337
+ return;
338
+ }
339
+
340
+ if (url.pathname === "/health") {
341
+ writeJson(res, 200, { ok: true, ...sanitizeStatus(config) }, config.corsOrigin);
342
+ return;
343
+ }
344
+
345
+ if (url.pathname !== "/notify" && url.pathname !== "/push") {
346
+ writeJson(res, 404, { ok: false, message: "not found" }, config.corsOrigin);
347
+ return;
348
+ }
349
+
350
+ if (!ensureAuthorized(req, config)) {
351
+ writeJson(res, 401, { ok: false, message: "unauthorized" }, config.corsOrigin);
352
+ return;
353
+ }
354
+
355
+ try {
356
+ const payload = await parseNotifyPayload(req, url);
357
+ const message = String(payload.message || "").trim();
358
+ if (!message) {
359
+ writeJson(res, 400, { ok: false, message: "missing message" }, config.corsOrigin);
360
+ return;
361
+ }
362
+
363
+ const push = await sendDingTalk(config, message, payload);
364
+ writeJson(res, push.ok ? 200 : 502, { ok: push.ok, push }, config.corsOrigin);
365
+ } catch (error) {
366
+ writeJson(
367
+ res,
368
+ 500,
369
+ {
370
+ ok: false,
371
+ message: error.message || String(error),
372
+ },
373
+ config.corsOrigin,
374
+ );
375
+ }
376
+ });
377
+
378
+ server.listen(config.port, config.host, () => {
379
+ const status = sanitizeStatus(config);
380
+ console.log(`[mvframe-notify] DingTalk service: http://${config.host}:${config.port}`);
381
+ console.log(
382
+ `[mvframe-notify] status: webhook=${status.hasWebhook ? "set" : "missing"}, secret=${status.hasSecret ? "set" : "missing"}, token=${status.hasToken ? "enabled" : "disabled"}`,
383
+ );
384
+ console.log(`[mvframe-notify] env: ${path.relative(process.cwd(), config.envFile)}`);
385
+ });
386
+
387
+ return server;
388
+ }
389
+
390
+ async function main() {
391
+ const args = parseArgs(process.argv.slice(2));
392
+ const config = createConfig(args);
393
+
394
+ if (args.once) {
395
+ await runOnce(config, args);
396
+ return;
397
+ }
398
+
399
+ startServer(config);
400
+ }
401
+
402
+ main().catch((error) => {
403
+ console.error(error.message || error);
404
+ process.exit(1);
405
+ });