ai-worklens-agent 0.1.5 → 0.1.7
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 +25 -7
- package/package.json +7 -2
- package/src/config.mjs +10 -0
- package/src/hook-adapter.mjs +187 -8
- package/src/install.mjs +117 -23
- package/src/protocol/client-update-policy.mjs +1 -1
- package/src/publish-npm.mjs +4 -3
- package/src/runtime-state.mjs +79 -0
- package/src/uploader.mjs +117 -2
package/README.md
CHANGED
|
@@ -35,13 +35,23 @@ 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.7 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.
|
|
47
|
+
NPM_CONFIG_UPDATE_NOTIFIER=false npx -y --loglevel=error -p ai-worklens-agent@0.1.7 worklens-agent-install \
|
|
40
48
|
--server-url http://192.168.1.241:8797 \
|
|
41
49
|
--tool codex \
|
|
42
50
|
--employee-pinyin zhangsan
|
|
43
51
|
```
|
|
44
52
|
|
|
53
|
+
安装完成后请重新打开一个新的 Codex 会话。第一次触发 Hook 时,如果 Codex 提示信任或允许 Hook,请选择信任/允许;否则 `codex-hook.cmd` 手动验证可以成功,但真实 Codex 会话不会触发采集。
|
|
54
|
+
|
|
45
55
|
发布到公共 npm 源需要 npm 账号 token。Scoped 包名需要账号拥有对应 scope。
|
|
46
56
|
|
|
47
57
|
```bash
|
|
@@ -60,7 +70,7 @@ NPM_TOKEN=<npm_token> npm run client:npm:publish -- \
|
|
|
60
70
|
如果管理员在官网发布了直链安装包,可以下载安装包后执行包内安装脚本:
|
|
61
71
|
|
|
62
72
|
```bash
|
|
63
|
-
curl -fL http://192.168.1.241:8797/site/downloads/ai-worklens-codex-0.1.
|
|
73
|
+
curl -fL http://192.168.1.241:8797/site/downloads/ai-worklens-codex-0.1.7.sh \
|
|
64
74
|
-o ai-worklens-install.sh
|
|
65
75
|
chmod +x ai-worklens-install.sh
|
|
66
76
|
./ai-worklens-install.sh zhangsan
|
|
@@ -83,6 +93,14 @@ npm run agent -- event \
|
|
|
83
93
|
echo '{"event":"plugin_use","pluginName":"Spreadsheets","message":"整理报价清单"}' | npm run hook:codex
|
|
84
94
|
```
|
|
85
95
|
|
|
96
|
+
Windows 命令提示符验证 Codex hook 是否真实可执行:
|
|
97
|
+
|
|
98
|
+
```bat
|
|
99
|
+
echo {"hook_event_name":"UserPromptSubmit","prompt":"worklens windows smoke","session_id":"manual-smoke"} | "%USERPROFILE%\.ai-worklens\codex-hook.cmd"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
如果这条命令成功上报,但真实 Codex 会话仍没有数据,优先检查是否已经在 Codex 中信任/允许了 Hook,并确认使用的是安装后新打开的 Codex 会话。
|
|
103
|
+
|
|
86
104
|
模拟 Claude Code 工具 hook 输入:
|
|
87
105
|
|
|
88
106
|
```bash
|
|
@@ -154,11 +172,11 @@ WORKLENS_QUEUE_FILE=/path/to/queue.json
|
|
|
154
172
|
|
|
155
173
|
- `client.json`:中心端地址、员工身份、上传配置。
|
|
156
174
|
- `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`:补传离线队列,拉取中心端静默更新策略,自动重写本地采集组件。
|
|
175
|
+
- `codex-mcp-snippet.toml` / `codex-hook.sh` / `codex-hook.cmd`:Codex 配置片段。
|
|
176
|
+
- `claude-code-mcp.json` / `claude-code-hook.sh` / `claude-code-hook.cmd` / `claude-code-hooks-settings.json`:Claude Code 配置片段和官方 hooks events 覆盖。
|
|
177
|
+
- `opencode-mcp.jsonc` / `opencode-hook.sh` / `opencode-hook.cmd` / `opencode-ai-worklens-plugin.js`:OpenCode MCP 配置和本地插件事件覆盖。
|
|
178
|
+
- `worklens-checkin.sh` / `worklens-checkin.cmd`:同步远程规则、补传离线队列并上报健康状态。
|
|
179
|
+
- `worklens-auto-update.sh` / `worklens-auto-update.cmd`:补传离线队列,拉取中心端静默更新策略,自动重写本地采集组件。
|
|
162
180
|
- `worklens-register-autoupdate.sh`:在 macOS 用户级 LaunchAgent 注册后台巡检任务。
|
|
163
181
|
- `worklens-unregister-autoupdate.sh`:移除后台巡检任务。
|
|
164
182
|
- `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.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Employee-side collector agent for AI WorkLens.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,5 +18,10 @@
|
|
|
18
18
|
"claude-code",
|
|
19
19
|
"opencode"
|
|
20
20
|
],
|
|
21
|
-
"license": "UNLICENSED"
|
|
21
|
+
"license": "UNLICENSED",
|
|
22
|
+
"private": false,
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public",
|
|
25
|
+
"registry": "https://registry.npmjs.org"
|
|
26
|
+
}
|
|
22
27
|
}
|
package/src/config.mjs
CHANGED
|
@@ -121,6 +121,14 @@ export function defaultQueueFile(configFile = defaultConfigFile()) {
|
|
|
121
121
|
return envValue(process.env, "WORKLENS_QUEUE_FILE") || path.join(path.dirname(configFile), "queue.json");
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
export function defaultStateFile(configFile = defaultConfigFile()) {
|
|
125
|
+
return envValue(process.env, "WORKLENS_STATE_FILE") || path.join(path.dirname(configFile), "state.json");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function defaultLogDir(configFile = defaultConfigFile()) {
|
|
129
|
+
return envValue(process.env, "WORKLENS_LOG_DIR") || path.join(path.dirname(configFile), "logs");
|
|
130
|
+
}
|
|
131
|
+
|
|
124
132
|
export function loadClientConfig(options = {}) {
|
|
125
133
|
const configFile = options.configFile || defaultConfigFile();
|
|
126
134
|
const fileConfig = readJson(configFile);
|
|
@@ -150,6 +158,8 @@ export function loadClientConfig(options = {}) {
|
|
|
150
158
|
configFile,
|
|
151
159
|
homeDir,
|
|
152
160
|
queueFile: options.queueFile || defaultQueueFile(configFile),
|
|
161
|
+
stateFile: options.stateFile || defaultStateFile(configFile),
|
|
162
|
+
logDir: options.logDir || defaultLogDir(configFile),
|
|
153
163
|
serverUrl,
|
|
154
164
|
collectorToken: options.collectorToken || envValue(env, "WORKLENS_COLLECTOR_TOKEN") || fileConfig.collectorToken || "",
|
|
155
165
|
clientId: options.clientId || envValue(env, "WORKLENS_CLIENT_ID") || fileConfig.clientId || defaultClientId(),
|
package/src/hook-adapter.mjs
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { loadClientConfig } from "./config.mjs";
|
|
3
3
|
import { buildEvent } from "./event-builder.mjs";
|
|
4
4
|
import { ClientAgent } from "./uploader.mjs";
|
|
5
|
+
import { appendRuntimeLog, eventLogEntry, updateRuntimeState } from "./runtime-state.mjs";
|
|
6
|
+
import fs from "node:fs";
|
|
5
7
|
import path from "node:path";
|
|
6
8
|
import { fileURLToPath } from "node:url";
|
|
7
9
|
|
|
@@ -152,6 +154,10 @@ function modeFromPayload(payload) {
|
|
|
152
154
|
);
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
function sessionIdFromPayload(payload) {
|
|
158
|
+
return firstText(payload.sessionId, payload.session_id, payload.localSessionId, payload.conversationId, payload.conversation_id);
|
|
159
|
+
}
|
|
160
|
+
|
|
155
161
|
function skillNameFromPayload(payload) {
|
|
156
162
|
return firstText(
|
|
157
163
|
payload.skillName,
|
|
@@ -341,7 +347,51 @@ function fileRefsFromPayload(payload) {
|
|
|
341
347
|
payload.toolInput?.filePath;
|
|
342
348
|
}
|
|
343
349
|
|
|
344
|
-
function
|
|
350
|
+
function compactSummaryText(value, options = {}) {
|
|
351
|
+
const text = String(value || "").replace(/\s+/g, " ").trim();
|
|
352
|
+
if (!text) return "";
|
|
353
|
+
if (options.allowRaw) return text;
|
|
354
|
+
const longPatch = /\*\*\*\s+Begin Patch|\bdiff --git\b|(?:^|\s)@@\s|^\s*[+-]{3}\s/m.test(String(value || ""));
|
|
355
|
+
if (longPatch && text.length > 240) {
|
|
356
|
+
return "围绕代码补丁或文件修改进行协作,包含较长 diff/patch 内容,已在采集端摘要化。";
|
|
357
|
+
}
|
|
358
|
+
const longCommand = options.kind && ["command", "verification", "tool_call", "tool_result"].includes(options.kind);
|
|
359
|
+
if (longCommand && text.length > 420) {
|
|
360
|
+
return `${text.slice(0, 220)} ...(较长工具输入已摘要)`;
|
|
361
|
+
}
|
|
362
|
+
if (text.length <= 420) return text;
|
|
363
|
+
return `${text.slice(0, 260)} ... ${text.slice(-100)}`;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function allowRawContent(config, eventType) {
|
|
367
|
+
if (eventType === "user_prompt") return config.collection?.storeRawPrompts === true;
|
|
368
|
+
if (eventType === "assistant_response") return config.collection?.storeFullReplies === true;
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function isDiagnosticPayload(payload, args, name) {
|
|
373
|
+
const sessionId = sessionIdFromPayload(payload);
|
|
374
|
+
const text = [
|
|
375
|
+
payload.prompt,
|
|
376
|
+
payload.message,
|
|
377
|
+
payload.summary,
|
|
378
|
+
payload.content,
|
|
379
|
+
payload.title,
|
|
380
|
+
payload.input?.prompt,
|
|
381
|
+
payload.input?.message
|
|
382
|
+
].map((item) => String(item || "")).join(" ");
|
|
383
|
+
return Boolean(
|
|
384
|
+
args.diagnostic === true ||
|
|
385
|
+
args.diagnostic === "true" ||
|
|
386
|
+
payload.diagnostic === true ||
|
|
387
|
+
payload.metadata?.diagnostic === true ||
|
|
388
|
+
/^smoke-|^hook-smoke$|^manual-smoke$/i.test(sessionId) ||
|
|
389
|
+
/worklens\s+(?:windows\s+)?smoke|hook\s+smoke|ai-worklens\s+smoke/i.test(text) ||
|
|
390
|
+
/hook-smoke/i.test(name)
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function pickSummary(payload, name, eventType, config) {
|
|
345
395
|
if (eventType === "mode_change") {
|
|
346
396
|
const previousMode = previousModeFromPayload(payload);
|
|
347
397
|
const nextMode = nextModeFromPayload(payload);
|
|
@@ -390,18 +440,22 @@ function pickSummary(payload, name, eventType) {
|
|
|
390
440
|
payload.error ||
|
|
391
441
|
payload.status ||
|
|
392
442
|
`hook:${name}`;
|
|
393
|
-
return {
|
|
443
|
+
return {
|
|
444
|
+
title: compactSummaryText(title, { allowRaw: true }),
|
|
445
|
+
content: compactSummaryText(content, { allowRaw: allowRawContent(config, eventType), kind: eventType })
|
|
446
|
+
};
|
|
394
447
|
}
|
|
395
448
|
|
|
396
449
|
export function normalizeHookPayload(payload, args, config) {
|
|
397
450
|
const name = hookName(payload, args);
|
|
398
451
|
const eventType = eventTypeFromHook(name, payload);
|
|
399
|
-
const summary = pickSummary(payload, name, eventType);
|
|
452
|
+
const summary = pickSummary(payload, name, eventType, config);
|
|
400
453
|
const metadata = payload.metadata && typeof payload.metadata === "object" ? payload.metadata : {};
|
|
401
454
|
const skillName = skillNameFromPayload(payload);
|
|
402
455
|
const pluginName = pluginNameFromPayload(payload);
|
|
403
456
|
const mcpServer = mcpServerFromPayload(payload);
|
|
404
457
|
const toolName = toolNameFromPayload(payload);
|
|
458
|
+
const diagnostic = isDiagnosticPayload(payload, args, name);
|
|
405
459
|
return buildEvent({
|
|
406
460
|
...payload,
|
|
407
461
|
eventType,
|
|
@@ -409,7 +463,7 @@ export function normalizeHookPayload(payload, args, config) {
|
|
|
409
463
|
title: summary.title,
|
|
410
464
|
content: summary.content,
|
|
411
465
|
hookName: name,
|
|
412
|
-
localSessionId: payload
|
|
466
|
+
localSessionId: sessionIdFromPayload(payload),
|
|
413
467
|
turnIndex: payload.turnIndex || payload.turn_index,
|
|
414
468
|
skillName,
|
|
415
469
|
pluginName,
|
|
@@ -435,7 +489,8 @@ export function normalizeHookPayload(payload, args, config) {
|
|
|
435
489
|
permissionDecision: permissionDecisionFromPayload(payload),
|
|
436
490
|
exitCode: payload.exitCode ?? payload.exit_code,
|
|
437
491
|
success: payload.success,
|
|
438
|
-
status: statusFromPayload(payload)
|
|
492
|
+
status: statusFromPayload(payload),
|
|
493
|
+
diagnostic
|
|
439
494
|
},
|
|
440
495
|
process: {
|
|
441
496
|
interactionType: eventType,
|
|
@@ -453,6 +508,108 @@ export function normalizeHookPayload(payload, args, config) {
|
|
|
453
508
|
}, config);
|
|
454
509
|
}
|
|
455
510
|
|
|
511
|
+
function textFromContent(value, depth = 0) {
|
|
512
|
+
if (depth > 5 || value === undefined || value === null) return "";
|
|
513
|
+
if (typeof value === "string") return value;
|
|
514
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
515
|
+
if (Array.isArray(value)) return value.map((item) => textFromContent(item, depth + 1)).filter(Boolean).join(" ");
|
|
516
|
+
if (typeof value === "object") {
|
|
517
|
+
if (typeof value.text === "string") return value.text;
|
|
518
|
+
if (typeof value.content === "string") return value.content;
|
|
519
|
+
if (Array.isArray(value.content)) return textFromContent(value.content, depth + 1);
|
|
520
|
+
if (typeof value.message === "string") return value.message;
|
|
521
|
+
if (value.message && typeof value.message === "object") return textFromContent(value.message, depth + 1);
|
|
522
|
+
if (typeof value.summary === "string") return value.summary;
|
|
523
|
+
}
|
|
524
|
+
return "";
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function transcriptAssistantSummary(filePath) {
|
|
528
|
+
if (!filePath) return null;
|
|
529
|
+
try {
|
|
530
|
+
if (!fs.existsSync(filePath)) return null;
|
|
531
|
+
const stats = fs.statSync(filePath);
|
|
532
|
+
const maxBytes = 1_000_000;
|
|
533
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
534
|
+
const content = stats.size > maxBytes ? raw.slice(-maxBytes) : raw;
|
|
535
|
+
let lastAssistantText = "";
|
|
536
|
+
let turnIndex = 0;
|
|
537
|
+
for (const line of content.split(/\r?\n/)) {
|
|
538
|
+
if (!line.trim()) continue;
|
|
539
|
+
let item = null;
|
|
540
|
+
try {
|
|
541
|
+
item = JSON.parse(line);
|
|
542
|
+
} catch {
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
const message = item.message && typeof item.message === "object" ? item.message : item;
|
|
546
|
+
const role = String(message.role || item.role || item.type || "").toLowerCase();
|
|
547
|
+
const text = textFromContent(message.content || message.text || message.summary || message);
|
|
548
|
+
if (!text) continue;
|
|
549
|
+
if (role.includes("user")) turnIndex += 1;
|
|
550
|
+
if (role.includes("assistant")) lastAssistantText = text;
|
|
551
|
+
}
|
|
552
|
+
if (!lastAssistantText) return null;
|
|
553
|
+
return { text: lastAssistantText, turnIndex };
|
|
554
|
+
} catch {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function assistantSummaryFromPayload(payload) {
|
|
560
|
+
const direct = firstText(
|
|
561
|
+
payload.assistantSummary,
|
|
562
|
+
payload.assistant_summary,
|
|
563
|
+
payload.assistantResponse,
|
|
564
|
+
payload.assistant_response,
|
|
565
|
+
payload.responseSummary,
|
|
566
|
+
payload.response_summary,
|
|
567
|
+
payload.outputSummary,
|
|
568
|
+
payload.output_summary,
|
|
569
|
+
payload.output?.summary,
|
|
570
|
+
payload.output?.responseSummary,
|
|
571
|
+
payload.result?.summary
|
|
572
|
+
);
|
|
573
|
+
if (direct) return { text: direct, turnIndex: Number(payload.turnIndex || payload.turn_index || 0) };
|
|
574
|
+
return transcriptAssistantSummary(payload.transcript_path || payload.transcriptPath);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function assistantEventFromHook(payload, args, config, baseEvent) {
|
|
578
|
+
const name = hookName(payload, args);
|
|
579
|
+
if (baseEvent.eventType === "assistant_response") return null;
|
|
580
|
+
if (baseEvent.eventType !== "session_end") return null;
|
|
581
|
+
const summary = assistantSummaryFromPayload(payload);
|
|
582
|
+
if (!summary?.text) return null;
|
|
583
|
+
const diagnostic = Boolean(baseEvent.metadata?.diagnostic);
|
|
584
|
+
return buildEvent({
|
|
585
|
+
eventType: "assistant_response",
|
|
586
|
+
source: payload.source || `${config.tool}_hook`,
|
|
587
|
+
title: "AI 回复摘要",
|
|
588
|
+
content: compactSummaryText(summary.text, { allowRaw: allowRawContent(config, "assistant_response"), kind: "assistant_response" }),
|
|
589
|
+
localSessionId: sessionIdFromPayload(payload) || baseEvent.session?.localSessionId,
|
|
590
|
+
turnIndex: summary.turnIndex || payload.turnIndex || payload.turn_index,
|
|
591
|
+
durationSeconds: payload.assistantDurationSeconds || payload.assistant_duration_seconds || 0,
|
|
592
|
+
metadata: {
|
|
593
|
+
hookAdapter: "ai-worklens",
|
|
594
|
+
sourceTool: config.tool,
|
|
595
|
+
rawHookEvent: name,
|
|
596
|
+
hookEventName: payload.hook_event_name || payload.hookEventName || payload.event || payload.type || name,
|
|
597
|
+
derivedFrom: payload.transcript_path || payload.transcriptPath ? "transcript" : "hook_payload",
|
|
598
|
+
transcriptPath: payload.transcript_path || payload.transcriptPath,
|
|
599
|
+
diagnostic
|
|
600
|
+
},
|
|
601
|
+
process: {
|
|
602
|
+
interactionType: "assistant_response"
|
|
603
|
+
}
|
|
604
|
+
}, config);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
export function normalizeHookEvents(payload, args, config) {
|
|
608
|
+
const baseEvent = normalizeHookPayload(payload, args, config);
|
|
609
|
+
const assistantEvent = assistantEventFromHook(payload, args, config, baseEvent);
|
|
610
|
+
return assistantEvent ? [baseEvent, assistantEvent] : [baseEvent];
|
|
611
|
+
}
|
|
612
|
+
|
|
456
613
|
async function main() {
|
|
457
614
|
const args = parseArgs(process.argv.slice(2));
|
|
458
615
|
const payload = await readStdin();
|
|
@@ -475,10 +632,32 @@ async function main() {
|
|
|
475
632
|
workspaceRoot: args.workspace,
|
|
476
633
|
branch: args.branch
|
|
477
634
|
});
|
|
478
|
-
const
|
|
635
|
+
const events = normalizeHookEvents(payload, args, config);
|
|
479
636
|
const agent = new ClientAgent(config);
|
|
480
|
-
const
|
|
481
|
-
|
|
637
|
+
const results = [];
|
|
638
|
+
for (const event of events) {
|
|
639
|
+
appendRuntimeLog(config, "hook", {
|
|
640
|
+
action: "received",
|
|
641
|
+
rawHookEvent: event.metadata.rawHookEvent,
|
|
642
|
+
...eventLogEntry(event)
|
|
643
|
+
});
|
|
644
|
+
updateRuntimeState(config, {
|
|
645
|
+
lastHookAt: event.occurredAt,
|
|
646
|
+
lastHookEventType: event.eventType,
|
|
647
|
+
lastHookEventId: event.eventId,
|
|
648
|
+
lastHookRawEvent: event.metadata.rawHookEvent || "",
|
|
649
|
+
lastHookDiagnostic: Boolean(event.metadata.diagnostic)
|
|
650
|
+
});
|
|
651
|
+
results.push({ eventId: event.eventId, eventType: event.eventType, result: await agent.record(event) });
|
|
652
|
+
}
|
|
653
|
+
process.stdout.write(`${JSON.stringify({
|
|
654
|
+
ok: true,
|
|
655
|
+
hook: events[0]?.metadata.rawHookEvent,
|
|
656
|
+
eventType: events[0]?.eventType,
|
|
657
|
+
eventId: events[0]?.eventId,
|
|
658
|
+
eventIds: events.map((event) => event.eventId),
|
|
659
|
+
events: results
|
|
660
|
+
})}\n`);
|
|
482
661
|
}
|
|
483
662
|
|
|
484
663
|
if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1] || "")) {
|
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");
|
|
@@ -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
|
|
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
|
|
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(
|
|
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
|
|
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.
|
|
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 \"
|
|
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
|
|
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
|
|
959
|
+
timeout
|
|
866
960
|
});
|
|
867
|
-
const register = spawnSync(
|
|
961
|
+
const register = spawnSync(registerCommand, registerArgs, {
|
|
868
962
|
encoding: "utf8",
|
|
869
|
-
timeout
|
|
963
|
+
timeout
|
|
870
964
|
});
|
|
871
965
|
return {
|
|
872
966
|
ok: checkin.status === 0,
|
package/src/publish-npm.mjs
CHANGED
|
@@ -31,7 +31,7 @@ export function parseArgs(argv = []) {
|
|
|
31
31
|
}
|
|
32
32
|
options.packageName = options.packageName || process.env.WORKLENS_NPM_PACKAGE_NAME || "";
|
|
33
33
|
options.version = options.version || process.env.WORKLENS_NPM_VERSION || "";
|
|
34
|
-
options.token = options.token || process.env.NPM_TOKEN || "";
|
|
34
|
+
options.token = options.token || process.env.NPM_TOKEN || process.env.WORKLENS_NPM_TOKEN || "";
|
|
35
35
|
return options;
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -40,11 +40,12 @@ export function usage() {
|
|
|
40
40
|
"Usage:",
|
|
41
41
|
" npm run client:npm:publish -- --package-name <npm-package> --version <version> --tag latest --dry-run",
|
|
42
42
|
" NPM_TOKEN=<token> npm run client:npm:publish -- --package-name <npm-package> --version <version> --tag latest",
|
|
43
|
+
" WORKLENS_NPM_TOKEN=<token> npm run client:npm:publish -- --package-name <npm-package> --version <version> --tag latest",
|
|
43
44
|
"",
|
|
44
45
|
"Notes:",
|
|
45
46
|
" - Public npm scoped packages require --access public.",
|
|
46
47
|
" - For @scope/name, the npm account token must have permission for that scope.",
|
|
47
|
-
" - Token can be passed through NPM_TOKEN or --token; it is written only to a temporary .npmrc and deleted after publish."
|
|
48
|
+
" - Token can be passed through NPM_TOKEN, WORKLENS_NPM_TOKEN or --token; it is written only to a temporary .npmrc and deleted after publish."
|
|
48
49
|
].join("\n");
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -127,7 +128,7 @@ export function normalizeNpmPublishTag(value = "latest") {
|
|
|
127
128
|
|
|
128
129
|
export function publishToNpm(options = {}) {
|
|
129
130
|
if (!options.dryRun && !options.token) {
|
|
130
|
-
throw new Error("NPM_TOKEN is required for real public npm publish; use --dry-run to preview without token");
|
|
131
|
+
throw new Error("NPM_TOKEN or WORKLENS_NPM_TOKEN is required for real public npm publish; use --dry-run to preview without token");
|
|
131
132
|
}
|
|
132
133
|
let prepared = null;
|
|
133
134
|
try {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
function baseDir(config = {}) {
|
|
5
|
+
return path.dirname(config.configFile || config.queueFile || path.join(process.cwd(), "client.json"));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function runtimeStateFile(config = {}) {
|
|
9
|
+
return config.stateFile || path.join(baseDir(config), "state.json");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function runtimeLogDir(config = {}) {
|
|
13
|
+
return config.logDir || path.join(baseDir(config), "logs");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readJson(filePath, fallback) {
|
|
17
|
+
try {
|
|
18
|
+
if (!fs.existsSync(filePath)) return fallback;
|
|
19
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
20
|
+
} catch {
|
|
21
|
+
return fallback;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function writeJsonAtomic(filePath, value) {
|
|
26
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
27
|
+
const tempPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
|
|
28
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
29
|
+
fs.renameSync(tempPath, filePath);
|
|
30
|
+
fs.chmodSync(filePath, 0o600);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function readRuntimeState(config = {}) {
|
|
34
|
+
return readJson(runtimeStateFile(config), {});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function updateRuntimeState(config = {}, patch = {}) {
|
|
38
|
+
try {
|
|
39
|
+
const filePath = runtimeStateFile(config);
|
|
40
|
+
const current = readJson(filePath, {});
|
|
41
|
+
const next = {
|
|
42
|
+
...current,
|
|
43
|
+
...patch,
|
|
44
|
+
updatedAt: new Date().toISOString()
|
|
45
|
+
};
|
|
46
|
+
writeJsonAtomic(filePath, next);
|
|
47
|
+
return next;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function appendRuntimeLog(config = {}, name, entry = {}) {
|
|
54
|
+
try {
|
|
55
|
+
const directory = runtimeLogDir(config);
|
|
56
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
57
|
+
const filePath = path.join(directory, `${name}.log`);
|
|
58
|
+
const line = {
|
|
59
|
+
at: new Date().toISOString(),
|
|
60
|
+
...entry
|
|
61
|
+
};
|
|
62
|
+
fs.appendFileSync(filePath, `${JSON.stringify(line)}\n`, { mode: 0o600 });
|
|
63
|
+
fs.chmodSync(filePath, 0o600);
|
|
64
|
+
return filePath;
|
|
65
|
+
} catch {
|
|
66
|
+
return "";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function eventLogEntry(event = {}) {
|
|
71
|
+
return {
|
|
72
|
+
eventId: event.eventId,
|
|
73
|
+
eventType: event.eventType,
|
|
74
|
+
source: event.source,
|
|
75
|
+
sessionId: event.session?.localSessionId || "",
|
|
76
|
+
turnIndex: event.session?.turnIndex || 0,
|
|
77
|
+
diagnostic: Boolean(event.metadata?.diagnostic)
|
|
78
|
+
};
|
|
79
|
+
}
|
package/src/uploader.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { writeClientConfig } from "./config.mjs";
|
|
|
5
5
|
import { normalizeCollectionSettings } from "./protocol/collection-settings.mjs";
|
|
6
6
|
import { normalizeClientUpdatePolicy, updateNeeded } from "./protocol/client-update-policy.mjs";
|
|
7
7
|
import { installClient } from "./install.mjs";
|
|
8
|
+
import { appendRuntimeLog, eventLogEntry, readRuntimeState, updateRuntimeState } from "./runtime-state.mjs";
|
|
8
9
|
|
|
9
10
|
async function requestJson(url, config, options = {}) {
|
|
10
11
|
const controller = new AbortController();
|
|
@@ -69,7 +70,30 @@ export class ClientAgent {
|
|
|
69
70
|
|
|
70
71
|
async record(event) {
|
|
71
72
|
const length = await this.queue.enqueue(event);
|
|
73
|
+
appendRuntimeLog(this.config, "events", {
|
|
74
|
+
action: "enqueue",
|
|
75
|
+
queueSize: length,
|
|
76
|
+
...eventLogEntry(event)
|
|
77
|
+
});
|
|
78
|
+
updateRuntimeState(this.config, {
|
|
79
|
+
lastEventAt: event.occurredAt,
|
|
80
|
+
lastEventId: event.eventId,
|
|
81
|
+
lastEventType: event.eventType,
|
|
82
|
+
lastQueueSize: length
|
|
83
|
+
});
|
|
72
84
|
if (this.config.collection?.enabled === false) {
|
|
85
|
+
appendRuntimeLog(this.config, "upload", {
|
|
86
|
+
action: "skip",
|
|
87
|
+
reason: "collection_disabled",
|
|
88
|
+
queueSize: length,
|
|
89
|
+
...eventLogEntry(event)
|
|
90
|
+
});
|
|
91
|
+
updateRuntimeState(this.config, {
|
|
92
|
+
lastUploadStatus: "skipped",
|
|
93
|
+
lastUploadError: "collection_disabled",
|
|
94
|
+
lastUploadRemaining: length,
|
|
95
|
+
lastQueueSize: length
|
|
96
|
+
});
|
|
73
97
|
return {
|
|
74
98
|
ok: true,
|
|
75
99
|
queued: length,
|
|
@@ -108,6 +132,22 @@ export class ClientAgent {
|
|
|
108
132
|
await this.queue.markFailed(failedItems.map((item) => item.id), "partial_upload_not_accepted");
|
|
109
133
|
}
|
|
110
134
|
const stats = await this.queue.stats();
|
|
135
|
+
appendRuntimeLog(this.config, "upload", {
|
|
136
|
+
action: "flush",
|
|
137
|
+
ok: failedItems.length === 0,
|
|
138
|
+
sent: accepted,
|
|
139
|
+
remaining: stats.total,
|
|
140
|
+
postponed: stats.waiting,
|
|
141
|
+
partial: failedItems.length > 0
|
|
142
|
+
});
|
|
143
|
+
updateRuntimeState(this.config, {
|
|
144
|
+
lastUploadAt: new Date().toISOString(),
|
|
145
|
+
lastUploadStatus: failedItems.length === 0 ? "ok" : "partial",
|
|
146
|
+
lastUploadError: failedItems.length ? "partial_upload_not_accepted" : "",
|
|
147
|
+
lastUploadSent: accepted,
|
|
148
|
+
lastUploadRemaining: stats.total,
|
|
149
|
+
lastQueueSize: stats.total
|
|
150
|
+
});
|
|
111
151
|
return {
|
|
112
152
|
ok: failedItems.length === 0,
|
|
113
153
|
sent: accepted,
|
|
@@ -120,6 +160,22 @@ export class ClientAgent {
|
|
|
120
160
|
} catch (error) {
|
|
121
161
|
await this.queue.markFailed(batch.map((item) => item.id), error);
|
|
122
162
|
const stats = await this.queue.stats();
|
|
163
|
+
appendRuntimeLog(this.config, "upload", {
|
|
164
|
+
action: "flush",
|
|
165
|
+
ok: false,
|
|
166
|
+
sent: 0,
|
|
167
|
+
remaining: stats.total,
|
|
168
|
+
postponed: stats.waiting,
|
|
169
|
+
error: error.message
|
|
170
|
+
});
|
|
171
|
+
updateRuntimeState(this.config, {
|
|
172
|
+
lastUploadAt: new Date().toISOString(),
|
|
173
|
+
lastUploadStatus: "failed",
|
|
174
|
+
lastUploadError: error.message,
|
|
175
|
+
lastUploadSent: 0,
|
|
176
|
+
lastUploadRemaining: stats.total,
|
|
177
|
+
lastQueueSize: stats.total
|
|
178
|
+
});
|
|
123
179
|
return {
|
|
124
180
|
ok: false,
|
|
125
181
|
sent: 0,
|
|
@@ -180,6 +236,7 @@ export class ClientAgent {
|
|
|
180
236
|
|
|
181
237
|
async checkin(overrides = {}) {
|
|
182
238
|
const queueItems = await this.queue.list();
|
|
239
|
+
const runtime = readRuntimeState(this.config);
|
|
183
240
|
return postJson("/api/clients/checkin", {
|
|
184
241
|
employeeId: this.config.employee.id,
|
|
185
242
|
employeeName: this.config.employee.name,
|
|
@@ -193,7 +250,15 @@ export class ClientAgent {
|
|
|
193
250
|
mcpReady: overrides.mcpReady ?? true,
|
|
194
251
|
hookReady: overrides.hookReady ?? true,
|
|
195
252
|
uploadQueueSize: queueItems.length,
|
|
196
|
-
issues: overrides.issues || []
|
|
253
|
+
issues: overrides.issues || [],
|
|
254
|
+
lastHookAt: overrides.lastHookAt ?? runtime.lastHookAt ?? "",
|
|
255
|
+
lastHookEventType: overrides.lastHookEventType ?? runtime.lastHookEventType ?? "",
|
|
256
|
+
lastHookEventId: overrides.lastHookEventId ?? runtime.lastHookEventId ?? "",
|
|
257
|
+
lastUploadAt: overrides.lastUploadAt ?? runtime.lastUploadAt ?? "",
|
|
258
|
+
lastUploadStatus: overrides.lastUploadStatus ?? runtime.lastUploadStatus ?? "",
|
|
259
|
+
lastUploadError: overrides.lastUploadError ?? runtime.lastUploadError ?? "",
|
|
260
|
+
lastUploadSent: overrides.lastUploadSent ?? runtime.lastUploadSent ?? 0,
|
|
261
|
+
lastUploadRemaining: overrides.lastUploadRemaining ?? runtime.lastUploadRemaining ?? queueItems.length
|
|
197
262
|
}, this.config);
|
|
198
263
|
}
|
|
199
264
|
|
|
@@ -355,8 +420,11 @@ export class ClientAgent {
|
|
|
355
420
|
clientId: this.config.clientId,
|
|
356
421
|
configFile: this.config.configFile,
|
|
357
422
|
queueFile: this.config.queueFile,
|
|
423
|
+
stateFile: this.config.stateFile,
|
|
424
|
+
logDir: this.config.logDir,
|
|
358
425
|
queueSize: queueItems.length,
|
|
359
426
|
queue: queueStats,
|
|
427
|
+
runtime: readRuntimeState(this.config),
|
|
360
428
|
collection: this.config.collection,
|
|
361
429
|
update: this.config.update
|
|
362
430
|
};
|
|
@@ -410,6 +478,11 @@ export class ClientAgent {
|
|
|
410
478
|
codexHooks.enabled,
|
|
411
479
|
codexHooks.message
|
|
412
480
|
));
|
|
481
|
+
checks.push(check(
|
|
482
|
+
"codex_hooks_executable",
|
|
483
|
+
codexHooks.executable,
|
|
484
|
+
codexHooks.executableMessage || "Codex hook 指向的脚本不存在,或不是当前系统可执行的脚本格式"
|
|
485
|
+
));
|
|
413
486
|
status.codexHooks = codexHooks;
|
|
414
487
|
}
|
|
415
488
|
|
|
@@ -435,13 +508,18 @@ function codexHookDiagnostics(config) {
|
|
|
435
508
|
const configPath = path.join(homeDir, ".codex", "config.toml");
|
|
436
509
|
const hookEvents = new Set(["session_start", "pre_tool_use", "post_tool_use", "user_prompt_submit", "stop"]);
|
|
437
510
|
const configuredEvents = codexConfiguredHookEvents(hooksPath);
|
|
511
|
+
const hookCommands = codexConfiguredHookCommands(hooksPath);
|
|
438
512
|
const disabledStates = codexDisabledHookStates(configPath, hooksPath, hookEvents);
|
|
513
|
+
const executableState = codexHookExecutableState(hookCommands);
|
|
439
514
|
return {
|
|
440
515
|
hooksPath,
|
|
441
516
|
configPath,
|
|
442
517
|
configured: configuredEvents.length > 0,
|
|
443
518
|
configuredEvents,
|
|
519
|
+
hookCommands,
|
|
444
520
|
enabled: disabledStates.length === 0,
|
|
521
|
+
executable: executableState.ok,
|
|
522
|
+
executableMessage: executableState.message,
|
|
445
523
|
disabledStates,
|
|
446
524
|
message: disabledStates.length
|
|
447
525
|
? `Codex hook 已配置但未启用:${disabledStates.join("、")},请重新运行安装命令或执行 worklens-agent-install 修复。`
|
|
@@ -461,6 +539,20 @@ function codexConfiguredHookEvents(hooksPath) {
|
|
|
461
539
|
}
|
|
462
540
|
}
|
|
463
541
|
|
|
542
|
+
function codexConfiguredHookCommands(hooksPath) {
|
|
543
|
+
try {
|
|
544
|
+
if (!fs.existsSync(hooksPath)) return [];
|
|
545
|
+
const hooks = JSON.parse(fs.readFileSync(hooksPath, "utf8"));
|
|
546
|
+
return Object.values(hooks.hooks || {})
|
|
547
|
+
.flatMap((entries) => Array.isArray(entries) ? entries : [])
|
|
548
|
+
.flatMap((entry) => Array.isArray(entry.hooks) ? entry.hooks : [])
|
|
549
|
+
.map((hook) => String(hook.command || ""))
|
|
550
|
+
.filter((command) => workLensHookPattern().test(command));
|
|
551
|
+
} catch {
|
|
552
|
+
return [];
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
464
556
|
function entriesContainWorkLensHook(entries) {
|
|
465
557
|
return (Array.isArray(entries) ? entries : []).some((entry) => {
|
|
466
558
|
return (entry.hooks || []).some((hook) => workLensHookPattern().test(String(hook.command || "")));
|
|
@@ -468,7 +560,30 @@ function entriesContainWorkLensHook(entries) {
|
|
|
468
560
|
}
|
|
469
561
|
|
|
470
562
|
function workLensHookPattern() {
|
|
471
|
-
return /(?:ai[-_]?worklens|worklens|silent-ai-observatory|hook-adapter\.mjs|codex-hook\.sh)/i;
|
|
563
|
+
return /(?:ai[-_]?worklens|worklens|silent-ai-observatory|hook-adapter\.mjs|codex-hook\.(?:sh|cmd))/i;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function codexHookExecutableState(commands) {
|
|
567
|
+
if (!commands.length) return { ok: false, message: "未找到 AI WorkLens Codex hook 命令" };
|
|
568
|
+
const scriptPaths = commands.map(extractCodexHookScriptPath).filter(Boolean);
|
|
569
|
+
if (!scriptPaths.length) return { ok: false, message: "Codex hook 命令中未找到 codex-hook 脚本路径" };
|
|
570
|
+
for (const scriptPath of scriptPaths) {
|
|
571
|
+
if (!fs.existsSync(scriptPath)) return { ok: false, message: `Codex hook 脚本不存在:${scriptPath}` };
|
|
572
|
+
if (process.platform === "win32" && !/\.cmd$/i.test(scriptPath)) {
|
|
573
|
+
return { ok: false, message: `Windows 需要 codex-hook.cmd,当前是:${scriptPath}` };
|
|
574
|
+
}
|
|
575
|
+
if (process.platform !== "win32" && !/\.sh$/i.test(scriptPath)) {
|
|
576
|
+
return { ok: false, message: `macOS/Linux 需要 codex-hook.sh,当前是:${scriptPath}` };
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return { ok: true, message: "" };
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function extractCodexHookScriptPath(command) {
|
|
583
|
+
const quoted = String(command || "").match(/["']([^"']*codex-hook\.(?:sh|cmd))["']/i);
|
|
584
|
+
if (quoted) return quoted[1];
|
|
585
|
+
const unquoted = String(command || "").match(/([^\s]+codex-hook\.(?:sh|cmd))/i);
|
|
586
|
+
return unquoted ? unquoted[1] : "";
|
|
472
587
|
}
|
|
473
588
|
|
|
474
589
|
function codexDisabledHookStates(configPath, hooksPath, hookEvents) {
|