ai-worklens-agent 0.1.4 → 0.1.6

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/README.md CHANGED
@@ -35,8 +35,16 @@ npm run mcp
35
35
 
36
36
  如果管理员已经把员工端发布到 npm 或企业私有 npm 源,可以使用 `npx` 首次安装:
37
37
 
38
+ Windows 命令提示符或 PowerShell 使用一行命令,不要混用 macOS/Linux 的反斜杠换行:
39
+
40
+ ```bat
41
+ npx -y --loglevel=error --registry https://registry.npmjs.org -p ai-worklens-agent@0.1.6 worklens-agent-install --server-url http://192.168.1.241:8797 --tool codex --employee-pinyin zhangpeng
42
+ ```
43
+
44
+ macOS / Linux:
45
+
38
46
  ```bash
39
- NPM_CONFIG_UPDATE_NOTIFIER=false npx -y --loglevel=error -p ai-worklens-agent@0.1.4 worklens-agent-install \
47
+ NPM_CONFIG_UPDATE_NOTIFIER=false npx -y --loglevel=error -p ai-worklens-agent@0.1.6 worklens-agent-install \
40
48
  --server-url http://192.168.1.241:8797 \
41
49
  --tool codex \
42
50
  --employee-pinyin zhangsan
@@ -60,7 +68,7 @@ NPM_TOKEN=<npm_token> npm run client:npm:publish -- \
60
68
  如果管理员在官网发布了直链安装包,可以下载安装包后执行包内安装脚本:
61
69
 
62
70
  ```bash
63
- curl -fL http://192.168.1.241:8797/site/downloads/ai-worklens-codex-0.1.4.sh \
71
+ curl -fL http://192.168.1.241:8797/site/downloads/ai-worklens-codex-0.1.6.sh \
64
72
  -o ai-worklens-install.sh
65
73
  chmod +x ai-worklens-install.sh
66
74
  ./ai-worklens-install.sh zhangsan
@@ -83,6 +91,12 @@ npm run agent -- event \
83
91
  echo '{"event":"plugin_use","pluginName":"Spreadsheets","message":"整理报价清单"}' | npm run hook:codex
84
92
  ```
85
93
 
94
+ Windows 命令提示符验证 Codex hook 是否真实可执行:
95
+
96
+ ```bat
97
+ echo {"hook_event_name":"UserPromptSubmit","prompt":"worklens windows smoke","session_id":"manual-smoke"} | "%USERPROFILE%\.ai-worklens\codex-hook.cmd"
98
+ ```
99
+
86
100
  模拟 Claude Code 工具 hook 输入:
87
101
 
88
102
  ```bash
@@ -154,11 +168,11 @@ WORKLENS_QUEUE_FILE=/path/to/queue.json
154
168
 
155
169
  - `client.json`:中心端地址、员工身份、上传配置。
156
170
  - `install-manifest.json`:MCP server、Hook adapter、CLI event entrypoint。
157
- - `codex-mcp-snippet.toml` / `codex-hook.sh`:Codex 配置片段。
158
- - `claude-code-mcp.json` / `claude-code-hook.sh` / `claude-code-hooks-settings.json`:Claude Code 配置片段和官方 hooks events 覆盖。
159
- - `opencode-mcp.jsonc` / `opencode-hook.sh` / `opencode-ai-worklens-plugin.js`:OpenCode MCP 配置和本地插件事件覆盖。
160
- - `worklens-checkin.sh`:同步远程规则、补传离线队列并上报健康状态。
161
- - `worklens-auto-update.sh`:补传离线队列,拉取中心端静默更新策略,自动重写本地采集组件。
171
+ - `codex-mcp-snippet.toml` / `codex-hook.sh` / `codex-hook.cmd`:Codex 配置片段。
172
+ - `claude-code-mcp.json` / `claude-code-hook.sh` / `claude-code-hook.cmd` / `claude-code-hooks-settings.json`:Claude Code 配置片段和官方 hooks events 覆盖。
173
+ - `opencode-mcp.jsonc` / `opencode-hook.sh` / `opencode-hook.cmd` / `opencode-ai-worklens-plugin.js`:OpenCode MCP 配置和本地插件事件覆盖。
174
+ - `worklens-checkin.sh` / `worklens-checkin.cmd`:同步远程规则、补传离线队列并上报健康状态。
175
+ - `worklens-auto-update.sh` / `worklens-auto-update.cmd`:补传离线队列,拉取中心端静默更新策略,自动重写本地采集组件。
162
176
  - `worklens-register-autoupdate.sh`:在 macOS 用户级 LaunchAgent 注册后台巡检任务。
163
177
  - `worklens-unregister-autoupdate.sh`:移除后台巡检任务。
164
178
  - `worklens-install-or-update.sh`:同步配置、静默更新、注册后台任务并自检。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-worklens-agent",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Employee-side collector agent for AI WorkLens.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,10 +18,5 @@
18
18
  "claude-code",
19
19
  "opencode"
20
20
  ],
21
- "license": "UNLICENSED",
22
- "private": false,
23
- "publishConfig": {
24
- "access": "public",
25
- "registry": "https://registry.npmjs.org"
26
- }
21
+ "license": "UNLICENSED"
27
22
  }
package/src/config.mjs CHANGED
@@ -137,10 +137,12 @@ export function loadClientConfig(options = {}) {
137
137
  family: options.modelFamily || envValue(env, "WORKLENS_MODEL_FAMILY") || fileConfig.model?.family
138
138
  });
