ai-worklens-agent 0.1.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/README.md +166 -0
- package/package.json +22 -0
- package/src/cli.mjs +159 -0
- package/src/config.mjs +83 -0
- package/src/event-builder.mjs +77 -0
- package/src/hook-adapter.mjs +351 -0
- package/src/hook-smoke.mjs +105 -0
- package/src/install.mjs +602 -0
- package/src/mcp-server.mjs +364 -0
- package/src/publish-npm.mjs +149 -0
- package/src/queue.mjs +128 -0
- package/src/team-rollout.mjs +197 -0
- package/src/uploader.mjs +428 -0
package/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Client Agent
|
|
2
|
+
|
|
3
|
+
员工端静默采集代理,负责把 Codex/MCP/hook 等来源的事件标准化、脱敏、缓存并上传到中心端。
|
|
4
|
+
|
|
5
|
+
MVP 能力:
|
|
6
|
+
|
|
7
|
+
- 一次性安装和更新。
|
|
8
|
+
- 本地配置和健康检查。
|
|
9
|
+
- 自动会话边界。
|
|
10
|
+
- 本地脱敏。
|
|
11
|
+
- 本地上传队列。
|
|
12
|
+
- heartbeat。
|
|
13
|
+
- Codex hook adapter。
|
|
14
|
+
- MCP、Skill、插件、命令、验证、返工事件采集。
|
|
15
|
+
- 首次安装后后台静默更新,自动拉齐客户端、MCP、Hook 和插件文件版本。
|
|
16
|
+
|
|
17
|
+
员工不需要手动输入云效任务号,也不需要手动触发开始和结束。
|
|
18
|
+
|
|
19
|
+
## 当前命令
|
|
20
|
+
|
|
21
|
+
在项目根目录执行:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm run agent:status
|
|
25
|
+
npm run agent:install -- --target-dir ./.worklens-client --server-url http://127.0.0.1:8797 --tool codex
|
|
26
|
+
npm run agent:install -- --target-dir ./.worklens-client --server-url http://127.0.0.1:8797 --tool claude-code
|
|
27
|
+
npm run agent:install -- --target-dir ./.worklens-client --server-url http://127.0.0.1:8797 --tool opencode
|
|
28
|
+
npm run agent:sync -- --config ./.worklens-client/client.json
|
|
29
|
+
npm run agent:auto-update -- --config ./.worklens-client/client.json
|
|
30
|
+
npm run agent:checkin
|
|
31
|
+
npm run agent:flush
|
|
32
|
+
npm run agent:recover
|
|
33
|
+
npm run mcp
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
如果管理员已经把员工端发布到 npm 或企业私有 npm 源,可以使用 `npx` 首次安装:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx -y -p ai-worklens-agent@0.1.0 worklens-agent-install \
|
|
40
|
+
--server-url http://127.0.0.1:8797 \
|
|
41
|
+
--tool codex \
|
|
42
|
+
--employee-pinyin zhangsan
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
发布到公共 npm 源需要 npm 账号 token。Scoped 包名需要账号拥有对应 scope。
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm run client:npm:publish -- \
|
|
49
|
+
--package-name @your-org/ai-worklens-agent \
|
|
50
|
+
--version 0.2.0 \
|
|
51
|
+
--tag latest \
|
|
52
|
+
--dry-run
|
|
53
|
+
|
|
54
|
+
NPM_TOKEN=<npm_token> npm run client:npm:publish -- \
|
|
55
|
+
--package-name @your-org/ai-worklens-agent \
|
|
56
|
+
--version 0.2.0 \
|
|
57
|
+
--tag latest
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
如果管理员在官网发布了直链安装包,可以下载安装包后执行包内安装脚本:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
curl -fL http://127.0.0.1:8797/site/downloads/ai-worklens-codex-0.1.0.sh \
|
|
64
|
+
-o ai-worklens-install.sh
|
|
65
|
+
chmod +x ai-worklens-install.sh
|
|
66
|
+
./ai-worklens-install.sh zhangsan
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
记录一个通用事件:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm run agent -- event \
|
|
73
|
+
--event-type verification \
|
|
74
|
+
--title "运行测试" \
|
|
75
|
+
--content "执行 npm test 验证通过" \
|
|
76
|
+
--commands "npm test" \
|
|
77
|
+
--duration-seconds 60
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
模拟 Codex hook 输入:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
echo '{"event":"plugin_use","pluginName":"Spreadsheets","message":"整理报价清单"}' | npm run hook:codex
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
模拟 Claude Code 工具 hook 输入:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
echo '{"hook_event_name":"PostToolUse","tool_name":"Bash","tool_input":{"command":"npm test"}}' | npm run hook:codex -- --tool claude-code
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
常用环境变量:
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
WORKLENS_SERVER_URL=http://127.0.0.1:8797
|
|
96
|
+
WORKLENS_EMPLOYEE_ID=E001
|
|
97
|
+
WORKLENS_EMPLOYEE_NAME=张三
|
|
98
|
+
WORKLENS_DEPARTMENT=产品研发
|
|
99
|
+
WORKLENS_ROLE=前端工程师
|
|
100
|
+
WORKLENS_CLIENT_ID=zhangsan-mac
|
|
101
|
+
WORKLENS_TOOL=codex
|
|
102
|
+
WORKLENS_MODEL_PROVIDER=openai
|
|
103
|
+
WORKLENS_MODEL_NAME=gpt-5.4
|
|
104
|
+
WORKLENS_MODEL_VERSION=5.4
|
|
105
|
+
WORKLENS_CONFIG_FILE=/path/to/client.json
|
|
106
|
+
WORKLENS_QUEUE_FILE=/path/to/queue.json
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Hook 事件映射
|
|
110
|
+
|
|
111
|
+
| Hook 输入 | 标准事件 |
|
|
112
|
+
| --- | --- |
|
|
113
|
+
| `user_prompt` / `prompt` / `user_message` | `user_prompt` |
|
|
114
|
+
| `assistant_response` / `assistant_turn` | `assistant_response` |
|
|
115
|
+
| `skill_use` | `skill_use` |
|
|
116
|
+
| `plugin_use` | `plugin_use` |
|
|
117
|
+
| `mcp_tool_call` | `mcp_tool_call` |
|
|
118
|
+
| `UserPromptSubmit` / `PostToolUse` / `PreToolUse` | `user_prompt` / `tool_call` / `command` |
|
|
119
|
+
| `command` 且命令包含 test/lint/build/typecheck | `verification` |
|
|
120
|
+
| `command` 普通命令 | `command` |
|
|
121
|
+
| `error` / `failure` | `error` |
|
|
122
|
+
| `rework` / `retry` / `fix` | `rework` |
|
|
123
|
+
|
|
124
|
+
上传失败时事件会留在本地 `queue.json`,下次 `agent:flush`、`agent:recover`、后台巡检或新事件触发时继续发送。
|
|
125
|
+
|
|
126
|
+
## 离线缓存和恢复
|
|
127
|
+
|
|
128
|
+
- 每条事件先写入本地队列,再按批上传到中心端。
|
|
129
|
+
- 中心端不可用、HTTP 失败、超时或只接收了部分事件时,未确认事件继续保留在 `queue.json`。
|
|
130
|
+
- 队列会记录失败次数、最近错误和下次重试时间,默认指数退避,避免中心端故障时持续高频请求。
|
|
131
|
+
- 中心端恢复后执行 `npm run agent:recover` 会同步最新采集规则、按批补传队列并上报健康状态。
|
|
132
|
+
- 需要人工立即补传时可执行 `npm run agent:recover -- --force`,会忽略 `nextAttemptAt` 直接重试。
|
|
133
|
+
- 服务端按 `eventId` 幂等接收,同一事件重传不会重复入库;客户端根据服务端返回的 `eventIds` 清理已确认事件。
|
|
134
|
+
|
|
135
|
+
## MCP 工具
|
|
136
|
+
|
|
137
|
+
`npm run mcp` 会启动一个最小 MCP server,当前提供:
|
|
138
|
+
|
|
139
|
+
- `record_ai_event`
|
|
140
|
+
- `record_skill_use`
|
|
141
|
+
- `record_plugin_use`
|
|
142
|
+
- `record_mcp_tool_call`
|
|
143
|
+
- `record_verification`
|
|
144
|
+
- `client_checkin`
|
|
145
|
+
- `flush_queue`
|
|
146
|
+
- `recover_queue`
|
|
147
|
+
- `sync_remote_config`
|
|
148
|
+
|
|
149
|
+
这些工具都写入同一套中心端事件协议,失败时进入本地队列。
|
|
150
|
+
|
|
151
|
+
## 安装清单
|
|
152
|
+
|
|
153
|
+
`agent:install` 会输出员工端配置和安装清单:
|
|
154
|
+
|
|
155
|
+
- `client.json`:中心端地址、员工身份、上传配置。
|
|
156
|
+
- `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`:补传离线队列,拉取中心端静默更新策略,自动重写本地采集组件。
|
|
162
|
+
- `worklens-register-autoupdate.sh`:在 macOS 用户级 LaunchAgent 注册后台巡检任务。
|
|
163
|
+
- `worklens-unregister-autoupdate.sh`:移除后台巡检任务。
|
|
164
|
+
- `worklens-install-or-update.sh`:同步配置、静默更新、注册后台任务并自检。
|
|
165
|
+
|
|
166
|
+
在当前沙箱内建议指定 `--target-dir` 到项目目录;真实员工机器可以使用默认目录 `~/.ai-worklens`。
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-worklens-agent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Employee-side collector agent for AI WorkLens.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"worklens-agent": "src/cli.mjs",
|
|
8
|
+
"worklens-agent-install": "src/install.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"ai-observability",
|
|
16
|
+
"mcp",
|
|
17
|
+
"codex",
|
|
18
|
+
"claude-code",
|
|
19
|
+
"opencode"
|
|
20
|
+
],
|
|
21
|
+
"license": "UNLICENSED"
|
|
22
|
+
}
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { loadClientConfig, writeClientConfig, defaultConfigFile } from "./config.mjs";
|
|
3
|
+
import { buildEvent } from "./event-builder.mjs";
|
|
4
|
+
import { ClientAgent } from "./uploader.mjs";
|
|
5
|
+
|
|
6
|
+
async function readStdin() {
|
|
7
|
+
const chunks = [];
|
|
8
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
9
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
10
|
+
if (!raw) return {};
|
|
11
|
+
return JSON.parse(raw);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseArgs(argv) {
|
|
15
|
+
const result = { _: [] };
|
|
16
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
17
|
+
const arg = argv[index];
|
|
18
|
+
if (!arg.startsWith("--")) {
|
|
19
|
+
result._.push(arg);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const key = arg.slice(2);
|
|
23
|
+
const next = argv[index + 1];
|
|
24
|
+
if (!next || next.startsWith("--")) result[key] = true;
|
|
25
|
+
else {
|
|
26
|
+
result[key] = next;
|
|
27
|
+
index += 1;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function eventInputFromArgs(args, stdin) {
|
|
34
|
+
return {
|
|
35
|
+
...stdin,
|
|
36
|
+
eventType: args["event-type"] || args.eventType || args.type || stdin.eventType,
|
|
37
|
+
source: args.source || stdin.source,
|
|
38
|
+
title: args.title || stdin.title,
|
|
39
|
+
content: args.content || args.summary || stdin.content || stdin.summary,
|
|
40
|
+
durationSeconds: args.duration || args["duration-seconds"] || stdin.durationSeconds,
|
|
41
|
+
files: args.files || stdin.files,
|
|
42
|
+
commands: args.commands || stdin.commands,
|
|
43
|
+
localSessionId: args.session || args["session-id"] || stdin.localSessionId,
|
|
44
|
+
turnIndex: args.turn || args["turn-index"] || stdin.turnIndex,
|
|
45
|
+
skillName: args.skill || args["skill-name"] || stdin.skillName,
|
|
46
|
+
pluginName: args.plugin || args["plugin-name"] || stdin.pluginName,
|
|
47
|
+
mcpServer: args.mcp || args["mcp-server"] || stdin.mcpServer,
|
|
48
|
+
hookName: args.hook || args["hook-name"] || stdin.hookName
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function initConfig(args) {
|
|
53
|
+
const file = args.config || defaultConfigFile();
|
|
54
|
+
const value = {
|
|
55
|
+
serverUrl: args["server-url"] || "http://127.0.0.1:8797",
|
|
56
|
+
collectorToken: args["collector-token"] || "",
|
|
57
|
+
clientId: args["client-id"] || "",
|
|
58
|
+
clientVersion: "0.1.0",
|
|
59
|
+
tool: args.tool || "codex",
|
|
60
|
+
model: {
|
|
61
|
+
provider: args["model-provider"] || "",
|
|
62
|
+
name: args["model-name"] || "",
|
|
63
|
+
version: args["model-version"] || "",
|
|
64
|
+
family: args["model-family"] || ""
|
|
65
|
+
},
|
|
66
|
+
employee: {
|
|
67
|
+
id: args["employee-id"] || "",
|
|
68
|
+
name: args["employee-name"] || "",
|
|
69
|
+
pinyinName: args["employee-pinyin"] || "",
|
|
70
|
+
department: args.department || "",
|
|
71
|
+
role: args.role || ""
|
|
72
|
+
},
|
|
73
|
+
upload: {
|
|
74
|
+
timeoutMs: 5000,
|
|
75
|
+
batchSize: 50
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
writeClientConfig(file, value);
|
|
79
|
+
return { ok: true, configFile: file };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function main() {
|
|
83
|
+
const args = parseArgs(process.argv.slice(2));
|
|
84
|
+
const command = args._[0] || "event";
|
|
85
|
+
if (command === "init") {
|
|
86
|
+
process.stdout.write(`${JSON.stringify(initConfig(args), null, 2)}\n`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const config = loadClientConfig({
|
|
91
|
+
configFile: args.config,
|
|
92
|
+
queueFile: args.queue,
|
|
93
|
+
serverUrl: args["server-url"],
|
|
94
|
+
collectorToken: args["collector-token"],
|
|
95
|
+
employeeId: args["employee-id"],
|
|
96
|
+
employeeName: args["employee-name"],
|
|
97
|
+
employeePinyin: args["employee-pinyin"],
|
|
98
|
+
department: args.department,
|
|
99
|
+
role: args.role,
|
|
100
|
+
clientId: args["client-id"],
|
|
101
|
+
tool: args.tool,
|
|
102
|
+
modelProvider: args["model-provider"],
|
|
103
|
+
modelName: args["model-name"],
|
|
104
|
+
modelVersion: args["model-version"],
|
|
105
|
+
modelFamily: args["model-family"],
|
|
106
|
+
repoName: args["repo-name"],
|
|
107
|
+
workspaceRoot: args.workspace,
|
|
108
|
+
branch: args.branch
|
|
109
|
+
});
|
|
110
|
+
const agent = new ClientAgent(config);
|
|
111
|
+
|
|
112
|
+
if (command === "status") {
|
|
113
|
+
process.stdout.write(`${JSON.stringify(await agent.status(), null, 2)}\n`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (command === "doctor" || command === "self-check") {
|
|
117
|
+
process.stdout.write(`${JSON.stringify(await agent.doctor(), null, 2)}\n`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (command === "flush") {
|
|
121
|
+
process.stdout.write(`${JSON.stringify(await agent.flush({ force: args.force === true || args.force === "true" }), null, 2)}\n`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (command === "recover" || command === "retry-upload") {
|
|
125
|
+
process.stdout.write(`${JSON.stringify(await agent.recover({
|
|
126
|
+
maxBatches: args["max-batches"] || 20,
|
|
127
|
+
sync: args.sync !== "false",
|
|
128
|
+
checkin: args.checkin !== "false",
|
|
129
|
+
force: args.force === true || args.force === "true"
|
|
130
|
+
}), null, 2)}\n`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (command === "checkin") {
|
|
134
|
+
process.stdout.write(`${JSON.stringify(await agent.checkin({
|
|
135
|
+
mcpReady: args["mcp-ready"] !== "false",
|
|
136
|
+
hookReady: args["hook-ready"] !== "false",
|
|
137
|
+
issues: args.issues ? String(args.issues).split(",").map((item) => item.trim()).filter(Boolean) : []
|
|
138
|
+
}), null, 2)}\n`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (command === "sync-config") {
|
|
142
|
+
process.stdout.write(`${JSON.stringify(await agent.syncConfig({ write: args.write !== "false" }), null, 2)}\n`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (command === "auto-update") {
|
|
146
|
+
process.stdout.write(`${JSON.stringify(await agent.autoUpdate({ force: args.force === true || args.force === "true" }), null, 2)}\n`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const stdin = await readStdin();
|
|
151
|
+
const event = buildEvent(eventInputFromArgs(args, stdin), config);
|
|
152
|
+
const result = await agent.record(event);
|
|
153
|
+
process.stdout.write(`${JSON.stringify({ ...result, eventId: event.eventId, eventType: event.eventType }, null, 2)}\n`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
main().catch((error) => {
|
|
157
|
+
process.stderr.write(`${JSON.stringify({ ok: false, error: error.message })}\n`);
|
|
158
|
+
process.exitCode = 1;
|
|
159
|
+
});
|
package/src/config.mjs
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import { normalizeCollectionSettings } from "../../collector-protocol/src/collection-settings.mjs";
|
|
6
|
+
import { normalizeClientUpdatePolicy } from "../../collector-protocol/src/client-update-policy.mjs";
|
|
7
|
+
import { normalizeModelInfo } from "../../collector-protocol/src/model-info.mjs";
|
|
8
|
+
import { normalizeToolId } from "../../collector-protocol/src/tool-profiles.mjs";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_CONFIG_DIR = ".ai-worklens";
|
|
11
|
+
|
|
12
|
+
function envValue(env, key) {
|
|
13
|
+
return env[key];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readJson(filePath) {
|
|
17
|
+
try {
|
|
18
|
+
if (!fs.existsSync(filePath)) return {};
|
|
19
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
20
|
+
} catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function defaultClientId() {
|
|
26
|
+
const seed = `${os.hostname()}:${os.userInfo().username}`;
|
|
27
|
+
return `device_${crypto.createHash("sha256").update(seed).digest("hex").slice(0, 12)}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function defaultConfigFile() {
|
|
31
|
+
return envValue(process.env, "WORKLENS_CONFIG_FILE") || path.join(os.homedir(), DEFAULT_CONFIG_DIR, "client.json");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function defaultQueueFile(configFile = defaultConfigFile()) {
|
|
35
|
+
return envValue(process.env, "WORKLENS_QUEUE_FILE") || path.join(path.dirname(configFile), "queue.json");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function loadClientConfig(options = {}) {
|
|
39
|
+
const configFile = options.configFile || defaultConfigFile();
|
|
40
|
+
const fileConfig = readJson(configFile);
|
|
41
|
+
const env = process.env;
|
|
42
|
+
const serverUrl = options.serverUrl || envValue(env, "WORKLENS_SERVER_URL") || fileConfig.serverUrl || "http://127.0.0.1:8797";
|
|
43
|
+
const collection = normalizeCollectionSettings(options.collection || fileConfig.collection || {});
|
|
44
|
+
const employee = {
|
|
45
|
+
id: options.employeeId || envValue(env, "WORKLENS_EMPLOYEE_ID") || fileConfig.employee?.id || os.userInfo().username,
|
|
46
|
+
name: options.employeeName || envValue(env, "WORKLENS_EMPLOYEE_NAME") || fileConfig.employee?.name || os.userInfo().username,
|
|
47
|
+
pinyinName: options.employeePinyin || options.pinyinName || envValue(env, "WORKLENS_EMPLOYEE_PINYIN") || fileConfig.employee?.pinyinName || "",
|
|
48
|
+
department: options.department || envValue(env, "WORKLENS_DEPARTMENT") || fileConfig.employee?.department || "",
|
|
49
|
+
role: options.role || envValue(env, "WORKLENS_ROLE") || fileConfig.employee?.role || ""
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
configFile,
|
|
53
|
+
queueFile: options.queueFile || defaultQueueFile(configFile),
|
|
54
|
+
serverUrl,
|
|
55
|
+
collectorToken: options.collectorToken || envValue(env, "WORKLENS_COLLECTOR_TOKEN") || fileConfig.collectorToken || "",
|
|
56
|
+
clientId: options.clientId || envValue(env, "WORKLENS_CLIENT_ID") || fileConfig.clientId || defaultClientId(),
|
|
57
|
+
clientVersion: options.clientVersion || fileConfig.clientVersion || "0.1.0",
|
|
58
|
+
tool: normalizeToolId(options.tool || envValue(env, "WORKLENS_TOOL") || fileConfig.tool || "codex"),
|
|
59
|
+
model: normalizeModelInfo({
|
|
60
|
+
...fileConfig.model,
|
|
61
|
+
provider: options.modelProvider || envValue(env, "WORKLENS_MODEL_PROVIDER") || fileConfig.model?.provider,
|
|
62
|
+
name: options.modelName || envValue(env, "WORKLENS_MODEL_NAME") || fileConfig.model?.name,
|
|
63
|
+
version: options.modelVersion || envValue(env, "WORKLENS_MODEL_VERSION") || fileConfig.model?.version,
|
|
64
|
+
family: options.modelFamily || envValue(env, "WORKLENS_MODEL_FAMILY") || fileConfig.model?.family
|
|
65
|
+
}),
|
|
66
|
+
workspaceRoot: options.workspaceRoot || envValue(env, "WORKLENS_WORKSPACE_ROOT") || process.cwd(),
|
|
67
|
+
repoName: options.repoName || envValue(env, "WORKLENS_REPO_NAME") || path.basename(options.workspaceRoot || envValue(env, "WORKLENS_WORKSPACE_ROOT") || process.cwd()),
|
|
68
|
+
branch: options.branch || envValue(env, "WORKLENS_BRANCH") || fileConfig.branch || "",
|
|
69
|
+
employee,
|
|
70
|
+
upload: {
|
|
71
|
+
timeoutMs: Number(options.timeoutMs || envValue(env, "WORKLENS_UPLOAD_TIMEOUT_MS") || fileConfig.upload?.timeoutMs || 5000),
|
|
72
|
+
batchSize: Number(options.batchSize || envValue(env, "WORKLENS_UPLOAD_BATCH_SIZE") || fileConfig.upload?.batchSize || collection.uploadBatchSize || 50)
|
|
73
|
+
},
|
|
74
|
+
collection,
|
|
75
|
+
update: normalizeClientUpdatePolicy(options.update || fileConfig.update || {})
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function writeClientConfig(configFile, value) {
|
|
80
|
+
fs.mkdirSync(path.dirname(configFile), { recursive: true, mode: 0o700 });
|
|
81
|
+
fs.writeFileSync(configFile, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
82
|
+
fs.chmodSync(configFile, 0o600);
|
|
83
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { normalizeCollectorEvent, stableHash } from "../../collector-protocol/src/event-schema.mjs";
|
|
3
|
+
|
|
4
|
+
function parseList(value) {
|
|
5
|
+
if (!value) return [];
|
|
6
|
+
if (Array.isArray(value)) return value;
|
|
7
|
+
return String(value).split(",").map((item) => item.trim()).filter(Boolean);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function buildEvent(input = {}, config) {
|
|
11
|
+
const workspaceRoot = input.workspaceRoot || config.workspaceRoot || process.cwd();
|
|
12
|
+
const repoName = input.repoName || config.repoName || path.basename(workspaceRoot);
|
|
13
|
+
const metadata = input.metadata && typeof input.metadata === "object" ? input.metadata : {};
|
|
14
|
+
const event = normalizeCollectorEvent({
|
|
15
|
+
...input,
|
|
16
|
+
employeeId: input.employeeId || config.employee.id,
|
|
17
|
+
employeeName: input.employeeName || config.employee.name,
|
|
18
|
+
department: input.department || config.employee.department,
|
|
19
|
+
role: input.role || config.employee.role,
|
|
20
|
+
clientId: input.clientId || config.clientId,
|
|
21
|
+
tool: input.tool || config.tool,
|
|
22
|
+
model: input.model || {
|
|
23
|
+
provider: input.modelProvider || config.model?.provider,
|
|
24
|
+
name: input.modelName || config.model?.name,
|
|
25
|
+
version: input.modelVersion || config.model?.version,
|
|
26
|
+
family: input.modelFamily || config.model?.family
|
|
27
|
+
},
|
|
28
|
+
source: input.source || "client_agent",
|
|
29
|
+
eventType: input.eventType || "tool_call",
|
|
30
|
+
workspace: {
|
|
31
|
+
rootHash: input.workspace?.rootHash || stableHash(workspaceRoot),
|
|
32
|
+
repoName: input.workspace?.repoName || repoName,
|
|
33
|
+
branch: input.workspace?.branch || input.branch || config.branch
|
|
34
|
+
},
|
|
35
|
+
session: {
|
|
36
|
+
localSessionId: input.session?.localSessionId || input.localSessionId || `${repoName}_${new Date().toISOString().slice(0, 10)}`,
|
|
37
|
+
turnIndex: input.session?.turnIndex || input.turnIndex || 0,
|
|
38
|
+
idleClosed: Boolean(input.session?.idleClosed || input.idleClosed)
|
|
39
|
+
},
|
|
40
|
+
summary: {
|
|
41
|
+
title: input.summary?.title || input.title || input.eventType || "AI 使用事件",
|
|
42
|
+
content: input.summary?.content || input.content || input.summary || input.message || ""
|
|
43
|
+
},
|
|
44
|
+
metrics: {
|
|
45
|
+
durationSeconds: input.metrics?.durationSeconds ?? input.durationSeconds ?? 0,
|
|
46
|
+
inputChars: input.metrics?.inputChars,
|
|
47
|
+
promptQualityScore: input.metrics?.promptQualityScore
|
|
48
|
+
},
|
|
49
|
+
process: {
|
|
50
|
+
...(input.process && typeof input.process === "object" ? input.process : {}),
|
|
51
|
+
interactionType: input.interactionType || input.interaction_type || input.process?.interactionType,
|
|
52
|
+
codexMode: input.codexMode || input.codex_mode || input.mode || input.process?.codexMode,
|
|
53
|
+
previousMode: input.previousMode || input.previous_mode || input.fromMode || input.from_mode || input.process?.previousMode,
|
|
54
|
+
nextMode: input.nextMode || input.next_mode || input.toMode || input.to_mode || input.process?.nextMode,
|
|
55
|
+
phase: input.phase || input.process?.phase,
|
|
56
|
+
roundIndex: input.roundIndex || input.round_index || input.process?.roundIndex,
|
|
57
|
+
toolName: input.toolName || input.tool_name || input.process?.toolName,
|
|
58
|
+
toolStatus: input.toolStatus || input.tool_status || input.status || input.process?.toolStatus,
|
|
59
|
+
exitCode: input.exitCode ?? input.exit_code ?? input.process?.exitCode,
|
|
60
|
+
permissionDecision: input.permissionDecision || input.permission_decision || input.decision || input.process?.permissionDecision,
|
|
61
|
+
retryAttempt: input.retryAttempt || input.retry_attempt || input.process?.retryAttempt,
|
|
62
|
+
failureKind: input.failureKind || input.failure_kind || input.errorCode || input.error_code || input.process?.failureKind
|
|
63
|
+
},
|
|
64
|
+
refs: {
|
|
65
|
+
files: parseList(input.refs?.files || input.files),
|
|
66
|
+
commands: parseList(input.refs?.commands || input.commands)
|
|
67
|
+
},
|
|
68
|
+
metadata: {
|
|
69
|
+
...metadata,
|
|
70
|
+
skillName: input.skillName || metadata.skillName,
|
|
71
|
+
pluginName: input.pluginName || metadata.pluginName,
|
|
72
|
+
mcpServer: input.mcpServer || metadata.mcpServer,
|
|
73
|
+
hookName: input.hookName || metadata.hookName
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return event;
|
|
77
|
+
}
|