dsh-niuma 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +6 -0
- package/docs/preview-light-dark.png +0 -0
- package/lib/client.js +814 -217
- package/lib/client.js.map +2 -2
- package/lib/index.js +48 -118
- package/package.json +3 -8
- package/src/client/index.js +623 -164
- package/src/core.mjs +88 -0
- package/src/holidays.mjs +90 -10
package/lib/index.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* 牛马加速器(dsh-niuma)—— 宿主半侧。
|
|
3
3
|
*
|
|
4
4
|
* 职责(详见 SPEC.md §5):
|
|
5
|
-
* 1.
|
|
5
|
+
* 1. 设置:JSON 文件持久化($DSH_HOME/niuma/config.json,可用 NIUMA_CONFIG_PATH 覆盖)。
|
|
6
|
+
* —— 不依赖 DSH 的 settings 服务:desktop 0.2.1-rc.2 里该服务缺席时写接口会永远 400。
|
|
6
7
|
* 2. 路由:通过 ctx.webServer 暴露三条只读/写配置路由,供浏览器半侧 fetch:
|
|
7
8
|
* GET /niuma/config -> { ok, data: { config, defaults, holidaySource } }
|
|
8
9
|
* POST /niuma/settings -> 校验并持久化配置(body 上限 64KB)
|
|
@@ -10,14 +11,16 @@
|
|
|
10
11
|
* 3. 鉴权:全部走 connection.requestRejection 守卫(与 DSH 内建路由同一套 Host/Origin 围栏)。
|
|
11
12
|
*
|
|
12
13
|
* 可靠性约束(0.x 插件血泪教训):
|
|
13
|
-
* -
|
|
14
|
-
*
|
|
14
|
+
* - 配置读写只用 node:fs;目录/文件权限 0700/0600,原子写(tmp + rename)。
|
|
15
|
+
* 任何读失败都退回 cordis.patch.yml 的 config + core.DEFAULTS,绝不拖垮 DSH 启动。
|
|
15
16
|
* - apply() 内任何异常只写 warn 日志,绝不向上抛。
|
|
16
17
|
* - 路由注册统一挂在 ctx.effect 上并返回 dispose,卸载时干净摘除。
|
|
17
18
|
*
|
|
18
19
|
* 纯业务口径(工资公式、节假日判定)全在 src/core.mjs,宿主侧只做「存取配置」。
|
|
19
20
|
*/
|
|
20
|
-
import { readFileSync } from 'node:fs';
|
|
21
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
22
|
+
import { homedir } from 'node:os';
|
|
23
|
+
import { dirname, join } from 'node:path';
|
|
21
24
|
import { DEFAULTS, normalizeConfig, validateConfig } from '../src/core.mjs';
|
|
22
25
|
import { HOLIDAY_SOURCE } from '../src/holidays.mjs';
|
|
23
26
|
|
|
@@ -35,8 +38,8 @@ export const name = 'niuma';
|
|
|
35
38
|
|
|
36
39
|
/**
|
|
37
40
|
* 依赖的宿主服务。
|
|
38
|
-
* 只声明 webServer
|
|
39
|
-
*
|
|
41
|
+
* 只声明 webServer(插件唯一必需出口)。配置持久化走自己的 JSON 文件,
|
|
42
|
+
* 不依赖 DSH 的 settings 服务(desktop 0.2.1-rc.2 里该服务缺席 → 写接口会永远 400)。
|
|
40
43
|
*/
|
|
41
44
|
export const inject = ['webServer'];
|
|
42
45
|
|
|
@@ -44,44 +47,19 @@ const NAMESPACE = name;
|
|
|
44
47
|
const ROUTE_BASE = `/${NAMESPACE}`;
|
|
45
48
|
/** POST /niuma/settings 的请求体上限(64KB) */
|
|
46
49
|
const MAX_BODY_BYTES = 64 * 1024;
|
|
47
|
-
/** 与 core.mjs 的校验口径保持一致(月薪上限) */
|
|
48
|
-
const SALARY_MAX = 10_000_000;
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
export
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
monthlySalary: z.number().min(0).max(SALARY_MAX)
|
|
63
|
-
.default(DEFAULTS.monthlySalary)
|
|
64
|
-
.description('实收月薪(元/月,税后到手;>0,上限 10,000,000)'),
|
|
65
|
-
workStart: z.string().default(DEFAULTS.workStart).description('上班时间(HH:mm)'),
|
|
66
|
-
workEnd: z.string().default(DEFAULTS.workEnd).description('下班时间(HH:mm,必须晚于上班时间,暂不支持跨夜班)'),
|
|
67
|
-
lunchEnabled: z.boolean().default(DEFAULTS.lunchEnabled).description('是否启用午休'),
|
|
68
|
-
lunchStart: z.string().default(DEFAULTS.lunchStart).description('午休开始时间(HH:mm,启用午休时必填)'),
|
|
69
|
-
lunchEnd: z.string().default(DEFAULTS.lunchEnd).description('午休结束时间(HH:mm,必须晚于午休开始且不晚于下班)'),
|
|
70
|
-
lunchPaid: z.boolean().default(DEFAULTS.lunchPaid).description('午休是否算工时(算工时则日有效工时更长、时薪更低)'),
|
|
71
|
-
holidays: z.array(z.string()).default([...DEFAULTS.holidays])
|
|
72
|
-
.description('法定节假日(YYYY-MM-DD,一行一个)。放假日期不算工作日,日工资会相应变高'),
|
|
73
|
-
extraWorkdays: z.array(z.string()).default([...DEFAULTS.extraWorkdays])
|
|
74
|
-
.description('调休上班日(YYYY-MM-DD,一行一个)。周末补班日期算工作日'),
|
|
75
|
-
});
|
|
76
|
-
} catch {
|
|
77
|
-
return null; // 可选依赖缺失:退化为「只读默认值 + 写接口 400」
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** 单例 schema promise:避免每次 apply 都重新 import。 */
|
|
82
|
-
function configSchema() {
|
|
83
|
-
if (!schemaPromise) schemaPromise = loadConfigSchema();
|
|
84
|
-
return schemaPromise;
|
|
51
|
+
/**
|
|
52
|
+
* 配置文件的解析路径(纯函数,导出供烟测/文档):
|
|
53
|
+
* NIUMA_CONFIG_PATH → $DSH_HOME/niuma/config.json → ~/.dsh/niuma/config.json
|
|
54
|
+
* 只写自己目录,绝不碰 profile 的 package.json / lockfile。
|
|
55
|
+
* @param {object} [env]
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
export function resolveConfigPath(env = process.env) {
|
|
59
|
+
const override = typeof env?.NIUMA_CONFIG_PATH === 'string' ? env.NIUMA_CONFIG_PATH.trim() : '';
|
|
60
|
+
if (override) return override;
|
|
61
|
+
const home = typeof env?.DSH_HOME === 'string' && env.DSH_HOME.trim() ? env.DSH_HOME.trim() : join(homedir(), '.dsh');
|
|
62
|
+
return join(home, 'niuma', 'config.json');
|
|
85
63
|
}
|
|
86
64
|
|
|
87
65
|
/* ------------------------------------------------------------------ *
|
|
@@ -182,84 +160,41 @@ export async function apply(ctx, config = {}) {
|
|
|
182
160
|
const baseConfig = config && typeof config === 'object' && !Array.isArray(config) ? config : {};
|
|
183
161
|
const mountedAt = new Date().toISOString();
|
|
184
162
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
let settingsService = null;
|
|
163
|
+
/** 配置文件路径:NIUMA_CONFIG_PATH(测试/高级用户)→ $DSH_HOME/niuma/config.json → ~/.dsh/niuma/config.json。 */
|
|
164
|
+
const configPath = resolveConfigPath(process.env);
|
|
188
165
|
|
|
189
|
-
/**
|
|
190
|
-
|
|
191
|
-
* 永远返回补全 + 规范化后的完整配置,视图侧不用再自己 merge。
|
|
192
|
-
*/
|
|
193
|
-
const currentConfig = () => {
|
|
194
|
-
let stored = {};
|
|
166
|
+
/** 读已持久化的配置;文件不存在/坏 JSON 都退回空对象(用默认值继续跑,绝不抛)。 */
|
|
167
|
+
const readStoredConfig = () => {
|
|
195
168
|
try {
|
|
196
|
-
const
|
|
197
|
-
|
|
169
|
+
const raw = readFileSync(configPath, 'utf8');
|
|
170
|
+
const parsed = JSON.parse(raw);
|
|
171
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
198
172
|
} catch (error) {
|
|
199
|
-
logger?.warn?.(`niuma: settings read failed: ${error?.message ?? error}`);
|
|
173
|
+
if (error?.code !== 'ENOENT') logger?.warn?.(`niuma: settings read failed: ${error?.message ?? error}`);
|
|
174
|
+
return {};
|
|
200
175
|
}
|
|
201
|
-
return normalizeConfig({ ...DEFAULTS, ...baseConfig, ...stored }).config;
|
|
202
176
|
};
|
|
203
177
|
|
|
204
|
-
/**
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
if (scope && typeof scope.replace === 'function') {
|
|
211
|
-
await scope.replace(next);
|
|
212
|
-
return true;
|
|
213
|
-
}
|
|
214
|
-
if (settingsService && typeof settingsService.update === 'function') {
|
|
215
|
-
await settingsService.update(NAMESPACE, next);
|
|
216
|
-
return true;
|
|
217
|
-
}
|
|
218
|
-
return false;
|
|
219
|
-
};
|
|
178
|
+
/**
|
|
179
|
+
* 当前生效配置 = core.DEFAULTS ← 组合配置 ← 已保存的用户配置(后者覆盖前者)。
|
|
180
|
+
* 永远返回补全 + 规范化后的完整配置,视图侧不用再自己 merge。
|
|
181
|
+
*/
|
|
182
|
+
const currentConfig = () => normalizeConfig({ ...DEFAULTS, ...baseConfig, ...readStoredConfig() }).config;
|
|
220
183
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
settingsService = settingsCtx.settings ?? null;
|
|
234
|
-
try {
|
|
235
|
-
// watch 只是为了与设置页联动(当前版本没有需要失效的缓存),失败不影响主流程
|
|
236
|
-
if (typeof settingsCtx.effect === 'function') {
|
|
237
|
-
settingsCtx.effect(() => scope?.watch?.(() => {}), 'niuma: settings watch');
|
|
238
|
-
}
|
|
239
|
-
} catch (error) {
|
|
240
|
-
logger?.warn?.(`niuma: settings watch failed: ${error?.message ?? error}`);
|
|
241
|
-
}
|
|
242
|
-
logger?.info?.('niuma: settings namespace registered');
|
|
243
|
-
} catch (error) {
|
|
244
|
-
logger?.warn?.(`niuma: settings namespace unavailable: ${error?.message ?? error}`);
|
|
245
|
-
}
|
|
246
|
-
})
|
|
247
|
-
.catch((error) => {
|
|
248
|
-
logger?.warn?.(`niuma: settings schema load failed: ${error?.message ?? error}`);
|
|
249
|
-
});
|
|
184
|
+
/**
|
|
185
|
+
* 把校验后的完整配置写到自己的 JSON 文件(原子写)。
|
|
186
|
+
* 失败抛错(写接口返回 400),绝不静默吞掉。
|
|
187
|
+
*/
|
|
188
|
+
const persistConfig = async (next) => {
|
|
189
|
+
const dir = dirname(configPath);
|
|
190
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
191
|
+
const tmp = `${configPath}.tmp-${process.pid}`;
|
|
192
|
+
writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
|
193
|
+
renameSync(tmp, configPath);
|
|
194
|
+
try { chmodSync(configPath, 0o600); } catch { /* 权限收紧失败不影响内容 */ }
|
|
195
|
+
return true;
|
|
250
196
|
};
|
|
251
197
|
|
|
252
|
-
try {
|
|
253
|
-
if (typeof ctx.inject === 'function') {
|
|
254
|
-
ctx.inject(['settings'], registerSettings);
|
|
255
|
-
} else if (ctx.settings) {
|
|
256
|
-
// 极端情况下没有 ctx.inject(老内核/测试桩):直接尝试注册一次,失败也静默
|
|
257
|
-
registerSettings(ctx);
|
|
258
|
-
}
|
|
259
|
-
} catch (error) {
|
|
260
|
-
logger?.warn?.(`niuma: settings injection failed: ${error?.message ?? error}`);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
198
|
// ---- 路由守卫:webServer 自身不做鉴权,复用 connection 的 Host/Origin 围栏 ----
|
|
264
199
|
const guarded = (handler) => async (request, response) => {
|
|
265
200
|
let rejection;
|
|
@@ -345,18 +280,13 @@ export async function apply(ctx, config = {}) {
|
|
|
345
280
|
return;
|
|
346
281
|
}
|
|
347
282
|
|
|
348
|
-
let persisted = false;
|
|
349
283
|
try {
|
|
350
|
-
|
|
284
|
+
await persistConfig(next);
|
|
351
285
|
} catch (error) {
|
|
352
286
|
logger?.warn?.(`niuma: settings persist failed: ${error?.stack ?? error}`);
|
|
353
287
|
sendJson(response, 400, { ok: false, error: `写入设置失败:${error?.message ?? error}` });
|
|
354
288
|
return;
|
|
355
289
|
}
|
|
356
|
-
if (!persisted) {
|
|
357
|
-
sendJson(response, 400, { ok: false, error: '当前环境不支持写入设置(settings 服务不可用)' });
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
290
|
sendJson(response, 200, { ok: true, data: { config: next } });
|
|
361
291
|
};
|
|
362
292
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-niuma",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "牛马加速器 — DSH 工资换算实时计数器:按实收月薪、上下班时间、午休(可设是否算工时)、法定节假日自动折算秒/分/时/日/周/月工资,工资每秒滚动增长。Community salary ticker plugin for DeepSeek Harness.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"lib",
|
|
17
17
|
"src",
|
|
18
18
|
"cordis.patch.yml",
|
|
19
|
-
"docs
|
|
19
|
+
"docs/preview-light-dark.png",
|
|
20
20
|
"README.md",
|
|
21
21
|
"README.en.md",
|
|
22
22
|
"CHANGELOG.md",
|
|
@@ -68,19 +68,14 @@
|
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {},
|
|
70
70
|
"peerDependencies": {
|
|
71
|
-
"@deepseek-ai/cordis": "^4.0.2"
|
|
72
|
-
"@deepseek-ai/schemastery": "^3.18.2"
|
|
71
|
+
"@deepseek-ai/cordis": "^4.0.2"
|
|
73
72
|
},
|
|
74
73
|
"peerDependenciesMeta": {
|
|
75
74
|
"@deepseek-ai/cordis": {
|
|
76
75
|
"optional": true
|
|
77
|
-
},
|
|
78
|
-
"@deepseek-ai/schemastery": {
|
|
79
|
-
"optional": true
|
|
80
76
|
}
|
|
81
77
|
},
|
|
82
78
|
"devDependencies": {
|
|
83
|
-
"@deepseek-ai/schemastery": "3.18.4",
|
|
84
79
|
"esbuild": "^0.28.2"
|
|
85
80
|
},
|
|
86
81
|
"scripts": {
|