139
139
  const inferredModel = inferToolModel({ tool, homeDir, env });
140
+ const employeePinyin = options.employeePinyin || options.pinyinName || envValue(env, "WORKLENS_EMPLOYEE_PINYIN") || fileConfig.employee?.pinyinName || "";
141
+ const employeeId = options.employeeId || envValue(env, "WORKLENS_EMPLOYEE_ID") || fileConfig.employee?.id || employeePinyin || os.userInfo().username;
140
142
  const employee = {
141
- id: options.employeeId || envValue(env, "WORKLENS_EMPLOYEE_ID") || fileConfig.employee?.id || os.userInfo().username,
142
- name: options.employeeName || envValue(env, "WORKLENS_EMPLOYEE_NAME") || fileConfig.employee?.name || os.userInfo().username,
143
- pinyinName: options.employeePinyin || options.pinyinName || envValue(env, "WORKLENS_EMPLOYEE_PINYIN") || fileConfig.employee?.pinyinName || "",
143
+ id: employeeId,
144
+ name: options.employeeName || envValue(env, "WORKLENS_EMPLOYEE_NAME") || fileConfig.employee?.name || employeeId || os.userInfo().username,
145
+ pinyinName: employeePinyin,
144
146
  department: options.department || envValue(env, "WORKLENS_DEPARTMENT") || fileConfig.employee?.department || "",
145
147
  role: options.role || envValue(env, "WORKLENS_ROLE") || fileConfig.employee?.role || ""
146
148
  };
