mvframe 1.0.73 → 1.0.74

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.74",
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
+ });
@@ -36,6 +36,22 @@ function write(rel, content) {
36
36
  console.log("[mvframe-init] 写入", rel);
37
37
  }
38
38
 
39
+ function appendGitignoreLines(lines) {
40
+ const fp = path.join(target, ".gitignore");
41
+ const existing = fs.existsSync(fp) ? fs.readFileSync(fp, "utf8") : "";
42
+ const current = new Set(
43
+ existing
44
+ .split(/\r?\n/)
45
+ .map((line) => line.trim())
46
+ .filter(Boolean),
47
+ );
48
+ const missing = lines.filter((line) => !current.has(line));
49
+ if (missing.length === 0) return;
50
+ const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
51
+ fs.writeFileSync(fp, `${existing}${prefix}${missing.join("\n")}\n`, "utf8");
52
+ console.log("[mvframe-init] 已更新 .gitignore");
53
+ }
54
+
39
55
  /**
40
56
  * 将本包 `.cursor/rules/*.mdc` 复制到目标工程,与 mvframe 仓库内 Cursor 规则保持一致。
41
57
  * 发布包需在 `package.json` 的 `files` 中包含 `.cursor/rules`。
@@ -166,6 +182,7 @@ function main() {
166
182
 
167
183
  copyCursorRulesFromPackage();
168
184
  upsertCodexAgents(target);
185
+ appendGitignoreLines([".env.local", ".env.mvframe-notify"]);
169
186
 
170
187
  write(
171
188
  "src/main.js",
@@ -354,6 +371,12 @@ export default {
354
371
  // url: "//at.alicdn.com/t/c/your_font.js",
355
372
  // prefix: "ant",
356
373
  },
374
+ notify: {
375
+ enabled: true,
376
+ endpoint: import.meta.env.VITE_MVFRAME_NOTIFY_ENDPOINT || "http://127.0.0.1:3300",
377
+ token: import.meta.env.VITE_MVFRAME_NOTIFY_TOKEN || "",
378
+ timeout: 5000,
379
+ },
357
380
  table: {
358
381
  summaryMetric: null,
359
382
  },
@@ -732,6 +755,35 @@ export default defineConfig({
732
755
  );
733
756
  }
734
757
 
758
+ write(
759
+ ".env.mvframe-notify.example",
760
+ `# Copy to .env.mvframe-notify and fill values for the local Node notify service.
761
+ # Do not put DingTalk webhook/secret in src/main.js or other browser-bundled files.
762
+
763
+ MVFRAME_NOTIFY_PORT=3300
764
+ MVFRAME_NOTIFY_HOST=127.0.0.1
765
+ MVFRAME_NOTIFY_TOKEN=
766
+ MVFRAME_NOTIFY_TIMEOUT_MS=5000
767
+ MVFRAME_NOTIFY_CORS_ORIGIN=*
768
+
769
+ DINGTALK_WEBHOOK="https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxx"
770
+ DINGTALK_SECRET="SECxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
771
+ DINGTALK_AT_MOBILES=""
772
+ DINGTALK_AT_USER_IDS=""
773
+ DINGTALK_AT_ALL=""
774
+ `,
775
+ );
776
+
777
+ write(
778
+ ".env.local.example",
779
+ `# Copy to .env.local when you need the browser app to call mvframe-notify.
780
+ # VITE_MVFRAME_NOTIFY_TOKEN should match MVFRAME_NOTIFY_TOKEN in .env.mvframe-notify.
781
+
782
+ VITE_MVFRAME_NOTIFY_ENDPOINT=http://127.0.0.1:3300
783
+ VITE_MVFRAME_NOTIFY_TOKEN=
784
+ `,
785
+ );
786
+
735
787
  write(
736
788
  "MVFRAME-SCAFFOLD.md",
737
789
  `# MVFrame 雏形已生成
@@ -744,6 +796,12 @@ export default defineConfig({
744
796
  yarn dev
745
797
  \`\`\`
746
798
 
799
+ 如需同时启动 Vite 与 MVFrame 钉钉通知服务,可显式执行框架命令(与 \`mvframe-b\` 类似由 MVFrame 包提供,不会自动覆盖宿主脚本):
800
+
801
+ \`\`\`bash
802
+ yarn exec mvframe-d
803
+ \`\`\`
804
+
747
805
  若需跳过对 \`package.json\` 的修改:\`node scripts/scaffold-app.js --no-package-json\`。
748
806
 
749
807
  自动生成的 \`vite.config.js\` 已包含 \`unplugin-auto-import\`;\`dts: true\` 时类型默认写在项目根 \`auto-imports.d.ts\`。
@@ -778,7 +836,43 @@ yarn dev
778
836
 
779
837
  ## 子路径(按需)
780
838
 
781
- 发布包 \`exports\` 还提供 \`mvframe/composition\`、\`mvframe/util\`、\`mvframe/store\` 等,业务侧可 \`import { ... } from "mvframe/composition"\` 等,参见包内 \`package.json\` 的 \`exports\`。
839
+ 发布包 \`exports\` 还提供 \`mvframe/composition\`、\`mvframe/util\`、\`mvframe/store\`、\`mvframe/notify\` 等,业务侧可 \`import { ... } from "mvframe/composition"\` 等,参见包内 \`package.json\` 的 \`exports\`。
840
+
841
+ ## 钉钉通知(mvframe-notify)
842
+
843
+ 脚手架已初始化:
844
+
845
+ - \`src/config/index.js\` 的 \`notify\` 节点:浏览器端只保存本地服务地址和访问 token。
846
+ - \`.env.mvframe-notify.example\`:复制为 \`.env.mvframe-notify\`,填写 \`DINGTALK_WEBHOOK\` / \`DINGTALK_SECRET\`。
847
+ - \`.env.local.example\`:复制为 \`.env.local\`,填写 \`VITE_MVFRAME_NOTIFY_ENDPOINT\` / \`VITE_MVFRAME_NOTIFY_TOKEN\`。
848
+
849
+ 启动通知服务:
850
+
851
+ \`\`\`bash
852
+ yarn exec mvframe-notify
853
+ \`\`\`
854
+
855
+ 同时启动 Vite 和通知服务:
856
+
857
+ \`\`\`bash
858
+ yarn exec mvframe-d
859
+ \`\`\`
860
+
861
+ 前端发送:
862
+
863
+ \`\`\`js
864
+ await globalThis.$notify.send("需要发送到钉钉的消息");
865
+ \`\`\`
866
+
867
+ 或按需导入:
868
+
869
+ \`\`\`js
870
+ import { notify } from "mvframe/notify";
871
+
872
+ await notify("需要发送到钉钉的消息");
873
+ \`\`\`
874
+
875
+ 注意:钉钉 webhook / secret 不应写进 \`main.js\`、\`src/config/index.js\` 或任何会被 Vite 打包到浏览器的文件。
782
876
 
783
877
  ## 目录约定
784
878