mvframe 1.0.72 → 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.72",
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,21 +40,30 @@
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",
47
+ "install-codex-rules": "node scripts/install-codex-agents.js",
41
48
  "scaffold-app": "node scripts/scaffold-app.js",
42
49
  "gen-icon": "node scripts/gen-iconfont-ant-names.js",
43
50
  "b": "node scripts/build-host.js"
44
51
  },
45
52
  "bin": {
53
+ "mvframe-b": "scripts/build-host.js",
54
+ "mvframe-d": "scripts/dev-with-notify.js",
46
55
  "mvframe-init-app": "scripts/scaffold-app.js",
56
+ "mvframe-install-codex-rules": "scripts/install-codex-agents.js",
47
57
  "mvframe-install-cursor-skill": "scripts/install-cursor-skill.js",
48
- "mvframe-b": "scripts/build-host.js"
58
+ "mvframe-notify": "scripts/notify-server.js"
49
59
  },
50
60
  "files": [
51
61
  "dist/*",
52
62
  "scripts/build-host.js",
63
+ "scripts/dev-with-notify.js",
64
+ "scripts/install-codex-agents.js",
53
65
  "scripts/install-cursor-skill.js",
66
+ "scripts/notify-server.js",
54
67
  "scripts/scaffold-app.js",
55
68
  ".cursor/skills/mvframe-app-init",
56
69
  ".cursor/rules"
@@ -72,7 +85,6 @@
72
85
  "devDependencies": {
73
86
  "@vitejs/plugin-vue": "^5.2.1",
74
87
  "@vue/shared": "^3.5.25",
75
- "devrq": "file:../devrq",
76
88
  "element-plus": "^2.13.6",
77
89
  "rollup-plugin-terser": "^7.0.2",
78
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,127 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 将 MVFrame 的 Codex 规则写入宿主项目 AGENTS.md。
4
+ *
5
+ * Codex 不会自动读取 Cursor 的 `.cursor/rules/*.mdc`,因此宿主项目需要
6
+ * `AGENTS.md` 明确告诉 Codex:优先使用 MVFrame 全局组件、全局方法和样式工具类。
7
+ *
8
+ * 用法:
9
+ * node path/to/mvframe/scripts/install-codex-agents.js
10
+ * node path/to/mvframe/scripts/install-codex-agents.js /abs/path/to/your-project
11
+ *
12
+ * 环境变量(可选):
13
+ * MVFRAME_CODEX_AGENTS_OUT=/path/to/project 等价于第一个参数
14
+ */
15
+
16
+ const fs = require("fs");
17
+ const path = require("path");
18
+
19
+ const BEGIN = "<!-- MVFRAME-CODEX-RULES:BEGIN -->";
20
+ const END = "<!-- MVFRAME-CODEX-RULES:END -->";
21
+
22
+ function renderCodexAgentsSection() {
23
+ return `${BEGIN}
24
+ # MVFrame Codex Rules
25
+
26
+ This project uses MVFrame. When editing Vue, JS, TS, SCSS, or CSS in this host app, prefer the framework globals before adding local implementations.
27
+
28
+ ## Global Components
29
+
30
+ - Prefer MVFrame global components in templates without local imports: \`Frame\`, \`Page\`, \`Table\`, \`Form\`, \`Input\`, \`Textarea\`, \`Select\`, \`SelectV2\`, \`Tabs\`, \`BtnGroup\`, \`Icon\`, \`Loading\`, \`Drawer\`, \`DrawerArea\`, \`Login\`, \`Lang\`.
31
+ - Do not rebuild an equivalent control with raw DOM, \`el-*\` combinations, or third-party components unless the MVFrame component cannot express the behavior.
32
+ - For segmented controls, single-choice button groups, login forms, page shells, tables, drawers, loading masks, and framework navigation, check the MVFrame component first.
33
+
34
+ ## Global Style Utilities
35
+
36
+ - Before writing scoped CSS, inline \`:style\`, or new SCSS utility code, check whether MVFrame global classes already cover it.
37
+ - MVFrame styles must be imported once from the app entry: \`import "mvframe/style"\` and \`import "mvframe/style/cpt"\`. If utility classes appear missing, fix the import before duplicating CSS.
38
+ - Prefer utility classes in templates for common layout, spacing, sizing, typography, color, border, scroll, hover, radius, and positioning work.
39
+
40
+ Common utility classes:
41
+
42
+ | Need | Prefer |
43
+ |------|--------|
44
+ | flex layout | \`flexMode\`, \`flexV\`, \`flex1\`, \`flexGrow\`, \`noShrink\`, \`minw0\`, \`minh0\` |
45
+ | flex alignment | \`hl\`, \`hc\`, \`hr\`, \`hb\`, \`ha\`, \`vl\`, \`vc\`, \`vr\`, \`vs\` |
46
+ | wrapping / gaps | \`flexWrap\`, \`grid\`, \`g2\` ... \`g40\` |
47
+ | spacing | \`p{N}\`, \`pt{N}\`, \`pr{N}\`, \`pb{N}\`, \`pl{N}\`, \`m{N}\`, \`mt{N}\`, \`mr{N}\`, \`mb{N}\`, \`ml{N}\` where N is one of \`0,2,4,6,8,10,12,16,20,24,30,32,36,40,48\` |
48
+ | sizing | \`w{N}\`, \`h{N}\`, \`minw{N}\`, \`maxw{N}\`, \`minh{N}\`, \`maxh{N}\`, \`wp25\`, \`wp50\`, \`wp75\`, \`wp100\`, \`hp100\`, \`vh100\` |
49
+ | text | \`fs{N}\`, \`lh{N}\`, \`fw300\`, \`fw500\`, \`fw700\`, \`txt-c\`, \`txt-r\`, \`txt-nowrap\`, \`txt-nowrap{N}\`, \`txt-wrap\`, \`txt-prewrap\` |
50
+ | color / background | \`txt-h1\`, \`txt-h2\`, \`txt-p\`, \`txt-tip\`, \`txt-gray\`, \`txt-primary\`, \`bg-primary\`, \`bg-page-header\`, \`border\`, \`border-t\`, \`border-b\`, \`border-none\` |
51
+ | positioning / effects | \`relative\`, \`abs\`, \`absFull\`, \`absCenter\`, \`sticky\`, \`z1\`, \`z2\`, \`z9\`, \`radius{N}\`, \`radiusP50\`, \`hover\`, \`trans-all\`, \`backdrop\` |
52
+ | interaction / scroll | \`point\`, \`grab\`, \`yscroll\`, \`xscroll\`, \`nobar\`, \`noscroll\`, \`noselect\`, \`noevent\`, \`hide\` |
53
+
54
+ Spacing rule: do not add odd-pixel \`margin\` or \`padding\` such as \`3px\`, \`5px\`, \`7px\`, \`9px\`, or \`11px\`. Use the MVFrame spacing classes or rem values aligned with the spacing scale.
55
+
56
+ Scoped styles are still allowed for component-specific behavior, complex selectors, custom brand visuals, third-party overrides, or values not covered by the utility system. Keep those scoped rules narrow and combine them with MVFrame classes.
57
+
58
+ ## Global Methods
59
+
60
+ - Prefer MVFrame globals instead of repeated imports or local helpers: \`globalThis.$d\`, \`$fa\`, \`$fu\`, \`$pm\`, \`$db\`, \`$copy\`, \`$deepClone\`, \`$getLang\`, \`$getImg\`, \`$sc\`, \`$c.info\`.
61
+ - In templates, call global methods directly when they are provided through Vue globalProperties.
62
+ - For ordinary JS modules, use \`globalThis.$xxx\` or the exported MVFrame subpath only when a direct import is explicitly needed.
63
+
64
+ ## Router, Store, And Maps
65
+
66
+ - Prefer route \`name\` navigation over raw \`path\` navigation.
67
+ - Use the MVFrame store factory pattern already wired by the app: \`inject("store")\` in components or \`import { store, pinia } from "mvframe/store"\` in setup code and guards.
68
+ - For MVFrame maps/i18n maps, prefer \`useMap\`, \`getMaps\`, \`patchMaps\`, \`mapLang\`, and \`mapLangPath\` from \`mvframe/maps\`.
69
+
70
+ ## Editing Checklist
71
+
72
+ - Before adding markup, check whether a MVFrame global component fits.
73
+ - Before adding CSS, check whether MVFrame utility classes fit.
74
+ - Before importing a helper library, check whether a MVFrame global helper already exists.
75
+ - If local CSS or a new helper is still needed, keep it small and specific to the component.
76
+ ${END}
77
+ `;
78
+ }
79
+
80
+ function upsertCodexAgents(projectRoot) {
81
+ const targetRoot = path.resolve(projectRoot);
82
+ const fp = path.join(targetRoot, "AGENTS.md");
83
+ const section = renderCodexAgentsSection();
84
+
85
+ let next;
86
+ if (fs.existsSync(fp)) {
87
+ const current = fs.readFileSync(fp, "utf8");
88
+ const start = current.indexOf(BEGIN);
89
+ const end = current.indexOf(END);
90
+ if (start >= 0 && end >= start) {
91
+ next = `${current.slice(0, start)}${section}${current.slice(end + END.length)}`;
92
+ } else {
93
+ const trimmed = current.replace(/\s*$/, "");
94
+ next = `${trimmed}\n\n${section}`;
95
+ }
96
+ } else {
97
+ next = `${section}`;
98
+ }
99
+
100
+ fs.writeFileSync(fp, next.endsWith("\n") ? next : `${next}\n`, "utf8");
101
+ console.log("[mvframe] 已写入 Codex 规则:");
102
+ console.log(" ", fp);
103
+ }
104
+
105
+ function main() {
106
+ const argPath = process.argv.find((a, i) => i >= 2 && !a.startsWith("--"));
107
+ const projectRoot = path.resolve(
108
+ process.env.MVFRAME_CODEX_AGENTS_OUT || argPath || process.cwd(),
109
+ );
110
+
111
+ if (!fs.existsSync(projectRoot)) {
112
+ console.error("[mvframe] 目录不存在:", projectRoot);
113
+ process.exit(1);
114
+ }
115
+
116
+ upsertCodexAgents(projectRoot);
117
+ console.log("[mvframe] Codex 打开该项目后会读取 AGENTS.md,并优先使用 MVFrame 全局能力。");
118
+ }
119
+
120
+ if (require.main === module) {
121
+ main();
122
+ }
123
+
124
+ module.exports = {
125
+ upsertCodexAgents,
126
+ renderCodexAgentsSection,
127
+ };
@@ -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
+ });