package/src/install.mjs CHANGED
@@ -38,6 +38,7 @@ function readPackageVersion() {
38
38
 
39
39
  export function buildInstallManifest(targetDir, options = {}) {
40
40
  const node = process.execPath;
41
+ const platform = normalizePlatform(options.platform);
41
42
  const selectedTool = normalizeToolId(options.tool || "codex");
42
43
  const profile = getToolProfile(selectedTool);
43
44
  const serverUrl = options.serverUrl || "http://127.0.0.1:8797";
@@ -50,6 +51,7 @@ export function buildInstallManifest(targetDir, options = {}) {
50
51
  const artifacts = buildToolArtifacts({
51
52
  targetDir,
52
53
  displayTargetDir,
54
+ platform,
53
55
  selectedTool,
54
56
  configFile,
55
57
  serverUrl,
@@ -61,6 +63,7 @@ export function buildInstallManifest(targetDir, options = {}) {
61
63
  const selectedArtifact = artifacts[selectedTool];
62
64
  return {
63
65
  version: 1,
66
+ platform,
64
67
  targetDir: displayTargetDir,
65
68
  selectedTool,
66
69
  supportedTools: listToolProfiles(),
@@ -93,6 +96,9 @@ export function buildInstallManifest(targetDir, options = {}) {
93
96
  autoUpdateScript: path.join(targetDir, "worklens-auto-update.sh"),
94
97
  registerAutoUpdateScript: path.join(targetDir, "worklens-register-autoupdate.sh"),
95
98
  unregisterAutoUpdateScript: path.join(targetDir, "worklens-unregister-autoupdate.sh"),
99
+ windowsCheckinScript: path.join(targetDir, "worklens-checkin.cmd"),
100
+ windowsSelfCheckScript: path.join(targetDir, "worklens-self-check.cmd"),
101
+ windowsAutoUpdateScript: path.join(targetDir, "worklens-auto-update.cmd"),
96
102
  windowsRegisterAutoUpdateScript: path.join(targetDir, "worklens-register-autoupdate.ps1"),
97
103
  installOrUpdateScript: path.join(targetDir, "worklens-install-or-update.sh"),
98
104
  readme: path.join(targetDir, "README.md")
@@ -112,6 +118,7 @@ export function buildInstallManifest(targetDir, options = {}) {
112
118
  }
113
119
 
114
120
  export function installClient(options = {}) {
121
+ const platform = normalizePlatform(options.platform);
115
122
  const targetDir = options.targetDir || path.join(os.homedir(), ".ai-worklens");
116
123
  const homeDir = options.homeDir || os.homedir();
117
124
  fs.mkdirSync(targetDir, { recursive: true, mode: 0o700 });
@@ -123,6 +130,9 @@ export function installClient(options = {}) {
123
130
  const autoUpdateScriptFile = path.join(targetDir, "worklens-auto-update.sh");
124
131
  const registerAutoUpdateScriptFile = path.join(targetDir, "worklens-register-autoupdate.sh");
125
132
  const unregisterAutoUpdateScriptFile = path.join(targetDir, "worklens-unregister-autoupdate.sh");
133
+ const windowsCheckinScriptFile = path.join(targetDir, "worklens-checkin.cmd");
134
+ const windowsSelfCheckScriptFile = path.join(targetDir, "worklens-self-check.cmd");
135
+ const windowsAutoUpdateScriptFile = path.join(targetDir, "worklens-auto-update.cmd");
126
136
  const windowsRegisterAutoUpdateScriptFile = path.join(targetDir, "worklens-register-autoupdate.ps1");
127
137
  const installOrUpdateScriptFile = path.join(targetDir, "worklens-install-or-update.sh");
128
138
  const readmeFile = path.join(targetDir, "README.md");
@@ -152,7 +162,7 @@ export function installClient(options = {}) {
152
162
  family: options.modelFamily || ""
153
163
  },
154
164
  employee: {
155
- id: options.employeeId || "",
165
+ id: options.employeeId || options.employeePinyin || options.pinyinName || "",
156
166
  name: options.employeeName || "",
157
167
  pinyinName: options.employeePinyin || options.pinyinName || "",
158
168
  department: options.department || "",
@@ -162,13 +172,13 @@ export function installClient(options = {}) {
162
172
  collection: options.collection || {},
163
173
  update: updatePolicy
164
174
  });
165
- const manifest = buildInstallManifest(targetDir, options);
175
+ const manifest = buildInstallManifest(targetDir, { ...options, platform });
166
176
  fs.writeFileSync(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
167
177
  fs.chmodSync(manifestFile, 0o600);
168
178
  for (const artifact of Object.values(manifest.toolArtifacts)) {
169
179
  fs.writeFileSync(artifact.configFile, `${artifact.config}\n`, { mode: 0o600 });
170
180
  fs.chmodSync(artifact.configFile, 0o600);
171
- fs.writeFileSync(artifact.hookFile, buildHookScript(artifact.hook.command, artifact.hook.args), { mode: 0o700 });
181
+ fs.writeFileSync(artifact.hookFile, buildHookScript(artifact.hook.command, artifact.hook.args, platform), { mode: 0o700 });
172
182
  fs.chmodSync(artifact.hookFile, 0o700);
173
183
  for (const file of Object.values(artifact.extraFiles || {})) {
174
184
  fs.writeFileSync(file.file, `${file.content}\n`, { mode: file.mode || 0o600 });
@@ -182,6 +192,12 @@ export function installClient(options = {}) {
182
192
  fs.chmodSync(selfCheckScriptFile, 0o700);
183
193
  fs.writeFileSync(autoUpdateScriptFile, buildAutoUpdateScript(manifest.eventCli.command, manifest.eventCli.args), { mode: 0o700 });
184
194
  fs.chmodSync(autoUpdateScriptFile, 0o700);
195
+ fs.writeFileSync(windowsCheckinScriptFile, buildWindowsCheckinScript(manifest.eventCli.command, manifest.eventCli.args), { mode: 0o600 });
196
+ fs.chmodSync(windowsCheckinScriptFile, 0o600);
197
+ fs.writeFileSync(windowsSelfCheckScriptFile, buildWindowsSelfCheckScript(manifest.eventCli.command, manifest.eventCli.args), { mode: 0o600 });
198
+ fs.chmodSync(windowsSelfCheckScriptFile, 0o600);
199
+ fs.writeFileSync(windowsAutoUpdateScriptFile, buildWindowsAutoUpdateScript(manifest.eventCli.command, manifest.eventCli.args), { mode: 0o600 });
200
+ fs.chmodSync(windowsAutoUpdateScriptFile, 0o600);
185
201
  fs.writeFileSync(registerAutoUpdateScriptFile, buildRegisterAutoUpdateScript(targetDir), { mode: 0o700 });
186
202
  fs.chmodSync(registerAutoUpdateScriptFile, 0o700);
187
203
  fs.writeFileSync(unregisterAutoUpdateScriptFile, buildUnregisterAutoUpdateScript(), { mode: 0o700 });
@@ -234,13 +250,26 @@ function upperFirst(value) {
234
250
  return input ? `${input[0].toUpperCase()}${input.slice(1)}` : input;
235
251
  }
236
252
 
237
- function buildToolArtifacts({ targetDir, displayTargetDir, configFile, serverUrl, mcpCommand, mcpArgs }) {
253
+ function normalizePlatform(value = process.platform) {
254
+ return String(value || process.platform).toLowerCase() === "win32" ? "win32" : "posix";
255
+ }
256
+
257
+ function isWindowsPlatform(platform = process.platform) {
258
+ return normalizePlatform(platform) === "win32";
259
+ }
260
+
261
+ function hookFileNameFor(profile, platform = process.platform) {
262
+ if (!isWindowsPlatform(platform)) return profile.hookFileName;
263
+ return profile.hookFileName.replace(/\.sh$/i, ".cmd");
264
+ }
265
+
266
+ function buildToolArtifacts({ targetDir, displayTargetDir, platform, configFile, serverUrl, mcpCommand, mcpArgs }) {
238
267
  return Object.fromEntries(listToolProfiles().map((profile) => {
239
268
  const hookArgs = [path.join(agentSrcDir, "hook-adapter.mjs"), "--config", configFile, "--tool", profile.id];
240
269
  const artifact = {
241
270
  profile,
242
271
  configFile: path.join(targetDir, profile.configFileName),
243
- hookFile: path.join(targetDir, profile.hookFileName),
272
+ hookFile: path.join(targetDir, hookFileNameFor(profile, platform)),
244
273
  hook: {
245
274
  name: profile.hookName,
246
275
  command: mcpCommand,
@@ -263,6 +292,7 @@ function buildToolArtifacts({ targetDir, displayTargetDir, configFile, serverUrl
263
292
  }),
264
293
  extraFiles: extraFilesFor(profile, {
265
294
  targetDir,
295
+ platform,
266
296
  configFile,
267
297
  mcpCommand,
268
298
  mcpArgs,
@@ -315,12 +345,12 @@ function toolConfigFor(profile, { configFile, serverUrl, mcpCommand, mcpArgs, ho
315
345
  return JSON.stringify({ command: mcpCommand, args: mcpArgs, env: { WORKLENS_CONFIG_FILE: configFile, WORKLENS_TOOL: profile.id, WORKLENS_SERVER_URL: serverUrl } }, null, 2);
316
346
  }
317
347
 
318
- function extraFilesFor(profile, { targetDir, hookCommand, hookArgs }) {
348
+ function extraFilesFor(profile, { targetDir, platform, hookCommand, hookArgs }) {
319
349
  if (profile.id === "claude-code") {
320
350
  return {
321
351
  hooksSettings: {
322
352
  file: path.join(targetDir, "claude-code-hooks-settings.json"),
323
- content: buildClaudeHooksSettings(hookCommand, hookArgs)
353
+ content: buildClaudeHooksSettings(hookCommand, hookArgs, platform)
324
354
  }
325
355
  };
326
356
  }
@@ -336,7 +366,7 @@ function extraFilesFor(profile, { targetDir, hookCommand, hookArgs }) {
336
366
  return {};
337
367
  }
338
368
 
339
- function buildClaudeHooksSettings(command, baseArgs) {
369
+ function buildClaudeHooksSettings(command, baseArgs, platform = process.platform) {
340
370
  const events = [
341
371
  "SessionStart",
342
372
  "ModeChange",
@@ -368,7 +398,7 @@ function buildClaudeHooksSettings(command, baseArgs) {
368
398
  hooks: [
369
399
  {
370
400
  type: "command",
371
- command: commandLine(command, [...baseArgs, "--event", eventName]),
401
+ command: commandLine(command, [...baseArgs, "--event", eventName], platform),
372
402
  timeout: 10
373
403
  }
374
404
  ]
@@ -412,6 +442,7 @@ function selectedIntegrationTools(value, manifest) {
412
442
 
413
443
  function installCodexIntegration(manifest, homeDir) {
414
444
  const artifact = manifest.toolArtifacts.codex;
445
+ const platform = normalizePlatform(manifest.platform);
415
446
  const configPath = path.join(homeDir, ".codex", "config.toml");
416
447
  const hooksPath = path.join(homeDir, ".codex", "hooks.json");
417
448
  const config = readTextConfig(configPath);
@@ -423,7 +454,7 @@ function installCodexIntegration(manifest, homeDir) {
423
454
 
424
455
  const hooks = readJsonConfig(hooksPath, { hooks: {} });
425
456
  hooks.hooks = hooks.hooks && typeof hooks.hooks === "object" ? hooks.hooks : {};
426
- const command = shellQuote(artifact.hookFile);
457
+ const command = isWindowsPlatform(platform) ? windowsCommandFileInvocation(artifact.hookFile) : shellQuote(artifact.hookFile);
427
458
  for (const eventName of ["SessionStart", "PreToolUse", "PostToolUse", "UserPromptSubmit", "Stop"]) {
428
459
  const entry = {
429
460
  hooks: [{ type: "command", command, ...(eventName === "Stop" ? { timeout: 30 } : {}) }]
@@ -492,7 +523,7 @@ function hookEntryContainsWorkLens(entry = {}) {
492
523
  }
493
524
 
494
525
  function workLensHookCommandPattern() {
495
- return /(?:ai[-_]?worklens|worklens|silent-ai-observatory|hook-adapter\.mjs|codex-hook\.sh|claude-code-hook\.sh|opencode-hook\.sh)/i;
526
+ return /(?:ai[-_]?worklens|worklens|silent-ai-observatory|hook-adapter\.mjs|codex-hook\.(?:sh|cmd)|claude-code-hook\.(?:sh|cmd)|opencode-hook\.(?:sh|cmd))/i;
496
527
  }
497
528
 
498
529
  function ensureHookEntry(current, entry) {
@@ -502,8 +533,8 @@ function ensureHookEntry(current, entry) {
502
533
  return [...list, entry];
503
534
  }
504
535
 
505
- function commandLine(command, args) {
506
- return [command, ...args].map(shellQuote).join(" ");
536
+ function commandLine(command, args, platform = process.platform) {
537
+ return [command, ...args].map((item) => quoteForShell(item, platform)).join(" ");
507
538
  }
508
539
 
509
540
  function readTextConfig(filePath) {
@@ -695,7 +726,20 @@ function shellQuote(value) {
695
726
  return `'${String(value).replaceAll("'", "'\\''")}'`;
696
727
  }
697
728
 
698
- function buildHookScript(command, args) {
729
+ function windowsQuote(value) {
730
+ return `"${String(value).replaceAll('"', '""')}"`;
731
+ }
732
+
733
+ function quoteForShell(value, platform = process.platform) {
734
+ return isWindowsPlatform(platform) ? windowsQuote(value) : shellQuote(value);
735
+ }
736
+
737
+ function windowsCommandFileInvocation(filePath) {
738
+ return `cmd.exe /d /s /c ${windowsQuote(filePath)}`;
739
+ }
740
+
741
+ function buildHookScript(command, args, platform = process.platform) {
742
+ if (isWindowsPlatform(platform)) return buildWindowsHookScript(command, args);
699
743
  return [
700
744
  "#!/usr/bin/env sh",
701
745
  "set -eu",
@@ -703,6 +747,14 @@ function buildHookScript(command, args) {
703
747
  ].join("\n") + "\n";
704
748
  }
705
749
 
750
+ function buildWindowsHookScript(command, args) {
751
+ return [
752
+ "@echo off",
753
+ "setlocal",
754
+ commandLine(command, args, "win32")
755
+ ].join("\r\n") + "\r\n";
756
+ }
757
+
706
758
  function buildCheckinScript(command, args) {
707
759
  return [
708
760
  "#!/usr/bin/env sh",
@@ -713,6 +765,17 @@ function buildCheckinScript(command, args) {
713
765
  ].join("\n") + "\n";
714
766
  }
715
767
 
768
+ function buildWindowsCheckinScript(command, args) {
769
+ const base = commandLine(command, args, "win32");
770
+ return [
771
+ "@echo off",
772
+ "setlocal",
773
+ `${base} sync-config`,
774
+ `${base} recover --sync false --checkin false`,
775
+ `${base} checkin`
776
+ ].join("\r\n") + "\r\n";
777
+ }
778
+
716
779
  function buildSelfCheckScript(command, args) {
717
780
  return [
718
781
  "#!/usr/bin/env sh",
@@ -721,6 +784,14 @@ function buildSelfCheckScript(command, args) {
721
784
  ].join("\n") + "\n";
722
785
  }
723
786
 
787
+ function buildWindowsSelfCheckScript(command, args) {
788
+ return [
789
+ "@echo off",
790
+ "setlocal",
791
+ `${commandLine(command, args, "win32")} doctor`
792
+ ].join("\r\n") + "\r\n";
793
+ }
794
+
724
795
  function buildAutoUpdateScript(command, args) {
725
796
  return [
726
797
  "#!/usr/bin/env sh",
@@ -730,6 +801,16 @@ function buildAutoUpdateScript(command, args) {
730
801
  ].join("\n") + "\n";
731
802
  }
732
803
 
804
+ function buildWindowsAutoUpdateScript(command, args) {
805
+ const base = commandLine(command, args, "win32");
806
+ return [
807
+ "@echo off",
808
+ "setlocal",
809
+ `${base} recover --checkin false`,
810
+ `${base} auto-update`
811
+ ].join("\r\n") + "\r\n";
812
+ }
813
+
733
814
  function buildRegisterAutoUpdateScript(targetDir) {
734
815
  const plist = "com.ai-worklens.autoupdate.plist";
735
816
  const interval = 1800;
@@ -793,11 +874,11 @@ function buildUnregisterAutoUpdateScript() {
793
874
  }
794
875
 
795
876
  function buildWindowsRegisterAutoUpdateScript(targetDir) {
796
- const autoUpdate = path.win32.join("%USERPROFILE%", ".ai-worklens", "worklens-auto-update.sh");
877
+ const autoUpdate = path.win32.join("%USERPROFILE%", ".ai-worklens", "worklens-auto-update.cmd");
797
878
  return [
798
879
  "$TaskName = \"AIWorkLensAutoUpdate\"",
799
880
  `$AutoUpdate = \"${autoUpdate}\"`,
800
- "$Action = New-ScheduledTaskAction -Execute \"wsl.exe\" -Argument \"sh $AutoUpdate\"",
881
+ "$Action = New-ScheduledTaskAction -Execute \"cmd.exe\" -Argument \"/d /s /c `\"$AutoUpdate`\"\"",
801
882
  "$Trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(5) -RepetitionInterval (New-TimeSpan -Hours 6)",
802
883
  "$Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries",
803
884
  "Register-ScheduledTask -TaskName $TaskName -Action $Action -Trigger $Trigger -Settings $Settings -Force",
@@ -841,12 +922,12 @@ function buildReadme(manifest) {
841
922
  `- client.json: 员工端配置和中心端下发规则。`,
842
923
  `- install-manifest.json: 安装清单。`,
843
924
  `- *-mcp.*: 各 AI 工具的 MCP 配置片段。`,
844
- `- *-hook.sh: 各 AI 工具的 hook adapter 启动脚本。`,
925
+ `- *-hook.sh / *-hook.cmd: 各 AI 工具的 hook adapter 启动脚本。`,
845
926
  `- claude-code-hooks-settings.json: Claude Code hooks 配置片段。`,
846
927
  `- opencode-ai-worklens-plugin.js: OpenCode 本地插件。`,
847
- `- worklens-checkin.sh: 同步规则、补传离线队列并上报健康状态。`,
848
- `- worklens-self-check.sh: 检查中心端连通性、本地配置和离线队列。`,
849
- `- worklens-auto-update.sh: 拉取中心端版本策略,并在中心端恢复后自动补传离线队列。`,
928
+ `- worklens-checkin.sh / worklens-checkin.cmd: 同步规则、补传离线队列并上报健康状态。`,
929
+ `- worklens-self-check.sh / worklens-self-check.cmd: 检查中心端连通性、本地配置和离线队列。`,
930
+ `- worklens-auto-update.sh / worklens-auto-update.cmd: 拉取中心端版本策略,并在中心端恢复后自动补传离线队列。`,
850
931
  `- worklens-register-autoupdate.sh: 在 macOS 用户级 LaunchAgent 注册后台静默更新和恢复上报任务。`,
851
932
  `- worklens-unregister-autoupdate.sh: 移除后台静默更新任务。`,
852
933
  `- worklens-install-or-update.sh: 同步中心端规则、上报健康并执行自检。`,
@@ -860,13 +941,26 @@ function buildReadme(manifest) {
860
941
 
861
942
  function runPostInstall(result, args = {}) {
862
943
  if (args["post-install"] === "false") return { ok: true, skipped: true };
863
- const checkin = spawnSync(result.generatedFiles.checkinScript, [], {
944
+ const platform = normalizePlatform(result.manifest?.platform || process.platform);
945
+ const timeout = Number(args["post-install-timeout-ms"] || 15000);
946
+ const checkinScript = isWindowsPlatform(platform)
947
+ ? result.generatedFiles.windowsCheckinScript
948
+ : result.generatedFiles.checkinScript;
949
+ const checkinCommand = isWindowsPlatform(platform) ? "cmd.exe" : checkinScript;
950
+ const checkinArgs = isWindowsPlatform(platform) ? ["/d", "/s", "/c", checkinScript] : [];
951
+ const registerCommand = isWindowsPlatform(platform)
952
+ ? "powershell.exe"
953
+ : result.generatedFiles.registerAutoUpdateScript;
954
+ const registerArgs = isWindowsPlatform(platform)
955
+ ? ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", result.generatedFiles.windowsRegisterAutoUpdateScript]
956
+ : [];
957
+ const checkin = spawnSync(checkinCommand, checkinArgs, {
864
958
  encoding: "utf8",
865
- timeout: Number(args["post-install-timeout-ms"] || 15000)
959
+ timeout
866
960
  });
867
- const register = spawnSync(result.generatedFiles.registerAutoUpdateScript, [], {
961
+ const register = spawnSync(registerCommand, registerArgs, {
868
962
  encoding: "utf8",
869
- timeout: Number(args["post-install-timeout-ms"] || 15000)
963
+ timeout
870
964
  });
871
965
  return {
872
966
  ok: checkin.status === 0,
@@ -1,4 +1,4 @@
1
- export const CLIENT_AGENT_VERSION = "0.1.4";
1
+ export const CLIENT_AGENT_VERSION = "0.1.6";
2
2
 
3
3
  export const DEFAULT_CLIENT_UPDATE_POLICY = {
4
4
  enabled: true,
package/src/queue.mjs CHANGED
@@ -7,11 +7,66 @@ const DEFAULT_RETRY = {
7
7
  baseDelayMs: 30 * 1000
8
8
  };
9
9
 
10
+ const LOCK_STALE_MS = 15 * 1000;
11
+ const LOCK_TIMEOUT_MS = 5 * 1000;
12
+
13
+ function sleep(ms) {
14
+ return new Promise((resolve) => setTimeout(resolve, ms));
15
+ }
16
+
17
+ function parseFirstJsonArray(raw) {
18
+ let started = false;
19
+ let depth = 0;
20
+ let inString = false;
21
+ let escaped = false;
22
+
23
+ for (let index = 0; index < raw.length; index += 1) {
24
+ const char = raw[index];
25
+ if (!started) {
26
+ if (/\s/.test(char)) continue;
27
+ if (char !== "[") return null;
28
+ started = true;
29
+ depth = 1;
30
+ continue;
31
+ }
32
+
33
+ if (inString) {
34
+ if (escaped) {
35
+ escaped = false;
36
+ } else if (char === "\\") {
37
+ escaped = true;
38
+ } else if (char === "\"") {
39
+ inString = false;
40
+ }
41
+ continue;
42
+ }
43
+
44
+ if (char === "\"") {
45
+ inString = true;
46
+ } else if (char === "[") {
47
+ depth += 1;
48
+ } else if (char === "]") {
49
+ depth -= 1;
50
+ if (depth === 0) {
51
+ const parsed = JSON.parse(raw.slice(0, index + 1));
52
+ return Array.isArray(parsed) ? parsed : null;
53
+ }
54
+ }
55
+ }
56
+ return null;
57
+ }
58
+
10
59
  async function readJsonArray(filePath) {
11
60
  try {
12
61
  const raw = await fs.readFile(filePath, "utf8");
13
- const parsed = JSON.parse(raw);
14
- return Array.isArray(parsed) ? parsed : [];
62
+ try {
63
+ const parsed = JSON.parse(raw);
64
+ return Array.isArray(parsed) ? parsed : [];
65
+ } catch (error) {
66
+ const recovered = parseFirstJsonArray(raw);
67
+ if (recovered) return recovered;
68
+ throw error;
69
+ }
15
70
  } catch (error) {
16
71
  if (error.code === "ENOENT") return [];
17
72
  throw error;
@@ -44,30 +99,96 @@ function retryDelay(attempts, options = {}) {
44
99
  return Math.min(maxDelayMs, baseDelayMs * (2 ** exponent));
45
100
  }
46
101
 
102
+ async function writeJsonArrayAtomic(filePath, items) {
103
+ const directory = path.dirname(filePath);
104
+ await fs.mkdir(directory, { recursive: true });
105
+ const tempPath = path.join(directory, `.${path.basename(filePath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
106
+ try {
107
+ await fs.writeFile(tempPath, `${JSON.stringify(items.map(normalizeItem), null, 2)}\n`, { mode: 0o600 });
108
+ await fs.rename(tempPath, filePath);
109
+ await fs.chmod(filePath, 0o600).catch(() => {});
110
+ } catch (error) {
111
+ await fs.unlink(tempPath).catch(() => {});
112
+ throw error;
113
+ }
114
+ }
115
+
116
+ async function acquireQueueLock(filePath, options = {}) {
117
+ const lockPath = `${filePath}.lock`;
118
+ const staleMs = Number(options.staleMs || LOCK_STALE_MS);
119
+ const timeoutMs = Number(options.timeoutMs || LOCK_TIMEOUT_MS);
120
+ const startedAt = Date.now();
121
+
122
+ while (true) {
123
+ try {
124
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
125
+ const handle = await fs.open(lockPath, "wx", 0o600);
126
+ await handle.writeFile(JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }));
127
+ return async () => {
128
+ await handle.close().catch(() => {});
129
+ await fs.unlink(lockPath).catch(() => {});
130
+ };
131
+ } catch (error) {
132
+ if (error.code !== "EEXIST") throw error;
133
+ const stats = await fs.stat(lockPath).catch(() => null);
134
+ if (stats && Date.now() - stats.mtimeMs > staleMs) {
135
+ await fs.unlink(lockPath).catch(() => {});
136
+ continue;
137
+ }
138
+ if (Date.now() - startedAt > timeoutMs) {
139
+ throw new Error(`queue lock timeout: ${lockPath}`);
140
+ }
141
+ await sleep(25 + Math.floor(Math.random() * 50));
142
+ }
143
+ }
144
+ }
145
+
47
146
  export class EventQueue {
48
147
  constructor(filePath) {
49
148
  this.filePath = filePath;
50
149
  }
51
150
 
52
- async list() {
151
+ async withLock(fn) {
152
+ const release = await acquireQueueLock(this.filePath);
153
+ try {
154
+ return await fn();
155
+ } finally {
156
+ await release();
157
+ }
158
+ }
159
+
160
+ async listUnlocked() {
53
161
  const items = await readJsonArray(this.filePath);
54
162
  return items.map(normalizeItem);
55
163
  }
56
164
 
165
+ async saveUnlocked(items) {
166
+ await writeJsonArrayAtomic(this.filePath, items);
167
+ }
168
+
169
+ async list() {
170
+ return this.listUnlocked();
171
+ }
172
+
57
173
  async save(items) {
58
- await fs.mkdir(path.dirname(this.filePath), { recursive: true });
59
- await fs.writeFile(this.filePath, `${JSON.stringify(items.map(normalizeItem), null, 2)}\n`, { mode: 0o600 });
174
+ await this.withLock(async () => {
175
+ await this.saveUnlocked(items);
176
+ });
60
177
  }
61
178
 
62
179
  async enqueue(event) {
63
- const items = await this.list();
64
- items.push(normalizeItem({ queuedAt: new Date().toISOString(), event }));
65
- await this.save(items);
66
- return items.length;
180
+ return this.withLock(async () => {
181
+ const items = await this.listUnlocked();
182
+ items.push(normalizeItem({ queuedAt: new Date().toISOString(), event }));
183
+ await this.saveUnlocked(items);
184
+ return items.length;
185
+ });
67
186
  }
68
187
 
69
188
  async replace(items) {
70
- await this.save(items);
189
+ await this.withLock(async () => {
190
+ await this.saveUnlocked(items);
191
+ });
71
192
  }
72
193
 
73
194
  async due({ now = new Date(), limit = Infinity, force = false } = {}) {
@@ -79,30 +200,34 @@ export class EventQueue {
79
200
  }
80
201
 
81
202
  async markFailed(failedIds, error, { now = new Date(), retry = DEFAULT_RETRY } = {}) {
82
- const idSet = new Set(failedIds);
83
- const nowIso = now instanceof Date ? now.toISOString() : new Date(now).toISOString();
84
- const items = await this.list();
85
- const marked = items.map((item) => {
86
- if (!idSet.has(item.id)) return item;
87
- const attempts = Number(item.attempts || 0) + 1;
88
- return {
89
- ...item,
90
- attempts,
91
- lastAttemptAt: nowIso,
92
- lastError: String(error?.message || error || "upload_failed"),
93
- nextAttemptAt: new Date(Date.parse(nowIso) + retryDelay(attempts, retry)).toISOString()
94
- };
203
+ return this.withLock(async () => {
204
+ const idSet = new Set(failedIds);
205
+ const nowIso = now instanceof Date ? now.toISOString() : new Date(now).toISOString();
206
+ const items = await this.listUnlocked();
207
+ const marked = items.map((item) => {
208
+ if (!idSet.has(item.id)) return item;
209
+ const attempts = Number(item.attempts || 0) + 1;
210
+ return {
211
+ ...item,
212
+ attempts,
213
+ lastAttemptAt: nowIso,
214
+ lastError: String(error?.message || error || "upload_failed"),
215
+ nextAttemptAt: new Date(Date.parse(nowIso) + retryDelay(attempts, retry)).toISOString()
216
+ };
217
+ });
218
+ await this.saveUnlocked(marked);
219
+ return marked;
95
220
  });
96
- await this.save(marked);
97
- return marked;
98
221
  }
99
222
 
100
223
  async remove(removeIds) {
101
- const idSet = new Set(removeIds);
102
- const items = await this.list();
103
- const remaining = items.filter((item) => !idSet.has(item.id));
104
- await this.save(remaining);
105
- return remaining;
224
+ return this.withLock(async () => {
225
+ const idSet = new Set(removeIds);
226
+ const items = await this.listUnlocked();
227
+ const remaining = items.filter((item) => !idSet.has(item.id));
228
+ await this.saveUnlocked(remaining);
229
+ return remaining;
230
+ });
106
231
  }
107
232
 
108
233
  async stats({ now = new Date() } = {}) {
@@ -123,6 +248,8 @@ export class EventQueue {
123
248
  }
124
249
 
125
250
  async clear() {
126
- await this.save([]);
251
+ await this.withLock(async () => {
252
+ await this.saveUnlocked([]);
253
+ });
127
254
  }
128
255
  }
package/src/uploader.mjs CHANGED
@@ -410,6 +410,11 @@ export class ClientAgent {
410
410
  codexHooks.enabled,
411
411
  codexHooks.message
412
412
  ));
413
+ checks.push(check(
414
+ "codex_hooks_executable",
415
+ codexHooks.executable,
416
+ codexHooks.executableMessage || "Codex hook 指向的脚本不存在,或不是当前系统可执行的脚本格式"
417
+ ));
413
418
  status.codexHooks = codexHooks;
414
419
  }
415
420
 
@@ -435,13 +440,18 @@ function codexHookDiagnostics(config) {
435
440
  const configPath = path.join(homeDir, ".codex", "config.toml");
436
441
  const hookEvents = new Set(["session_start", "pre_tool_use", "post_tool_use", "user_prompt_submit", "stop"]);
437
442
  const configuredEvents = codexConfiguredHookEvents(hooksPath);
443
+ const hookCommands = codexConfiguredHookCommands(hooksPath);
438
444
  const disabledStates = codexDisabledHookStates(configPath, hooksPath, hookEvents);
445
+ const executableState = codexHookExecutableState(hookCommands);
439
446
  return {
440
447
  hooksPath,
441
448
  configPath,
442
449
  configured: configuredEvents.length > 0,
443
450
  configuredEvents,
451
+ hookCommands,
444
452
  enabled: disabledStates.length === 0,
453
+ executable: executableState.ok,
454
+ executableMessage: executableState.message,
445
455
  disabledStates,
446
456
  message: disabledStates.length
447
457
  ? `Codex hook 已配置但未启用:${disabledStates.join("、")},请重新运行安装命令或执行 worklens-agent-install 修复。`
@@ -461,6 +471,20 @@ function codexConfiguredHookEvents(hooksPath) {
461
471
  }
462
472
  }
463
473
 
474
+ function codexConfiguredHookCommands(hooksPath) {
475
+ try {
476
+ if (!fs.existsSync(hooksPath)) return [];
477
+ const hooks = JSON.parse(fs.readFileSync(hooksPath, "utf8"));
478
+ return Object.values(hooks.hooks || {})
479
+ .flatMap((entries) => Array.isArray(entries) ? entries : [])
480
+ .flatMap((entry) => Array.isArray(entry.hooks) ? entry.hooks : [])
481
+ .map((hook) => String(hook.command || ""))
482
+ .filter((command) => workLensHookPattern().test(command));
483
+ } catch {
484
+ return [];
485
+ }
486
+ }
487
+
464
488
  function entriesContainWorkLensHook(entries) {
465
489
  return (Array.isArray(entries) ? entries : []).some((entry) => {
466
490
  return (entry.hooks || []).some((hook) => workLensHookPattern().test(String(hook.command || "")));
@@ -468,7 +492,30 @@ function entriesContainWorkLensHook(entries) {
468
492
  }
469
493
 
470
494
  function workLensHookPattern() {
471
- return /(?:ai[-_]?worklens|worklens|silent-ai-observatory|hook-adapter\.mjs|codex-hook\.sh)/i;
495
+ return /(?:ai[-_]?worklens|worklens|silent-ai-observatory|hook-adapter\.mjs|codex-hook\.(?:sh|cmd))/i;
496
+ }
497
+
498
+ function codexHookExecutableState(commands) {
499
+ if (!commands.length) return { ok: false, message: "未找到 AI WorkLens Codex hook 命令" };
500
+ const scriptPaths = commands.map(extractCodexHookScriptPath).filter(Boolean);
501
+ if (!scriptPaths.length) return { ok: false, message: "Codex hook 命令中未找到 codex-hook 脚本路径" };
502
+ for (const scriptPath of scriptPaths) {
503
+ if (!fs.existsSync(scriptPath)) return { ok: false, message: `Codex hook 脚本不存在:${scriptPath}` };
504
+ if (process.platform === "win32" && !/\.cmd$/i.test(scriptPath)) {
505
+ return { ok: false, message: `Windows 需要 codex-hook.cmd,当前是:${scriptPath}` };
506
+ }
507
+ if (process.platform !== "win32" && !/\.sh$/i.test(scriptPath)) {
508
+ return { ok: false, message: `macOS/Linux 需要 codex-hook.sh,当前是:${scriptPath}` };
509
+ }
510
+ }
511
+ return { ok: true, message: "" };
512
+ }
513
+
514
+ function extractCodexHookScriptPath(command) {
515
+ const quoted = String(command || "").match(/["']([^"']*codex-hook\.(?:sh|cmd))["']/i);
516
+ if (quoted) return quoted[1];
517
+ const unquoted = String(command || "").match(/([^\s]+codex-hook\.(?:sh|cmd))/i);
518
+ return unquoted ? unquoted[1] : "";
472
519
  }
473
520
 
474
521
  function codexDisabledHookStates(configPath, hooksPath, hookEvents) {